Spring Boot REST API
開発完全マスターガイド
初心者が必要とする「Javaの基本」から「現場の設計」までの網羅的知識
1. クラスとファイルの関係
1.1 物理的な構成ルール
Javaでは、一つの .java ファイルの中に含めることができる要素に厳格なルールがあります。
- public要素の制限: 1つのファイルに
public修飾子がついた要素(class, interface, enum)は最大1つだけです。 - ファイル名の一致: ファイル名は、その中にある
public要素の名前と完全に一致(大文字小文字も含む)させる必要があります。 - 非public要素:
publicを付けない要素であれば、1つのファイルに何個でも書けます。これらは同一パッケージ内でのみ使用される補助的な役割を果たします。
// ItemService.java というファイル名の場合
public interface ItemService {
// publicなインターフェース。ファイル名と一致必須。
void process();
}
abstract class BaseProcessor {
// 非publicな抽象クラス。同一ファイル内に記述可能。
}
class HelperTool {
// 非publicな普通のクラス。何個でも追加可能。
}
2. 命名規則の完全ルール
Javaには「誰が書いても同じように見える」ための強力な慣習があります。これに背くと非常に読みにくいコードになります。
| 対象 | 形式 | ルール・慣習 | 具体例 |
|---|---|---|---|
| クラス | PascalCase | 名詞。大文字で始める。 | UserController |
| インターフェース | PascalCase | 名詞・形容詞。"I"は付けない。 | UserService |
| 実装クラス | PascalCase | Implを末尾に付けるのが伝統。 | UserServiceImpl |
| メソッド | camelCase | 動詞で始める。小文字で始める。 | findUserById() |
| 変数 | camelCase | 意味のわかる名詞。小文字で始める。 | retryCount |
| 定数 | SNAKE_CASE | すべて大文字。単語間をアンダースコア。 | MAX_TIMEOUT |
| パッケージ | 小文字 | ドメインを逆にしたもの。 | com.example.app |
インターフェースに "I" を付けない理由
UserService)をインターフェースに与えます。
実装側に Impl と付けるのは、「これはあくまで実装の一つですよ」という控えめな表現なのです。
3. 型・アクセス修飾子
3.1 変数の基本型(全プリミティブ型)
Javaには8つのプリミティブ型があります。C++と異なり、long double は存在しませんが、代わりに BigDecimal という高精度な参照型を多用します。
| 分類 | 型 | 用途 | 初期値 | 例 |
|---|---|---|---|---|
| 整数 | byte | 8bit整数 (-128〜127) | 0 | byte b = 1; |
| short | 16bit整数 | 0 | short s = 100; | |
| int | 32bit整数(基本はこれ) | 0 | int i = 25; | |
| long | 64bit整数(IDなどに使用) | 0L | long id = 100L; | |
| 浮動小数点 | float | 32bit小数(物理計算など) | 0.0f | float f = 0.5f; |
| double | 64bit小数(小数の標準) | 0.0 | double d = 0.05; | |
| 文字 | char | 16bit Unicode文字 | '\u0000' | char c = 'A'; |
| 論理 | boolean | 真偽値 | false | boolean b = true; |
| 参照型 | String | 文字列 | null | String s = "Hello"; |
Javaにはありません。精度が非常に重要な計算(銀行、会計システムなど)では、
double も誤差が生じるため使用しません。代わりに java.math.BigDecimal というクラスを使用して、10進数として厳密な計算を行います。
4. 文字列操作・比較・型変換
s1 == s2 は絶対NG。中身の比較は必ず s1.equals(s2) を使用してください。
4.1 主要なStringメソッド
String s = "Spring Boot";
s.toUpperCase(); // "SPRING BOOT"
s.toLowerCase(); // "spring boot"
s.startsWith("Spring"); // true
s.contains(" "); // true
s.substring(0, 6); // "Spring"
// ===== よく使う基本メソッド =====
s.equals("Spring Boot"); // true(文字列の内容比較)
s.equalsIgnoreCase("spring boot"); // true(大文字小文字を無視)
s.replace("Boot", "Framework"); // "Spring Framework"
s.replaceAll(" ", "_"); // "Spring_Boot"(正規表現対応)
s.length(); // 11(文字数)
s.indexOf("Boot"); // 7(見つからない場合は -1)
s.trim(); // 前後の空白を除去
s.isEmpty(); // false(長さが0か)
String[] parts = s.split(" "); // {"Spring", "Boot"}
4.2 型の相互変換 (BigDecimal含む)
// --- String -> 各型 ---
int i = Integer.parseInt("10");
double d = Double.parseDouble("3.14");
// BigDecimalへの変換 (文字列を渡すのが最も安全)
BigDecimal bd = new BigDecimal("1234.567");
// --- 各型 -> String ---
String s1 = String.valueOf(100);
String s2 = String.valueOf(3.14);
String s3 = bd.toString(); // BigDecimalを文字列へ
4.3 数値操作 (Mathクラス)
Javaの標準 Math クラスには、数学的な計算を行うための静的メソッドが豊富に用意されています。
double val = 3.55;
// 基本の丸め処理
Math.round(val); // 4 (四捨五入してlongを返す)
Math.ceil(val); // 4.0 (切り上げ)
Math.floor(val); // 3.0 (切り捨て)
// その他よく使うもの
Math.abs(-10); // 10 (絶対値)
Math.max(10, 20); // 20 (大きい方)
Math.min(10, 20); // 10 (小さい方)
Math.pow(2, 3); // 8.0 (2の3乗)
Math.sqrt(16); // 4.0 (平方根)
Math.random(); // 0.0以上1.0未満の乱数
お金の計算などで
BigDecimal を使う場合は、Mathクラスではなく setScale() メソッドを使います。bd.setScale(0, RoundingMode.HALF_UP); // 四捨五入
5. 全演算子(算術・代入・論理・ビット)
5.1 算術演算子 & インクリメント
+,-,*,/,%(基本演算)++(インクリメント: 1増やす) /--(デクリメント: 1減らす)
int n = 10;
n++; // n は 11 になる
System.out.println(++n); // 先に増やしてから表示 (12)
System.out.println(n--); // 表示してから減らす (12を表示後、nは11になる)
5.2 代入演算子
=(基本代入)+=,-=,*=,/=,%=(複合代入)
int x = 5;
x += 10; // x = x + 10 と同じ。結果は 15。
x *= 2; // x = x * 2 と同じ。結果は 30。
5.3 論理・ビット演算子
Javaでは数値リテラルとして、2進数(0b始まり)や16進数(0x始まり)を直接記述できます。
int a = 0b1010; // 2進数 (10進数で10)
int b = 0b1100; // 2進数 (10進数で12)
int hex = 0xFF; // 16進数 (10進数で255)
// ビット演算の実例
int res1 = a & b; // AND: 両方1なら1 (1000 = 8)
int res2 = a | b; // OR : どちらか1なら1 (1110 = 14)
int res3 = a ^ b; // XOR: 異なれば1 (0110 = 6)
int res4 = ~a; // NOT: 反転
// 論理演算 (条件分岐で使用)
boolean logic = (a > 5) && (b < 20); // AND (かつ)
5.4 三項演算子
条件によって代入する値を1行で切り替える際に非常に便利です。
// 書式: (条件式) ? 真の場合の値 : 偽の場合の値
int age = 20;
String type = (age >= 18) ? "Adult" : "Minor";
// これは以下のif文と同じ意味です
if (age >= 18) { type = "Adult"; } else { type = "Minor"; }
6. 比較・ループ・Switch構文
6.1 分岐(if-else)
if (score >= 90) {
System.out.println("Excellent");
} else if (score >= 70) {
System.out.println("Good");
} else {
System.out.println("Retry");
}
6.2 Switch文とSwitch式
従来の Switch文 (breakが必要)
break を忘れると、下のケースまで実行(フォールスルー)されてしまうため注意が必要です。
switch (day) {
case 1:
System.out.println("Mon");
break;
default:
System.out.println("Other");
break;
}
最新の Switch式 (Java 14+)
-> を使うことで break が不要になり、値を直接変数に代入できます。
String msg = switch (status) {
case 200 -> "Success";
case 404 -> "Not Found";
default -> "Unknown";
};
6.3 ループ(for / while)
標準的な for ループ
回数が決まっている処理に適しています。
for (int i = 0; i < 5; i++) {
System.out.println("Count: " + i);
}
拡張 for 文 (foreach)
配列やリストの全要素を順番に処理する、最も安全で推奨される方法です。
List<String> list = List.of("A", "B", "C");
for (String s : list) {
System.out.println(s);
}
while と do-while
do-while は条件に関わらず最低1回は必ず実行されるのが最大の特徴です。
// while: 最初から条件判定(1回も動かない可能性あり)
while (count < 0) { ... }
// do-while: 実行してから判定(必ず1回は動く)
do {
System.out.println("必ず1回は表示されます");
} while (false);
6.4 ループの制御(break / continue)
for (int i = 0; i < 10; i++) {
if (i == 3) continue; // 3の時だけスキップして次へ
if (i == 7) break; // 7になったらループ自体を即終了
System.out.println(i);
}
7. 配列・リスト・セット・辞書(Map) の全メソッド
7.1 Array (配列)
サイズが固定されている最も基本的なコンテナです。
String[] fruits = {"Apple", "Banana"};
// --- 要素へのアクセス ---
String first = fruits[0]; // 0番目を取得
fruits[1] = "Orange"; // 書き換え
// --- 要素数 (sizeではありません) ---
int len = fruits.length; // 2 (フィールドなので括弧なし)
// --- できないこと ---
// fruits.indexOf("Apple"); // 配列にこのメソッドはありません
// fruits.add("Cherry"); // サイズ変更は不可能です
7.2 List (動的リスト)
順序を保持し、重複を許容します。API開発で最も使われます。
List<String> list = new ArrayList<>();
// --- 追加・挿入 ---
list.add("A"); // 末尾に追加
list.add(0, "Insert"); // 指定位置への挿入(Insert)
// --- 検索・取得 ---
list.get(0); // 指定位置の要素取得
list.contains("A"); // true (存在確認)
list.indexOf("A"); // 1 (位置取得。なければ-1)
list.size(); // 要素数 (メソッドなので括弧あり)
list.isEmpty(); // 空かどうか
// --- 削除の仕様 (要注意) ---
boolean b = list.remove("Z"); // 値指定: 消せたらtrue。対象がなくても例外は出ない
String s = list.remove(0); // index指定: 削除した要素を返す。範囲外は例外発生
// --- ソート ---
list.sort(Comparator.naturalOrder());
// --- ループ (拡張for文) ---
for (String item : list) {
System.out.println(item);
}
List<User> の contains / indexOf はそのまま使える?
結論:
そのままでは期待どおりに使えません。
equals()(できれば hashCode() も)を
User クラスに実装する必要があります。
なぜ使えないのか
List.contains() や List.indexOf() は、
内部で equals() を使って要素を比較します。
しかし、User クラスで equals() を
オーバーライドしていない場合、
Object.equals()(参照比較)が使われます。
// 同じ内容でも別インスタンスなら false
users.contains(new User("たかお", 24)); // false
users.indexOf(new User("たかお", 24)); // -1
解決方法
User クラスに equals() と
hashCode() を実装します。
public class User {
public String name;
public int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return age == user.age &&
name.equals(user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
実務でよく使われる方法
Spring Boot では Lombok を使って
equals() / hashCode() を
自動生成するのが一般的です。
@Data
public class User {
private String name;
private int age;
}
この場合、contains()・indexOf()・
remove(Object) も
String と同じ感覚で安全に使えます。
7.3 Set (重複不可の集合)
同じ値は2つ以上入りません。順序の概念がないため、「○番目に挿入(insert)」はできません。
Set<String> set = new HashSet<>();
set.add("A");
set.add("A"); // 2回目は無視される。エラーにはならない。
// --- 主要メソッド ---
set.size(); // 1 (Setもsize()を使用)
set.contains("A"); // true (Listより圧倒的に高速)
set.remove("A"); // 削除
// --- ループ ---
for (String s : set) {
System.out.println(s); // 取り出される順番は不定
}
7.4 Map (キー・バリュー型辞書)
Keyを元にValueを高速に引き出します。JSONとの相性が抜群です。
Map<String, Integer> map = new HashMap<>();
map.put("Apple", 100);
map.put("Orange", 80);
// --- 取得・確認 ---
map.get("Apple"); // 100 (キーがなければ null)
map.containsKey("Apple"); // true (キー存在確認)
map.containsValue(80); // true (値存在確認)
map.size(); // 2
map.remove("Apple"); //100(削除した値。なければ null)
map.remove("Apple", 100); // true(key と value が一致したら削除)
// --- 全要素ループ (3パターン) ---
// Pattern A: キーで回す
for (String key : map.keySet()) {
System.out.println(key + " -> " + map.get(key));
}
// Pattern B: キーと値を同時に回す (推奨: 高速)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
// Pattern C: 値だけで回す
for (Integer val : map.values()) {
System.out.println(val);
}
@Data を付けたクラスなら Set / Map でも使える?
結論:
はい、使えます。
@Data を付けたクラスであれば、
equals() と hashCode() が自動生成されるため、
Set や Map の contains / remove 系メソッドが正しく動作します。
使えるメソッド一覧
| コレクション | メソッド | 条件 |
|---|---|---|
| Set<User> | contains(), remove() | equals() + hashCode() |
| Map<User, ?> | containsKey(), remove(key) | equals() + hashCode() |
| Map<?, User> | containsValue() | equals() |
なぜ Set / Map は特に注意が必要か
HashSet や HashMap は、
まず hashCode() で場所を決め、次に equals() で比較します。
// hashCode が違うと equals は呼ばれない
set.contains(user); // hashCode → equals の順
そのため、hashCode() が正しく実装されていないと contains できません。
Lombok @Data を使う場合
@Data
public class User {
private String name;
private int age;
}
このクラスでは equals() と hashCode() が
全フィールドを使って自動生成されるため、
Set / Map でも安全に利用できます。
⚠ 注意点(重要)
-
Set / Map に入れた後でフィールドを変更すると壊れる
(hashCode が変わるため、見つからなくなる) -
@Data は全フィールドを equals/hashCode に含める
意図しない項目まで比較対象になることがある
Set<User> set = new HashSet<>();
User u = new User("たかお", 24);
set.add(u);
u.setAge(25);
set.contains(u); // false(要注意)
実務での定番ルール
- DTO や値オブジェクト →
@Dataで OK -
Entity や Key に使うクラス →
@EqualsAndHashCode(of = "id")を使う
@Data
@EqualsAndHashCode(of = "id")
public class User {
private Long id;
private String name;
private int age;
}
これにより、Set / Map / JPA すべてで事故を防げます。
7.5 型の相互変換テクニック
Array ⇔ List
// Array -> List
List<String> list = new ArrayList<>(Arrays.asList(array));
// List -> Array
String[] array = list.toArray(new String[0]);
Set ⇔ List
// Set -> List (重複を除去してからソートしたい時などに)
List<String> listFromSet = new ArrayList<>(set);
Map ⇔ List
// 全キーをリスト化
List<String> keyList = new ArrayList<>(map.keySet());
// 全値をリスト化
List<Integer> valList = new ArrayList<>(map.values());
8. インターフェース (Interface)
インターフェース内に
private String name; のような、個々のインスタンスが状態を持つための変数は定義できません。
定義できるのは public static final な定数 のみです。
インターフェースの目的は外部への公開です。そのため、メソッドは 暗黙的に
public abstract、変数は 暗黙的に public static final になります。現場では省略するのが慣習です。
public interface UserLoader {
// 1. 変数は自動的に public static final (定数) になる
int MAX_RETRY = 3;
// 2. メソッドは自動的に public abstract になる
User load(Long id);
// 3. 多重継承:implements A, B のように複数を同時に実装可能
}
9. 抽象クラス (Abstract Class)
抽象クラスは「共通部品」の宝庫です。親で共通の仕組みを作り、子で細部を仕上げます。
インターフェースと異なり、抽象クラスは
final ではない通常の変数を持つことができます。
これにより、全子クラス共通の「カウント用変数」や「現在のステータス」などを親で管理させることが可能です。
public abstract class AbstractService {
// 1. インスタンス変数(状態)を持てる
protected final String serviceId;
// 2. final でない(書き換え可能な)変数も持てる
protected int accessCount = 0;
// コンストラクタ:親が変数を持つなら、子からデータを受け取る必要がある
public AbstractService(String id) { this.serviceId = id; }
// 実装済みメソッド:子クラスからそのまま呼び出して再利用できる
public void log(String msg) {
this.accessCount++; // 変数を更新する共通処理
System.out.println("[" + serviceId + "] count:" + accessCount + " " + msg);
}
// 抽象メソッド:ここだけ子クラスで個別に実装させる
// インターフェースと違い、public 修飾子の明示が必要
public abstract void execute();
}
10. クラス (Class) の全実装パターン
「親の部品を使いつつ、外部との契約(IF)も守り、さらに自分独自の機能を追加する」最強の形です。
public class DatabaseUserService extends AbstractService implements UserLoader {
// 1. 独自のプロパティ(親にはないもの)を自由に追加可能
private int queryCount = 0;
public DatabaseUserService() {
// super() で親クラスのコンストラクタを動かし、共通変数を初期化する
super("DB_USER_SRV");
}
// 2. --- IF(UserLoader)の契約を果たす ---
@Override
public User load(Long id) {
log("データ取得中..."); // 親の部品(log)をそのまま再利用!
this.queryCount++;
return new User(id, "Database Taro");
}
@Override
public void execute() { /* 独自処理 */ }
// 3. --- 親にもIFにもない「自分だけの新機能」を自由に追加できる ---
public int getQueryCount() { return this.queryCount; }
}
11. Record:不変のデータキャリアとボイラープレートの排除
Recordは、一度作成したら中身を書き換えられない「不変性(Immutable)」に特化した型です。
Recordは、Javaが「クラスを単なるデータコンテナとして扱う」ための究極の final 形態です。
- クラスそのものが final: 勝手な継承によって、データの同一性判定(equals)が壊されるのを防ぎます。
- フィールドもすべて final: オブジェクトが「いつ・どこで」書き換えられたかを追跡する苦労がゼロになります。
- 並列処理の安全: 複数の処理が同時に読んでも「常に同じ値」であることが保証されるため、バグが激減します。
- Mapのキーとしての信頼性: 中身が変わらないので
hashCodeが安定し、確実にデータを探し出せます。
11.1 ボイラープレート(定型コード)との比較
【従来の不変クラス】(約30行)
public final class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name; this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User user)) return false;
return age == user.age && Objects.equals(name, user.name);
}
@Override public int hashCode() { return Objects.hash(name, age); }
@Override public String toString() { return "User[name="+name+",age="+age+"]"; }
}
【Record】(たったの1行)
public record User(String name, int age) { }
// これだけで、左側のコンストラクタ、Getter、
// equals、hashCode、toString がすべて
// 内部的に自動生成されます。
11.2 Record の具体的な使用方法
// 1. インスタンスの作成 (new)
var user = new User("Taro", 25);
// 2. 値の取得 (メソッド名は「フィールド名()」)
String name = user.name(); // getName() ではない点に注意
// 3. 不変性(Immutable):Setterは存在しない
// user.name = "Jiro"; // エラー!フィールドは final です。
// 4. 内容を「変更」したい時は、新しく作り直すのがルール
var updatedUser = new User("Jiro", user.age());
12. Enum:実務でのデータ連携フローと変換の自動化
Enumに ("日本語", 10) と持たせるのは、外部システム(DBやAPI)との連携のためです。
例えば、DBには数値 10 として保存されている「準備中」という状態を、プログラム内では OrderStatus.PREPARING として扱い、さらにAPIレスポンスでは「準備中」という日本語を返す、といった具合です。
このような「数値 ⇔ Enumインスタンス ⇔ 日本語」の変換をEnum自身に持たせることで、コード全体で一貫したデータの扱いが可能になります。
Java の Enum において、PREPARING("準備中", 10) という定義は、内部的には new OrderStatus("準備中", 10) というインスタンス化 を行っているのと同義です。そのため、カッコ内の引数を受け取って内部のフィールド(label や code)に保存するためのコンストラクタが必須となります。
public enum OrderStatus {
PREPARING("準備中", 10),
SHIPPED("発送済み", 20);
private final String label;
private final int code;
// コンストラクタ:外部からは絶対に new できない (暗黙的に private)
OrderStatus(String label, int code) {
this.label = label;
this.code = code;
}
public String getLabel() { return label; }
public int getCode() { return code; }
// DBの数値(10)からEnumインスタンスを特定する「変換メソッド」
public static OrderStatus of(int code) {
for (OrderStatus s : values()) {
if (s.code == code) return s;
}
throw new IllegalArgumentException("不正なコード: " + code);
}
}
実務での具体的なサンプル例
サンプル1:DBから取得した「10」を変換して使う
// DBから数値 10 が返ってきたと仮定
int dbValue = 10;
// ofメソッドで Enum インスタンスを取得
OrderStatus status = OrderStatus.of(dbValue);
System.out.println(status); // PREPARING
サンプル2:APIレスポンスで日本語を返す
// if文を羅列せずに、Enum自身から日本語名を取得する
String displayLabel = status.getLabel();
// APIの戻り値(JSON)のフィールドとして使用
return new OrderResponse(displayLabel); // 結果: { "status": "準備中" }
単なる数値
10 だけでは「これが何を意味するのか」をプログラム全体で共有できません。Enumに of や getLabel を持たせることで、どこからでも status.getLabel() と呼ぶだけで「正しい日本語」が取り出せるようになり、コードの重複とバグが劇的に減ります。
12. Enum:実務でのデータ連携フロー
特定の言語に依存しないよう、Enumには外部ファイルの「キー」だけを持たせます。
public enum OrderStatus {
PREPARING("status.order.preparing", 10),
SHIPPED("status.order.shipped", 20);
private final String messageKey;
private final int code;
OrderStatus(String key, int code) {
this.messageKey = key;
this.code = code;
}
public String getMessageKey() { return messageKey; }
public int getCode() { return code; }
}
15. 動的多言語化 (i18n) と LocaleContextHolder
Locale.JAPANESE などをソースに書くと、多言語対応は失敗します。
Spring Boot が提供する LocaleContextHolder を使い、実行時のリクエスト情報を取得します。
Spring Bootの MessageSource を使って、Enumのキーから多言語メッセージを動的に取得するサンプルコードを記述しました。
15.1 メッセージファイルの準備
messages.properties (日本語)
status.order.preparing=準備中
messages_en.properties (英語)
status.order.preparing=Preparing
15.2 LocaleContextHolder を用いた「真」の多言語対応
LocaleContextHolder.getLocale() を呼ぶだけで、HTTPリクエストの Accept-Language ヘッダーに基づいた最適な言語が自動で選択されます。
@Service
@RequiredArgsConstructor
public class OrderService {
private final MessageSource messageSource;
public String getStatusLabel(OrderStatus status) {
// ソースコードに Locale.JAPANESE などを書いてはいけない!
// LocaleContextHolder を使って、リクエストごとの言語設定を動的に取得する。
return messageSource.getMessage(
status.getMessageKey(),
null,
LocaleContextHolder.getLocale()
);
}
}
- 全自動: 日本人ユーザーがブラウザで開けば「準備中」、アメリカ人が開けば「Preparing」が自動で返ります。
- コードの修正不要: 新しく「中国語」を追加したくなっても、Javaを書き換える必要はありません。
messages_zh.propertiesを置くだけで対応完了です。
Accept-Language: en ヘッダーを付けてリクエストを送ることで、動作を確認できます。
16. 関数の定義方法(メソッド)と全戻り値
16.1 戻り値の全バリエーション(完全復元)
// 1. void:値を返さない(処理のみ)
public void saveUser(User user) { /* ロジック */ }
// 2. 普通のクラスを返す(単一オブジェクト)
public User findById(Long id) { return new User("Taro", 25); }
// 3. Record を返す(複数の値をセットで返す、現代の標準)
public record UserResult(User user, String message) {}
public UserResult process() {
return new UserResult(new User("Taro", 20), "成功");
}
16.2 Pair vs Record の選択(重要)
① Pair を使う場合 (Spring Data環境等)
※ org.springframework.data.util.Pair を使用。
public Pair<String, Integer> getCoordinate() {
return Pair.of("X軸", 100);
}
// 利用時:何が first で何が second か分かりにくい...
var p = getCoordinate();
System.out.println(p.getFirst());
② Record を使う場合(推奨)
※ 意味のある名前を付けられるため、可読性と型安全性が向上します。
public record Coordinate(String axis, int value) {}
public Coordinate getCoordinate() {
return new Coordinate("X軸", 100);
}
// 利用時:axis, value という名前でアクセスできるため明確!
var c = getCoordinate();
System.out.println(c.axis());
17. ラムダ式と高階関数(関数を引数に取る関数)
17.1 基本的な使い方(変数への代入)
Function<String, Integer> stringLength = s -> s.length();
int len = stringLength.apply("Hello"); // 5
17.2 【詳説】高階関数の定義と実行メカニズム
メソッドの引数に関数型インターフェースを指定することで、呼び出し側から自由にロジックを注入できます。
① 【定義側】 関数を引数に指定する
※ Function<String, String> が「関数を受け取るための型」です。
// 文字列(input)と加工ロジック(logic)をセットで受け取る
public void displayProcessedMessage(String input, Function<String, String> logic) {
// 注入されたロジック(関数)を内部で実行する命令。
// 実行するまで、大文字になるか記号が付くかは不明(動的)。
String result = logic.apply(input);
System.out.println("最終出力: " + result);
}
② 【呼び出し側】 ラムダ式でロジックを注入する
※ メソッド呼び出しの瞬間に「何をするか」を決定します。
// 呼び出し例1:大文字にするロジックを渡す
displayProcessedMessage("hello", s -> s.toUpperCase());
// 呼び出し例2:アスタリスクで囲む別のロジックを渡す
displayProcessedMessage("hello", s -> "*** " + s + " ***");
17.3 代表的な関数型インターフェース一覧
| 型 | 特徴 | メソッド名 |
|---|---|---|
Function<T, R> | Tを入れ、Rを出す | apply(T) |
Consumer<T> | Tを入れ、戻り値なし | accept(T) |
Supplier<T> | 何も入れず、Tを出す | get() |
Predicate<T> | Tを入れ、真偽値を出す | test(T) |
18. 引数の仕様と解決策(Builderパターン)
引数が多くなる場合は、意味を取り違えないよう Builderパターン を使うのがプロの設計です。
18.2 Builderパターンの完全実装(Lombok使用)
① 【定義側】 クラスの設計
@Builder アノテーションにより、Javaがビルダークラスを自動生成します。
@Builder @Getter
public class UserProfile {
private final String name;
private final int age;
private final String email;
private final String address;
}
② 【利用側】 メソッドチェーンによる生成(名前指定と同等)
引数の順番に関わらず、フィールド名で値を指定できます。
UserProfile user = UserProfile.builder()
.name("Taro")
.age(25)
.email("taro@example.com")
.address("Tokyo")
.build(); // 最後にbuildを呼んで完成
18.3 可変長引数 (Varargs) の取り扱い
引数の個数を固定せず、任意の数を受け取りたい場合に 型... 引数名 の形式で使用します。
① 基本的な定義と呼び出し
※ 内部的には「配列」として処理されます。
// 定義側:引数の最後に一つだけ定義可能
public void printAll(String... messages) {
// messages は String[] 型として扱える
for (String msg : messages) {
System.out.println(msg);
}
}
// 呼び出し側
printAll("A"); // 1個
printAll("A", "B", "C"); // 3個
printAll(); // 0個(空の配列として渡る)
可変長引数は必ず 「引数リストの最後」 に置かなければなりません。
(String... keys, int id) のような定義はコンパイルエラーになります。
② 実務テクニック:Lombok の @Singular との連携
Builderパターンで「List」を扱う際、一個ずつ値を追加したい場合に非常に強力です。
@Builder
public class Group {
@Singular // これを付けると、複数形フィールドに「単数形」のメソッドが追加される
private final List<String> members;
}
// 利用側:一個ずつ追加できる(内部で可変長引数的な柔軟さを提供)
Group g = Group.builder()
.member("Taro")
.member("Jiro")
.build();
13. REST API 開発に必須のアノテーション詳説
Spring Boot で RESTful な API を構築する際、リクエストの受け口となる Controller クラスで使用する主要なアノテーションを、実例とともに解説します。
13.1 クラスレベルの基本定義
@RestController
@Controller と @ResponseBody を組み合わせたものです。このクラス内の全メソッドの戻り値が、自動的に JSON (または XML) として HTTP レスポンスボディに書き込まれます。
@RestController
public class UserApiController { ... }
@RequestMapping
API の共通パスを指定します。クラスに付けることで、配下のメソッド全てのパスの起点となります。
@RestController
@RequestMapping("/api/v1/users")
public class UserApiController {
// 全てのメソッドが /api/v1/users から始まるパスになります
}
13.2 HTTP メソッドとパス変数
@GetMapping / @PostMapping / @PutMapping / @DeleteMapping
HTTP リクエストのメソッド(取得、作成、更新、削除)に対応させます。
@GetMapping("/list")
public List<User> getAll() { ... }
@PostMapping("/create")
public User create() { ... }
@PathVariable
URL パス内の変数(例: /users/{id} の {id} 部分)を取得します。
// URLが /api/v1/users/123 の場合、id に 123 が入る
@GetMapping("/{id}")
public User getOne(@PathVariable Long id) {
return userService.findById(id);
}
13.3 リクエストデータのバインド
@RequestBody
HTTP リクエストのボディ(通常は JSON)を Java オブジェクトへ自動変換します。POST や PUT で必須です。
// クライアントが送った JSON { "name": "Taro", "age": 20 } が User オブジェクトに変換される
@PostMapping
public User createUser(@RequestBody User user) {
return userService.save(user);
}
@RequestParam
クエリパラメータ(例: ?name=Taro&age=20)を取得します。
// URL: /api/v1/users/search?keyword=Java
@GetMapping("/search")
public List<User> search(@RequestParam String keyword) {
return userService.search(keyword);
}
// 必須でない場合は defaultValue を指定可能
@GetMapping("/list")
public List<User> list(@RequestParam(defaultValue = "1") int page) { ... }
- リソースを特定する ID などは @PathVariable(必須項目)。
- 検索条件やページ番号などの補助的なフィルタリングは @RequestParam(任意項目)。
- 新規登録や更新などの複雑なデータ一式は @RequestBody(JSON)。
14. DI (依存性の注入) と Profile 戦略
14.1 DI (Dependency Injection) とは?
DIは「自分で使う道具を自分で用意(new)せず、外部(Spring Container)に用意してもらう」設計手法です。 これにより、利用側(ControllerやService)は具体的な実装クラスを意識せず、インターフェースに対してプログラミングできるようになります。
クラス内で
Service s = new ServiceImpl(); と書くと、そのクラスは ServiceImpl に強く依存し、テストや環境変更が困難になります。DIを使えば、コンパイル時ではなく実行時に中身を差し替える「疎結合」が実現します。
初心者が最初につまずくこの名前。実は Java(コーヒー) の名前にちなんだユーモアと深い意味があります。
由来と意味:
「Java」はコーヒーの銘柄が由来です。コーヒーを作るには「豆(Bean)」が必要ですよね。ITの世界でも 「Javaで作られた小さな部品(種)」 のことを JavaBeans(のちにBean) と呼ぶようになりました。一粒の豆が完成された小さな単位であるように、プログラムも「独立した部品」の集まりとして作ろう、という思想です。
キッチンの設備に例えると:
プロのレストランの厨房に最初から備え付けられている「大型冷蔵庫」や「業務用コンロ」がBeanです。
- 管理者はあなたじゃない: 設備(Bean)を設置・管理するのは「厨房のオーナー(Spring)」です。
- 申請するだけ: あなた(クラス)は「コンロを使いたい」と申請するだけで、オーナーが最適なコンロを貸してくれます。
- みんなでシェア: 基本的に一台の冷蔵庫をみんなで使い回します(これをシングルトンと呼びます)。
一台の設備を全員で共有してもデータが混ざらない秘訣は、「ステートレス(状態を持たない)」設計にあります。 クラス内に「誰かのデータ」を保持する変数(インスタンス変数)を書かず、データはすべて「メソッドの引数」として受け渡すことで、並列処理でも安全に使い回せるのです。
結論:
@Service と書くことは、「このクラスを厨房(Spring)の『公式設備(豆=Bean)』として登録して!みんなが必要な時にオーナーから貸し出せるようにして!」とお願いすることなのです。
14.3 実務での呼び出し例(コンストラクタ注入の正体)
① 我々が実際に書くコード
@RestController
@RequiredArgsConstructor
public class OrderController {
private final NotificationService service;
}
② 裏側で自動生成されているコード
@RestController
public class OrderController {
private final NotificationService service;
public OrderController(NotificationService service) {
this.service = service;
}
}
14.7 ステートレス設計:誰が「状態」を保つのか?
サーバーがステートレス(記憶喪失状態)であるなら、ユーザーの「名前」や「現在の注文状況」といったデータはどこにあるのでしょうか? 実務では、以下の2箇所に状態を預けます。
ユーザープロフィールや履歴など。「誰が使っても同じ結果であるべき永続的なデータ」はDBに保管します。ServiceはDBから取ってくるだけで、自分では覚えません。
「今ログインしているのは誰か?」という情報は、クライアントが持つ JWTトークン 等に含め、リクエストのたびにサーバーへ送り届けます。
【実務コード例:状態を一切持たない Service】
@Service
@RequiredArgsConstructor
public class UserProfileService {
private final UserRepository userRepository;
// このメソッドはシングルトン(1台)だが、引数が違うので100万人同時にさばける
public User updateBio(Long userId, String newBio) {
// 1. 「以前の状態」を外部(DB)から呼び出す
User user = userRepository.findById(userId);
// 2. 「渡されたデータ」で加工する(Service自身は何も記憶しない)
user.setBio(newBio);
// 3. 「新しい状態」を外部(DB)へ預け直す
return userRepository.save(user);
}
}
【利用側:状態をバケツリレーで渡す】
@RestController
public class UserController {
private final UserProfileService service;
@PatchMapping("/me/bio")
public User updateBio(
// クライアントから送られた「自分は誰か」というID(状態)を受け取る
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody String bio
) {
// バケツリレーのように、引数としてデータを流し込む
return service.updateBio(principal.getId(), bio);
}
}
サーバー(Bean)は 「加工工場」 です。 ユーザー(材料)がベルトコンベア(引数)に乗って流れてきたら、加工して、出荷(戻り値)するだけ。 工場の中に材料を置きっぱなしにしないことが、大規模アクセスに耐える秘訣なのです。
14.2 Profile戦略:環境による実装の出し分け
① インターフェースの定義
public interface NotificationService { void send(String message); }
② 開発用(Mock実装)
@Service @Profile("dev")
public class MockNotificationService implements NotificationService { ... }
③ 本番用(Real実装)
@Service @Profile("prod")
public class RealNotificationService implements NotificationService { ... }
14.3 実務での呼び出し例(コンストラクタ注入)
@RestController @RequiredArgsConstructor
public class OrderController {
private final NotificationService notificationService;
@PostMapping("/order")
public void placeOrder() { notificationService.send("完了"); }
}
14.4 推奨されるディレクトリ構成
├── service (NotificationService.java)
│ ├── impl (RealNotificationService.java)
│ └── mock (MockNotificationService.java)
src/main/resources/application.properties に spring.profiles.active=dev と記述するか、実行時にコマンドライン引数で指定することで、Beanの有効・無効を瞬時に切り替えられます。
14.5 DIで使用する主要アノテーション一覧
Spring Boot のDIコンテナを制御するための主要なアノテーションを整理します。
| アノテーション | 役割・意味 |
|---|---|
| @Component | 汎用的なBeanとして登録する(全ての基本)。 |
| @Service | ビジネスロジックを担当するクラスに付与する(中身は@Component)。 |
| @Repository | DBアクセスを担当するクラスに付与。データ例外の変換機能も持つ。 |
| @Configuration | Bean定義を行うための設定クラスであることを示す。 |
| @Bean | メソッドに付与し、その戻り値をBeanとしてDIコンテナに登録する。 |
| @Autowired | 型が一致するBeanを自動で注入する。 |
| @Qualifier | 同じ型のBeanが複数ある場合、名前を指定して一つに絞り込む。 |
| @Primary | 同じ型のBeanが複数ある場合、優先的に注入されるBeanを指定する。 |
| @RequiredArgsConstructor | Lombokのアノテーション。finalフィールドを引数に取るコンストラクタを自動生成(DI推奨手法)。 |
14.6 DIアノテーションの具体的な使用例
14.6 DIアノテーションの役割と「裏側」の仕組み
1. @Service と @Repository:専門職とトランザクション
① トランザクション管理の自動化
`@Transactional` を書くと、Springがあなたのクラスを 「偽装(Proxy)」 して包み込みます。メソッド実行前に BEGIN を、正常終了後に COMMIT を、エラー時に ROLLBACK を、SQLで勝手に発行してくれます。
② 例外翻訳(Exception Translation)
`@Repository` を書くと、DB固有の難解なエラー(例:MySQLのエラー1064等)が、Spring共通の分かりやすい例外(例:`BadSqlGrammarException`)に自動で変換されます。これにより、DBの種類を変えても呼び出し側のJavaコードへの影響を最小限に抑えられます。
【定義例:リポジトリ(データの出し入れ係)】
// 1. インターフェース
public interface UserRepository {
void updateName(Long id, String name);
}
// 2. 実装クラス
@Repository
public class UserRepositoryImpl implements UserRepository {
@Override
public void updateName(Long id, String name) {
try {
// 低レベルなDB操作
db.execute("UPDATE users SET name = ?", name, id);
} catch (Exception e) {
// リポジトリでは「何が起きたか」をログし、上位へ投げる
// 投げられた例外は、@Repositoryの境界でSpringが綺麗に翻訳してくれる
log.error("DB更新エラー", e);
throw e;
}
}
}
【実践例:サービス(業務判断の頭脳)】
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final LogRepository logRepository;
@Transactional // 複数テーブル更新時の「All or Nothing」を保証
public void updateProfile(Long id, String newName) {
try {
// 処理A(Userテーブル更新)
userRepository.updateName(id, newName);
// 処理B(操作ログテーブル保存)
logRepository.save("Update name to " + newName);
// もし処理Bで失敗したら、処理Aの更新も自動で巻き戻される(ロールバック)
if (newName.equals("error")) throw new RuntimeException();
} catch (Exception e) {
// サービス層での try-catch は「業務上のリトライ」や
// 「エラー時の代替案の提示」のために行う。
throw e; // ロールバックさせるために再度外へ投げる
}
}
}
2. @Configuration と @Bean:外部ライブラリの自動登録プロセス
あなたが
@Bean を付けたメソッド(例:passwordEncoder()) を呼び出すコードを自分で書く必要はありません。Springが起動するときに、それらのメソッドを自動で見つけて実行してくれるからです。Spring Boot 起動時の内部フロー:
1. **スキャン**: Springが
@Configuration の付いたクラスを見つける。2. **自動実行**: その中の
@Bean が付いたメソッドを、Spring自身が「部品の作り方」として自動的に呼び出す。3. **倉庫へ保管**: メソッドから戻ってきた「実体(インスタンス)」を、Springが自分の倉庫(DIコンテナ)にポイッと入れる。
4. **配達(DI)**: 他のクラス(例:
AuthService)がその型を必要としていたら、倉庫から届ける。つまり、
@Bean メソッドは「あなたが呼ぶための関数」ではなく、「SpringがBean(実体)を作るために使う『部品製造マニュアル』」なのです。
【実装例 A:部品の「製造マニュアル」を登録する】
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class AppConfig {
// このメソッドは Spring によってアプリ起動時に一度だけ自動実行される
@Bean
public BCryptPasswordEncoder passwordEncoder() {
// new クラス名() で本物の「実体」が誕生する
return new BCryptPasswordEncoder();
}
}
【実装例 B:Springが自動で届けて(注入して)くれる】
@Service
@RequiredArgsConstructor
public class AuthService {
// 上で Spring が自動実行して作ったインスタンスがここに自動で届く
private final BCryptPasswordEncoder encoder;
public void register(String rawPassword) {
// 届いた(注入された)実体を使って処理を行う
String encoded = encoder.encode(rawPassword);
System.out.println("暗号化済み: " + encoded);
}
}
@Primary と @Qualifier
同じインターフェースを持つBeanが複数ある場合の制御方法です。
// 実装1
@Service @Primary
public class DefaultPayment implements Payment { ... }
// 実装2
@Service("creditPayment")
public class CreditPayment implements Payment { ... }
// --- 利用側 ---
@Service
@RequiredArgsConstructor
public class CheckoutService {
// 何も指定しないと @Primary の DefaultPayment が注入される
private final Payment defaultPayment;
// @Qualifier を使えば、名前で特定の実装を指定できる
@Qualifier("creditPayment")
private final Payment creditPayment;
}
@Autowired (フィールド・メソッド注入)
コンストラクタ以外でBeanを注入したい場合に使いますが、現在はテスト容易性の観点からコンストラクタ注入が主流です。
public class LegacyService {
@Autowired
private HelperComponent helper; // フィールドへの直接注入
}
15. 動的多言語化 (i18n) と LocaleContextHolder
@Service @RequiredArgsConstructor
public class OrderService {
private final MessageSource messageSource;
public String getStatusLabel(OrderStatus s) {
return messageSource.getMessage(s.getMessageKey(), null, LocaleContextHolder.getLocale());
}
}
16. JDBC操作の完全理解 (JDBC Deep Dive)
設定ファイルからパラメータの受け渡しまで、データが流れる全プロセスを解説します。
1. コネクション設定の場所と「自動設定」の魔法
1. Springが
application.properties を読み、裏側で接続機(Bean)を準備します。2. 開発者は自分の Repository クラスの フィールドに
private final NamedParameterJdbcTemplate db; と書く だけです。3. 起動時に Spring が「このクラス、操作機が必要なんだな」と判断し、コンストラクタを通して実体を自動で流し込み(注入し)ます。
つまり、自分で接続コードを一文字も書かずに、変数
db が使える状態になることを指します。
jdbc:sqlite:mydata.db のようにファイル名だけを書いた場合、そのファイルは 「プロジェクトのルートディレクトリ(pom.xml や build.gradle がある場所)」 に作成・配置されることが前提となります。パスを指定する方法:
特定の場所に保存したい場合は、以下のように絶対パスや相対パスを記述します。
- 絶対パス (Windows):
jdbc:sqlite:C:/db/mydata.db - 絶対パス (Mac/Linux):
jdbc:sqlite:/Users/name/db/mydata.db - ホームディレクトリ相対:
jdbc:sqlite:~/mydata.db
spring.datasource.driver-class-name=org.sqlite.JDBC
2. 複数パラメータ(:id, :addr等)の安全なバインド
【○】正しい方法:自分も同じ定義を書く
// UserRepository 以外で DB を使いたい場合
@Service
@RequiredArgsConstructor
public class OrderService {
// ★他のクラスでも、自分が必要ならこの一行を必ず書く!
private final NamedParameterJdbcTemplate db;
public void order() {
// Spring がこのクラス専用の「db」という窓口に、本物の道具を届けてくれる
db.update(...);
}
}
【方法 A】Map.of(手軽・少人数用)
String sql = "SELECT * FROM users WHERE id = :id AND address = :addr";
Map<String, Object> params = Map.of("id", 1, "addr", "Tokyo");
return db.queryForObject(sql, params, rowMapper);
【方法 B】MapSqlParameterSource(推奨・大人数用)
String sql = "UPDATE users SET address = :addr, age = :age WHERE id = :id";
var params = new MapSqlParameterSource()
.addValue("id", 1).addValue("addr", "Tokyo").addValue("age", 25);
db.update(sql, params);
結論:いいえ、絶対に見えません。
Java の
private というのは、「そのクラスファイルの中だけで有効な変数名」という意味です。そのため、以下のルールが適用されます。- 「db」はただのあだ名:
UserRepositoryImplが、Springから借りた道具に、自分のクラス内だけで通用する「db」という名前(あだ名)を付けているだけです。 - 他のクラスも書く必要がある: もし
OrderServiceなどの別のクラスでも DB 操作を行いたいなら、そのクラスの中でも 全く同じ定義をもう一度書く必要があります。
道具(Bean)は一台ですが、それを使うための「受付窓口(private変数)」は各クラスが自分で用意しなければならないのです。
3. BeanPropertyRowMapper:自動詰め替えの「翻訳家」
【定義:受け皿となる Java クラス (POJO)】
翻訳家がデータを詰め替えるためには、以下の形式のクラスが必要です。
public class User {
private Long id;
private String name;
private String address;
// 必須:中身を詰め替えるための空のコンストラクタ
public User() {}
// 必須:Springが値をセットするための setter 群
public void setId(Long id) { this.id = id; }
public void setName(String name) { this.name = name; }
public void setAddress(String address) { this.address = address; }
// データを取得するための getter 群
public Long getId() { return id; }
public String getName() { return name; }
}
【実装:データの取得と繰り返し処理】
// 0. 翻訳家の生成
RowMapper<User> rowMapper = new BeanPropertyRowMapper<>(User.class);
// 1. 1レコード形式で取得 (queryForObject)
User singleUser = db.queryForObject(
"SELECT * FROM users WHERE id = :id",
Map.of("id", 1),
rowMapper
);
// 2. リスト形式で全件取得 (query)
List<User> userList = db.query("SELECT * FROM users", rowMapper);
// 3. for 文による各データの繰り返し処理
for (User user : userList) {
// 詰め替えられたデータを一つずつ取り出して表示
System.out.println("ID: " + user.getId() + ", Name: " + user.getName());
}
質問回答:エイリアス(AS名称)はスネークケースである必要はありません。
BeanPropertyRowMapper は、比較の際に「アンダースコアを削除」し、かつ「大文字小文字を無視」します。したがって、以下の SQL エイリアスはすべて Java 側の
userId という変数に正しくマッピングされます。SELECT id AS user_id(標準的)SELECT id AS USERID(大文字)SELECT id AS UserId(キャメル)SELECT id AS "user id"(空白あり)
Controller / Service / Repository の関係(初心者向けまとめ)
1) まず用語の定義(専門用語をかみ砕く)
Controller(コントローラ)
HTTPリクエスト(URLアクセス)を受け取って、レスポンス(返すデータ)を返す入口です。
例:/users にアクセスされたら、対応する処理を呼び出して結果を返します。
Service(サービス)
アプリの「業務ルール(やりたい処理の手順)」を書く場所です。
Controllerから呼ばれ、「何をするか」をまとめたメソッドを提供します。
例:「20歳以上のユーザーを返す」「登録時に入力チェックする」など。
Repository(リポジトリ)
DB(データベース)から読み書きするための窓口です。
SQL(SELECT/INSERT/UPDATE/DELETE)を直接書かずに、JavaメソッドでDB操作をできるようにします。
DB(データベース)
ユーザー情報などを保存する場所です(例:SQLite / MySQL / PostgreSQL など)。
DI(Dependency Injection:依存性注入)
Spring Boot が必要な部品(RepositoryやService)を自動で作成して渡してくれる仕組みです。
そのため、Repositoryは自分で new しません。
2) 役割分担の全体像(重要)
Controller → Service → Repository → DB
入口 業務処理 DB操作 保存場所
- Controller:HTTPの入口。パラメータを受け取り、Serviceを呼び、結果を返す
- Service:アプリとして意味のある処理(業務ルール)をまとめる
- Repository:DBアクセスを担当する(検索や保存など)
3) 質問への答え:Serviceの中でRepositoryを使っていいの?
はい。Serviceの中でRepositoryを使うのが正しい設計です。
逆に、Controllerが直接Repositoryを呼び出すのは、初心者がハマりやすい(後で困る)書き方です。
4) なぜ Service を挟むのか(何が嬉しいのか)
- 変更に強い:画面(Controller)が変わっても、業務処理(Service)は流用できる
- 再利用できる:別の画面や別のAPIでも同じServiceを使える
- テストしやすい:ServiceはHTTPなしでも動作確認しやすい
5) サンプル:Entity / Repository / Service / Controller のつながり
Entity(エンティティ):DBの1行を表すクラス
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
private int age;
// getter / setter 省略
}
Repository:DB操作の定義を書く(@Queryもここ)
public interface UserRepository extends JpaRepository<User, Long> {
// @Query は Repository に書く(Serviceには書かない)
@Query("SELECT u FROM User u WHERE u.age >= :age")
List<User> findOlderThan(@Param("age") int age);
}
Service:アプリとして使えるメソッドを作り、Repositoryを呼ぶ
@Service
public class UserService {
private final UserRepository userRepository;
// DI:Spring Boot が userRepository を自動で渡す
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// Controller から呼ばれる「実際に使える処理」
public List<User> getUsersOlderThan(int age) {
return userRepository.findOlderThan(age);
}
}
Controller:HTTPを受け取り、Serviceを呼び、結果を返す
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/older")
public List<User> older(@RequestParam int age) {
return userService.getUsersOlderThan(age);
}
}
6) よくある間違い(初心者が混乱しやすい点)
Q: userRepository は User クラスのインスタンス?
いいえ。 userRepository は UserRepository(DB操作の窓口)のインスタンスです。
User(Entity)は「データ」、UserRepository は「DB操作の道具」です。
Q: Repository はどうやって作るの? new するの?
new しません。 Spring Boot が起動時に Repository の実装を自動生成して管理し、 ServiceやControllerに渡してくれます(DI)。
まとめ
- Controller:入口(HTTP)
- Service:業務処理(アプリのやりたいこと)
- Repository:DB操作(SQLを隠す)
- @Query:複雑な検索が必要なときにRepositoryに書く
21. Java ファイル操作 (NIO.2) 完全ガイド
Java 11 以降の実務標準である java.nio.file.Files を活用し、Path結合、Open、書き込み/読み込み、例外処理、自動Closeまでを関数として完結させた実例です。
Files.write や Files.readAllBytes といった静的メソッドは、「内部で自動的にファイルを開き、処理し、即座に閉じる」 ように設計されています。開発者がファイルストリーム(蛇口)を直接生成しないため、手動で
close() を呼ぶ必要がありません。これだけで安全にリソースが解放されます。
1. バイナリデータの操作 (画像・PDF等)
readAllBytes と write を使い、バイト配列として一気に処理します。
import java.nio.file.*;
import java.io.IOException;
public class BinaryFileHandler {
// バイナリ書き込み関数の例 (Pathの結合を含む)
public void saveBinary(String dir, String fileName, byte[] data) {
// 1. Path.of でディレクトリとファイル名を安全に結合
Path path = Path.of(dir, fileName);
try {
// 2. 書き込み実行 (Open -> Write -> Close が内部で完結)
Files.write(path, data);
System.out.println("バイナリ保存完了: " + path.toAbsolutePath());
} catch (IOException e) {
// 3. エラーハンドリング
System.err.println("保存失敗: " + e.getMessage());
}
}
// バイナリ読み込み関数の例
public byte[] loadBinary(String dir, String fileName) {
Path path = Path.of(dir).resolve(fileName); // resolveを使った結合例
try {
// ファイルを一気にバイト配列として取得
return Files.readAllBytes(path);
} catch (IOException e) {
System.err.println("読み込み失敗: " + e.getMessage());
return null;
}
}
}
2. 「readLine」と「writeLine」はどこにある?
Java では「行」を扱う機能はクラスやメソッド名が少し異なります。
- readLine():
BufferedReaderクラスのメソッドとして存在します。 - writeLine(): Java にこの名前のメソッドはありません。
newLine()メソッドを使って改行を明示的に挿入します。
【行単位の操作の実装例】
// --- 読み込み (readLine) ---
try (BufferedReader reader = Files.newBufferedReader(path)) {
String line;
while ((line = reader.readLine()) != null) { // 1行ずつ読み込む
System.out.println(line);
}
}
// --- 書き出し (newLine) ---
try (BufferedWriter writer = Files.newBufferedWriter(path)) {
writer.write("Hello World");
writer.newLine(); // これが writeLine の役割(OSに応じた改行を挿入)
}
2. テキストデータの一括操作 (小規模ファイル)
全行を 1 つの文字列やリストとして読み書きします。
import java.nio.file.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class TextFileHandler {
// テキスト全行読み込み関数の例
public void readAllText(String dir, String file) {
Path path = Path.of(dir, file);
try {
// 全行を List<String> として取得 (UTF-8)
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
// 拡張 for 文での処理
for (String line : lines) {
System.out.println("内容: " + line);
}
} catch (IOException e) {
System.err.println("テキスト読み込み失敗: " + e.getMessage());
}
}
// テキスト一括書き込み関数の例
public void writeAllText(String dir, String file, String content) {
Path path = Path.of(dir, file);
try {
// 文字列を一気に書き込む
Files.writeString(path, content);
} catch (IOException e) {
System.err.println("テキスト書き込み失敗: " + e.getMessage());
}
}
}
3. バッファを利用した大規模操作 (1行ずつ処理)
数GBある巨大なログファイル等、メモリを節約しながら 1 行ずつ Open ➔ Read ➔ Close する関数です。
import java.nio.file.*;
import java.io.*;
public class LargeFileHandler {
// 1行ずつ読み込む関数の例 (try-with-resources を使用)
public void processFileLineByLine(String dir, String file) {
Path path = Path.of(dir, file);
// try () の中でリソースを Open。ブロックを抜ける際自動で Close される。
try (BufferedReader br = Files.newBufferedReader(path)) {
String line;
// readLine() は 1 行ずつ読み込み、末尾に達すると null を返す
while ((line = br.readLine()) != null) {
// ここで一行ずつの処理(メモリに優しい)
System.out.println("読み込み行: " + line);
}
} catch (IOException e) {
// Open失敗や読み込みエラーをキャッチ
System.err.println("バッファ読み込みエラー: " + e.getMessage());
}
}
// 少しずつ書き込む関数の例
public void logLineByLine(String dir, String file, String msg) {
Path path = Path.of(dir, file);
try (BufferedWriter bw = Files.newBufferedWriter(path, StandardOpenOption.APPEND)) {
bw.write(msg);
bw.newLine(); // 改行コードを挿入
} catch (IOException e) {
System.err.println("追記書き込み失敗: " + e.getMessage());
}
}
}
4. 実務の必須知識:Path 結合と例外設計のラップ
Path.of("a", "b", "c") は OS に応じて a/b/c (Linux) や a\b\c (Windows) を自動生成します。
実務では、いちいち throws IOException を書かなくて済むように UncheckedIOException で包む(ラップする)手法が多用されます。
catch (IOException e) {
// 実行時例外に包み直すことで、呼び出し側での try-catch 強制を回避する
throw new UncheckedIOException("致命的なファイルエラー", e);
}
5. テンポラリーファイルの生成・読み書き・削除
OS の一時フォルダにファイルを生成し、処理終了後に確実に削除する実務パターンです。
import java.nio.file.*;
import java.io.*;
import java.util.UUID;
public class TempFileHandler {
// テンポラリーファイルの一連の操作メソッド
public void handleTemporaryProcess() {
Path tempFilePath = null;
try {
// 1. 生成: 接頭辞と接尾辞(拡張子)を指定して一時ファイルを生成
// (デフォルトの一時ディレクトリに作成される)
tempFilePath = Files.createTempFile("work_", ".tmp");
System.out.println("作成場所: " + tempFilePath);
// 2. 書き込み: テキストを保存
String secretData = "一時的な重要データ: " + UUID.randomUUID();
Files.writeString(tempFilePath, secretData);
// 3. 読み込み: 保存したデータを確認
String readData = Files.readString(tempFilePath);
System.out.println("読み込み結果: " + readData);
} catch (IOException e) {
throw new UncheckedIOException("一時ファイル操作エラー", e);
} finally {
// 4. 削除: 正常・異常問わず最後に必ず削除する
if (tempFilePath != null) {
try {
// ファイルを物理的に削除
Files.deleteIfExists(tempFilePath);
System.out.println("一時ファイルを安全に削除しました。");
} catch (IOException e) {
System.err.println("削除に失敗しました: " + e.getMessage());
}
}
}
}
// 補足: deleteOnExit() を使う方法
public void createAutoDeleteTempFile() throws IOException {
File temp = File.createTempFile("auto_", ".tmp");
// JVMが終了したときに自動で削除されるように予約する
temp.deleteOnExit();
}
}
deleteOnExit() はJVMが異常終了した際にファイルが残ってしまうリスクがあるため、可能な限り finally ブロックでの Files.deleteIfExists() による即時削除を推奨します。
ウェブから送られてくるフォームデータ(key=value)やアップロードファイルを、サーバー側のファイルシステムに安全に保存する仕組みを解説します。
2. フォームデータ (www-form-urlencoded) の処理
2. 実践:フォームデータのバリデーションとDB更新
HTMLフォームから送られた「ユーザーID」と「新しい住所」を受け取り、Java側で入力チェック(バリデーション)を行った後、データベースを更新します。
【① ウェブ画面 (HTML):送信フォーム】
<!-- application/x-www-form-urlencoded 形式で送信 -->
<form action="/api/users/update-address" method="post">
<!-- name属性が Java 引数の @RequestParam("...") に対応する -->
<input type="hidden" name="userId" value="123">
<input type="text" name="address" placeholder="新住所を入力">
<button type="submit">住所を更新する</button>
</form>
【② 受付窓口 (Controller):バリデーションの実行】
import org.springframework.web.bind.annotation.*;
import lombok.RequiredArgsConstructor;
@RestController
@RequiredArgsConstructor
public class UserProfileController {
private final UserService userService;
@PostMapping("/api/users/update-address")
public String updateAddress(
@RequestParam("userId") Long id,
@RequestParam("address") String addr
) {
// 1. 実務に必須のバリデーション処理
if (addr == null || addr.isBlank()) {
return "住所が空です";
}
if (addr.length() > 100) {
return "住所が長すぎます(100文字以内)";
}
// 2. ビジネスロジック(Service)を呼び出す
userService.updateUserAddress(id, addr);
return "住所を更新しました";
}
}
【③ 永続化 (Service/Repository):テーブル更新】
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
@Service
@RequiredArgsConstructor
public class UserService {
// 16章で詳説した DB 操作用 Bean (DI)
private final NamedParameterJdbcTemplate db;
@Transactional
public void updateUserAddress(Long id, String newAddr) {
String sql = "UPDATE users SET address = :addr WHERE id = :id";
// パラメータのバインド (SQLインジェクション対策)
var params = new MapSqlParameterSource()
.addValue("id", id)
.addValue("addr", newAddr);
// DBのテーブルを物理的に更新
db.update(sql, params);
}
}
6. ファイルアップロード (MultipartFile)
ブラウザから送信された「画像ファイルやドキュメント」をサーバー上の特定のディレクトリ(uploads)へ保存します。
【① HTML:アップロード用設定】
<!-- enctype="multipart/form-data" が必須 -->
<form action="/api/files/upload" method="post" enctype="multipart/form-data">
<input type="file" name="targetFile">
<button type="submit">アップロード</button>
</form>
【② Controller & Service 一貫サンプル】
import org.springframework.web.multipart.MultipartFile;
import java.nio.file.*;
import java.io.*;
@RestController
public class UploadController {
// 1. MultipartFile でファイルの実体を受け取る
@PostMapping("/api/files/upload")
public String upload(@RequestParam("targetFile") MultipartFile file) {
// 2. ファイルが空でないかチェック
if (file.isEmpty()) return "ファイルがありません";
// 3. 保存先の決定 (Path結合)
Path savePath = Path.of("uploads").resolve(file.getOriginalFilename());
try {
// 4. 保存先ディレクトリがなければ作成
Files.createDirectories(savePath.getParent());
// 5. ファイルを保存先に書き出す (自動で Close される)
file.transferTo(savePath);
return "アップロード完了: " + savePath;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
22. 外部サーバーとのHTTP通信 (WebClient)
22. 外部サーバーとのHTTP通信 (WebClient)
非同期通信を駆使し、外部APIからデータを取得・送信する方法を解説します。エラー発生時に特定のステータスコードを返す実務的な実装も含みます。
1. ステータスコード指定を含む WebClient Service
外部サーバーからのエラー応答(404, 500等)を検知するには
onStatus を使います。また、自サーバーの応答として特定のステータスを返したい場合は、Controllerで ResponseEntity を Mono で包んで返却します。
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.web.server.ResponseStatusException;
import reactor.core.publisher.Mono;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class ExternalApiService {
private final WebClient.Builder webClientBuilder;
// GET: エラーを能動的に例外へ変換する
public Mono<String> fetchWithDetailedError(String url) {
return webClientBuilder.build()
.get()
.uri(url)
.retrieve()
// 4xx, 5xx のステータスを検知して特定の例外を投げる
.onStatus(HttpStatusCode::isError, response -> {
HttpStatus status = (HttpStatus) response.statusCode();
return Mono.error(new ResponseStatusException(status, "外部サーバーがエラーを返しました"));
})
.bodyToMono(String.class);
}
// POST: JSONを送信し、登録結果(JSON)を期待する
public Mono<User> postToExternal(String url, User body) {
return webClientBuilder.build()
.post()
.uri(url)
// bodyValue: Userオブジェクトを自動的に JSON 変換
.bodyValue(body)
.retrieve()
// 期待: 外部サーバーが登録後の User 情報を JSON で返してくること
.bodyToMono(User.class);
}
}
2. 受付窓口 Controller (例外ごとの個別ハンドリング)
ResponseEntity を使い、エラー理由(原因)をボディに含め、適切なステータスを返す実戦的な実装です。
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
@Slf4j
@RestController
@RequiredArgsConstructor
public class ExternalProxyController {
private final ExternalApiService apiService;
// --- GET の堅牢なハンドリング ---
@GetMapping("/api/proxy/data")
public Mono<ResponseEntity<Object>> getProxyData() {
return apiService.fetchWithDetailedError("https://api.example.com/data")
.map(data -> ResponseEntity.ok((Object) data))
// 例外種類に応じたきめ細やかな対応
.onErrorResume(e -> {
if (e instanceof ResponseStatusException rse) {
// Service層で投げた意図的なエラー
log.error("外部サーバー起因のエラー: {}", rse.getReason());
return Mono.just(ResponseEntity.status(rse.getStatusCode())
.body(Map.of("error", rse.getReason())));
} else if (e instanceof WebClientResponseException wce) {
// 通信そのものの失敗(タイムアウト等)
log.error("ネットワークエラー: {}", wce.getMessage());
return Mono.just(ResponseEntity.status(HttpStatus.BAD_GATEWAY).build());
}
// 未知のエラー
log.error("予期せぬエラーが発生しました", e);
return Mono.just(ResponseEntity.internalServerError().build());
});
}
// --- POST の堅牢なハンドリング ---
@PostMapping("/api/proxy/users")
public Mono<ResponseEntity<Object>> postProxyUser(@RequestBody User user) {
return apiService.postToExternal("https://api.example.com/users", user)
.map(res -> ResponseEntity.status(HttpStatus.CREATED).body((Object) res))
.onErrorResume(e -> {
log.error("POST通信失敗: {}", e.getMessage());
// クライアントに原因(Map)を添えて 502 を返す
return Mono.just(ResponseEntity.status(HttpStatus.BAD_GATEWAY)
.body(Map.of("message", "外部サーバーへの登録に失敗しました", "detail", e.getMessage())));
});
}
}
23. VS Code での Spring Boot 開発完全ガイド
VS Code を最強の Java 開発環境にするための設定と、プロジェクトの作成、ビルド、デバッグのフローを解説します。
1. 必須拡張機能 (Extensions)
- Extension Pack for Java: Language Support, Debugger, Test Runner, Maven 等を含む Java 開発の核。
- Spring Boot Extension Pack: Spring Boot Tools, Initializr, Dashboard 等がセットになった公式パック。
- Lombok Annotations Support: 本マニュアルで使用している
@RequiredArgsConstructor等を VS Code に認識させるために必須。
2. プロジェクトの新規作成方法
-
コマンドパレットを起動:
Ctrl + Shift + Pを押します。 -
Initializr を選択:
Spring Initializr: Create a Maven Project...を入力・選択します。 -
基本情報の入力: バージョン(3.x.x)、言語(Java)、Group Id (
com.example)、Artifact Id (demo) 等を順に決定します。 -
依存関係の選択:
Spring Web,Lombok,Spring Data JDBC,SQLite Driverを検索して追加し、Enter を押します。
3. ビルドと実行
【方法A】Spring Boot Dashboard (推奨)
左サイドバーの「Spring Boot Dashboard」アイコンから、プロジェクト名の右にある「実行」ボタンをクリックします。これが最も IDE らしい操作です。
【方法B】ターミナル実行
# プロジェクト直下の Maven Wrapper を使用
./mvnw spring-boot:run
4. デバッグ実行とブレークポイント
ブレークポイント: 行番号の左側をクリックして赤い点を付けます。プログラムはその行の実行直前で一時停止します。
デバッグの開始パターン
- メインクラスから:
Application.javaを開き、mainメソッドの上部に表示されるDebugをクリック。 - ショートカット:
F5キーでデバッグセッションを開始。 - ダッシュボードから: Spring Boot Dashboard のデバッグアイコン(虫のマーク)をクリック。
※ 停止中は「変数」パネルで現在の値を確認したり、「ステップ実行」で1行ずつ処理を進めることができます。
24. JAR から WAR への変更手順
Spring Boot の標準は実行可能 JAR (Embedded Tomcat) ですが、外部の Web サーバー(Tomcat 等)へデプロイする場合は WAR (Web Application Archive) 形式にする必要があります。
1. pom.xml の修正
war に変更し、組み込み Tomcat をビルドから除外(provided)します。
【変更箇所:packaging】
<!-- 変更前: <packaging>jar</packaging> -->
<packaging>war</packaging>
【変更箇所:dependencies】
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<!-- provided を指定することで、外部サーバーの Tomcat を利用するようになります -->
<scope>provided</scope>
</dependency>
2. Application クラスの修正
SpringBootServletInitializer の子クラスにします。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
@SpringBootApplication
// 1. SpringBootServletInitializer を継承する
public class DemoApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
// 2. configure メソッドをオーバーライドする
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(DemoApplication.class);
}
}
3. ビルドの実行
ターミナルで以下のコマンドを実行します。
# クリーンアップとビルドの実行
./mvnw clean package
ビルド完了後、
target/ ディレクトリ配下に xxx.war ファイルが生成されていれば成功です。このファイルを外部の Tomcat の webapps/ フォルダに配置して利用します。
25. 設定ファイル (resources) の完全管理
Spring Boot の挙動を制御する各種ファイルは、すべて src/main/resources フォルダに配置します。
1. メインコンフィグ:application.properties
src/main/resources/application.properties役割:DB接続情報、ポート番号、および「アプリ独自のカスタム設定」を記述します。
# --- サーバー設定 ---
server.port=8080
# --- データベース接続設定 (Connection Info) ---
# ここで指定した接続先に対して、後述の schema.sql 等が実行されます
spring.datasource.url=jdbc:sqlite:mydata.db
spring.datasource.driver-class-name=org.sqlite.JDBC
# 初期化スクリプト(schema.sql等)を常に実行する設定 (Spring Boot 3.x以降で推奨)
spring.sql.init.mode=always
# --- 【重要】アプリ独自のカスタム設定 ---
# グラフの種類や背景色など、プログラム内で使いたい定数を定義できます
app.ui.default-chart-type=bar
app.ui.background-color=lightgray
app.system.max-upload-size=10MB
@Value("${app.ui.background-color}") String bgColor; と書くことで、プロパティファイルの値を変数に注入できます。
5. 【重要】ユーザー設定はどこに保存するのか?
src/main/resources に置いたファイルは、ビルド時に JAR ファイルの中に閉じ込められます。アプリ実行中にプログラムからこの中身を書き換えることは不可能です。そのため、ユーザー個別の設定(背景色の好みなど)は、以下のいずれかに保存します。
user_settings テーブルを作成し、ユーザーIDをキーにして保存します。これが最も一般的で安全な方法です。
JAR ファイルの外(OS のファイルシステム上)に .json や .properties ファイルを作成して保存します。一般的には「ユーザーのホームディレクトリ」などが選ばれます。
【実装例:ユーザー設定を外部ファイルへ保存する】
import java.nio.file.*;
import java.io.IOException;
@Service
public class UserConfigService {
// 保存場所: C:/Users/ユーザー名/.myapp/config.json など
private final Path configPath = Path.of(System.getProperty("user.home"), ".myapp", "config.json");
public void saveUserPreference(String jsonContent) throws IOException {
// 1. 親ディレクトリがなければ作成
Files.createDirectories(configPath.getParent());
// 2. JARの外にあるファイルなので、Files.writeString で書き込み可能!
Files.writeString(configPath, jsonContent);
}
public String loadUserPreference() throws IOException {
if (!Files.exists(configPath)) return "{}";
return Files.readString(configPath);
}
}
2. メッセージ多言語化:messages.properties
【A】デフォルト (messages.properties)
※該当言語がない場合の予備(一般的に英語)。
error.notfound=User not found.
【B】日本語用 (messages_ja.properties)
error.notfound=ユーザーが見つかりませんでした。
【C】中国語用 (messages_zh_CN.properties)
error.notfound=未找到用户。
4. DB 自動初期化:schema.sql / data.sql
これらのファイルは、「application.properties の spring.datasource.url で指定した接続先」 に対して自動的に実行されます。別ファイルで紐付けを書く必要はありません。Spring Boot が「接続先が決まったから、ついでに初期化スクリプトも流そう」と判断してくれるためです。
【schema.sql:テーブル定義】
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
【data.sql:初期データ】
INSERT OR IGNORE INTO users (id, name) VALUES (1, 'テスト太郎');
その他のアノテーション(Lombok / Spring)
このセクションでは、本文で登場しきれなかった「よく見るけど意味が分かりにくい」アノテーションをまとめます。読者想定は Spring Boot 初心者です。
@Data や @AllArgsConstructor は Spring の機能ではなく、Lombok が提供するアノテーションです。
使うとコードが短くなりますが、何が自動生成されるのかを知らないと、デバッグ時に混乱しがちです。
A. @Data(全部盛り:Getter/Setter/toString/equals/hashCode)
@Data は、Lombok の「全部入り」です。次を自動生成します:
Getter / Setter / toString / equals / hashCode /(final以外の)RequiredArgsConstructor。
便利ですが、Entity などでは乱用しない方が安全です(理由は下で説明)。
// build.gradle / pom.xml で Lombok を追加した前提
@Data
public class UserDto {
private Long id;
private String name;
}
- JPA Entity: equals/hashCode が主キーや遅延ロードと絡み、予期せぬ挙動になることがあります。
- 可変オブジェクトの同一性: Setter があると、途中で値が変わり equals/hashCode の意味が壊れやすいです。
@Data でもOK、Entityでは
@Getter + 必要最小限の @Setter などに寄せるのが無難です。
@Data を避けたい場面(初心者向け:定義から説明)
1) まず用語の定義
JPA Entity(エンティティ)
JPA(Spring BootでDBを扱う仕組み)で、データベースの「1行」を表すJavaクラスです。
例:usersテーブルの1行(id=1, name="たかお")が、Javaでは User オブジェクト1個になります。
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
}
DTO(Data Transfer Object)
DTOは、画面やAPIの送受信のための「データを運ぶ箱」です。
DBの行そのものではなく、リクエスト/レスポンスに必要な項目だけを入れます。
DTOは保存対象ではないので、@Data や record と相性が良いです。
// 例:APIレスポンス用の箱(DTO)
public record UserDto(String name, int age) {}
遅延ロード(Lazy Loading)
「必要になった瞬間にDBから取りに行く」仕組みです。
たとえば注文(Order)からユーザー(User)を参照していても、最初はUserを読まず、
order.getUser() した瞬間にDBアクセスが走る、という動きになります。
2) なぜ Entity に @Data を付けるとハマりやすいのか
理由A:equals/hashCode が「全フィールド」で作られやすい
@Data は equals() と hashCode() も自動生成します。
その際、基本的に全フィールドを比較・計算に使います。
しかし Entity は「DBの1行」を表すので、実務ではid(主キー)だけで同一性を判断したいことが多いです。
@Data に任せると、意図せず「nameが変わったら別人扱い」などが起きやすくなります。
理由B:可変オブジェクトだと途中で値が変わり、同一性が壊れる
Entity には setter があることが多く、途中でフィールドが変わります。
もし hashCode() に使われている項目(例:name)が変更されると、
Set/Mapの中で見つからなくなるなどの不具合が起きます。
// イメージ:途中でフィールド変更すると Set の中で行方不明になり得る
Set<User> set = new HashSet<>();
User u = new User();
u.setName("たかお");
set.add(u);
u.setName("たろう"); // hashCodeに使われる値が変わると危険
set.contains(u); // false になることがある
理由C:遅延ロードと toString がぶつかる
@Data は toString() も自動生成します。
Entity が他のEntityを参照していて、それが遅延ロード(Lazy)だと、
ログに出しただけでDBアクセスが走る、状況によっては例外になることがあります。
3) 迷ったときの安全な指針
-
DTO(リクエスト/レスポンスの箱):
@DataやrecordでOK(全項目比較でも困りにくい) -
Entity(DBの1行):
@Dataは避け、@Getter+ 必要最小限の@Setterに寄せるのが無難 -
Entity の同一性は id(主キー)基準にしたいことが多いので、
@EqualsAndHashCode(of = "id")のように明示すると安全
// Entityの例(安全寄りの定番)
@Entity
@Getter
@Setter // 本当に必要なものだけにしたい場合は個別にsetterを書く
@EqualsAndHashCode(of = "id")
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
}
Entity クラスで使われているアノテーションの意味
以下のクラスは、Spring Boot + JPA でよく使われる 「安全寄りの Entity 定義」です。
@Entity
@Getter
@Setter
@EqualsAndHashCode(of = "id")
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
}
@Entity
このクラスが JPA Entity(データベースの1行)であることを示します。
Spring Boot はこのアノテーションを見て、
「このクラスはDBと対応付ける対象だ」と判断します。
これにより、SQLを自分で書かなくても、
このクラスをそのまま使って
保存(INSERT)・更新(UPDATE)・取得(SELECT)・削除(DELETE)
といったデータベース操作を
Javaコードだけで安全に行えるようになります。
@Entity が付いたクラスは、
保存・更新・削除の対象になります。
① @Entity を付けた場合(JPAがDB操作を肩代わり)
クラスに @Entity を付けると、
Spring Boot(JPA)がこのクラスを
「DBのテーブルと対応するもの」として認識します。
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
}
// Repository を使ったDB操作(SQL不要)
User user = new User();
user.setName("たかお");
userRepository.save(user); // INSERT
userRepository.findById(1L); // SELECT
userRepository.delete(user); // DELETE
このように、SQLを書かずに Java のメソッド呼び出しだけで DB操作ができるのが最大のメリットです。
findById 以外の find メソッドはある?
はい、たくさんあります。
Spring Data JPA では、
メソッド名を書くことで
自動的に SELECT 文を作ってくれます。
よく使われる find 系メソッド
// 全件取得
userRepository.findAll();
// 条件検索(メソッド名から自動生成)
userRepository.findByName("たかお");
userRepository.findByAge(24);
// 複数条件
userRepository.findByNameAndAge("たかお", 24);
// 部分一致
userRepository.findByNameContaining("たか");
// 存在チェック
userRepository.existsByName("たかお");
// 件数取得
userRepository.count();
その他に以下のようなものがあります(呼び出し例)
// ソート付き全件取得
userRepository.findAll(Sort.by("name").descending());
// ページング付き全件取得(0ページ目、10件)
userRepository.findAll(PageRequest.of(0, 10));
// 部分一致(LIKE %たか%)
userRepository.findByNameContaining("たか");
// 比較
userRepository.findByAgeGreaterThan(20);
userRepository.findByAgeBetween(20, 30);
// null 判定
userRepository.findByNameIsNull();
userRepository.findByNameIsNotNull();
// AND / OR
userRepository.findByNameAndAge("たかお", 24);
userRepository.findByNameOrAge("たかお", 24);
// 組み合わせ
userRepository.findByNameContainingAndAgeGreaterThan("たか", 20);
// IN / NOT IN
userRepository.findByAgeIn(List.of(10, 20));
userRepository.findByNameIn(List.of("たかお", "たろう"));
userRepository.findByAgeNotIn(List.of(10, 20));
これらは Repository インターフェースに「宣言」して使います
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByNameContaining(String name);
List<User> findByAgeGreaterThan(int age);
List<User> findByAgeBetween(int min, int max);
List<User> findByNameIsNull();
List<User> findByNameIsNotNull();
List<User> findByNameAndAge(String name, int age);
List<User> findByNameOrAge(String name, int age);
List<User> findByNameContainingAndAgeGreaterThan(String name, int age);
List<User> findByAgeIn(List<Integer> ages);
List<User> findByNameIn(List<String> names);
List<User> findByAgeNotIn(List<Integer> ages);
}
複雑になったら @Query に逃げてOK
@Query("""
SELECT u FROM User u
WHERE (u.name = :name OR u.age = :age)
AND u.email = :email
""")
List findCustom(@Param("name") String name,
@Param("age") int age,
@Param("email") String email);
@Query("SELECT u FROM User u WHERE u.age IN :ages")
List userRepository.findByAges(@Param("ages") List ages);
@Query("SELECT u FROM User u WHERE u.name LIKE %:name%")
List userRepository.findByNameLike(@Param("name") String name);
@Query("SELECT u FROM User u WHERE u.name = :name AND u.age > :age")
List userRepository.findByNameAndAge(@Param("name") String name, @Param("age") int age);
これらはすべて SQLを書かずに使える SELECT 操作です。
update(更新)はどうするの?
JPA では、専用の update メソッドはありません。
代わりに、save() を使って更新します。
① 最も基本的な更新方法(初心者向け)
// ① 取得(SELECT)
User user = userRepository.findById(1L).orElseThrow();
// ② 値を変更
user.setName("たろう");
// ③ 保存(UPDATE)
userRepository.save(user);
id がすでに存在する Entity を
save() すると、
JPA は UPDATE と判断します。
なぜ save() で INSERT と UPDATE を両方できるの?
JPA は id の状態を見て判断します。
id == null→ 新規(INSERT)id != null→ 既存(UPDATE)
② SQL っぽく update したい場合(中級者向け)
大量更新などで
「UPDATE 文を直接書きたい」場合は、
@Modifying を使います。
@Modifying
@Query("update User u set u.name = :name where u.id = :id")
void updateName(@Param("id") Long id,
@Param("name") String name);
ただしこれは、 Entity管理をすり抜けるため注意が必要です。 初心者のうちは無理に使う必要はありません。
初心者向けまとめ
| 操作 | 方法 |
|---|---|
| INSERT | save()(id が null) |
| SELECT | findById / findByXxx |
| UPDATE | 値変更 → save() |
| DELETE | delete() / deleteById() |
つまり JPA では、
「save は INSERT と UPDATE の両方を担当する」
という考え方になります。
② @Entity を付けなかった場合(ただのJavaクラス)
@Entity を付けない場合、
このクラスは ただの POJO(普通のJavaクラス) になります。
public class User {
private Long id;
private String name;
}
// JPAでは保存できない(エラー or そもそも対象外)
userRepository.save(user); // ❌ Entityではないため使えない
DBに保存したい場合は、 自分で SQL を書き、 JDBC や MyBatis などを使って 手動でマッピングする必要があります。
// 例:自分でSQLを書く必要があるイメージ
String sql = "INSERT INTO users(name) VALUES(?)";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setString(1, "たかお");
ps.executeUpdate();
③ 比較まとめ
| 項目 | @Entity あり | @Entity なし |
|---|---|---|
| DBとの対応 | 自動で対応付け | 対応なし |
| SQL記述 | 不要 | 必要 |
| save / find / delete | 使える | 使えない |
| 用途 | DBの1行を表す | 単なるデータの箱 |
つまり @Entity は、
「このクラスを DB と直結させるための宣言」です。
@Id
このフィールドが主キー(Primary Key)であることを示します。
DBの世界でいう「この行を一意に識別する番号」です。
JPAはこの id を使って、
「新規INSERTか、既存UPDATEか」を判断します。
@GeneratedValue
主キーの値は自動生成するという指定です。
Entity を保存するとき、
id は自分でセットせず、
DB(またはJPA)に番号を振ってもらいます。
保存後には、Entity オブジェクトに
自動的に id がセットされます。
@GeneratedValue は DB によって動きが変わる?
結論から言うと、はい、データベースによって内部の動きは変わります。
ただし、JPA(Spring Boot)が差分を吸収してくれるため、
基本的には意識せずに使えます。
@GeneratedValue は省略形で、
実際には次のような指定が隠れています。
@GeneratedValue(strategy = GenerationType.AUTO)
GenerationType.AUTO は
「使っているDBに一番合った方法を自動で選ぶ」
という意味です。
主な GenerationType と DB の関係
| DB | 主な仕組み | よく使われる strategy |
|---|---|---|
| SQLite | AUTOINCREMENT | IDENTITY / AUTO |
| MySQL | AUTO_INCREMENT | IDENTITY |
| PostgreSQL | SEQUENCE / SERIAL | SEQUENCE / AUTO |
| Oracle | SEQUENCE | SEQUENCE |
| SQL Server | IDENTITY | IDENTITY |
よく使われる指定例
① DB に完全に任せる(初心者向け・無難)
@Id
@GeneratedValue
private Long id;
AUTO が使われ、
DBごとの最適な方法が自動選択されます。
② MySQL / SQL Server を明示する場合
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
AUTO_INCREMENT / IDENTITY を使う方式です。
③ PostgreSQL / Oracle で SEQUENCE を使う場合
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
シーケンスオブジェクトを使って IDを生成します。
初心者が知っておけば十分なポイント
-
@GeneratedValueは DBごとの違いをJPAが吸収してくれる仕組み -
まずは
@GeneratedValue(AUTO)で問題ない -
DBを固定して運用する場合のみ
strategyを明示することが多い
つまり、
「DBが変わるたびにEntityを書き換える必要は基本的にない」
というのが @GeneratedValue の嬉しい点です。
@Getter
Lombok のアノテーションで、 全フィールドの getter メソッドを自動生成します。
Entity では「読むだけ」は安全なことが多いため、
@Getter は基本的に付けて問題ありません。
@Setter
Lombok のアノテーションで、 全フィールドの setter メソッドを自動生成します。
Entity では値の変更が「同一性の破壊」につながることがあるため、 本当に必要なものだけ setter を許可するのが理想です。
初心者のうちは全体に @Setter を付けても構いませんが、
慣れてきたら個別に setter を書く方が安全です。
@EqualsAndHashCode(of = "id")
Lombok のアノテーションで、 equals() と hashCode() を自動生成します。
of = "id" を指定することで、
id だけを使って同一性を判定するようになります。
これは JPA Entity の考え方と一致します:
- 名前や年齢が変わっても
- id が同じなら「同じEntity」
もし @Data を使うと、
全フィールドが equals/hashCode の対象になり、
Set や Map、遅延ロードと組み合わさって
予期しない挙動を引き起こしやすくなります。
まとめ(初心者向け)
@Entity:DBの1行を表すクラス@Id:主キー(身分証明書)@GeneratedValue:主キーは自動採番@Getter:読むだけは安全@Setter:必要最小限が理想-
@EqualsAndHashCode(of = "id"): Entityの同一性は id 基準
この組み合わせは、 初心者でも事故を起こしにくい Entity 定義として 非常によく使われます。
B. @AllArgsConstructor(全フィールド引数のコンストラクタ)
@AllArgsConstructor は、全フィールドを引数に取るコンストラクタを自動生成します。
「new する時に全部渡す」用途や、テストで便利です。
@AllArgsConstructor
public class Point {
private int x;
private int y;
}
// 生成されるイメージ:
// public Point(int x, int y) { this.x = x; this.y = y; }
// 利用例
var p = new Point(10, 20);
その場合は
@Builder(後述)や Record の利用も検討してください。
C. @NoArgsConstructor(引数なしコンストラクタ)
@NoArgsConstructor は 引数なし(デフォルト)コンストラクタを生成します。
JPA(Entity) では「引数なしコンストラクタが必要」なことが多く、よく使われます。
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
private Long id;
private String name;
}
D. @RequiredArgsConstructor(final / @NonNull だけのコンストラクタ)
@RequiredArgsConstructor は、final フィールド(または @NonNull が付いたフィールド)だけを引数に取るコンストラクタを生成します。
Spring Boot では「コンストラクタインジェクション」に直結するので、現場で非常に重要です。
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public UserDto find(Long id) {
var user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("not found"));
return new UserDto(user.getId(), user.getName());
}
}
final にすると「この依存は必ず必要(途中で差し替えない)」がコードで表現されます。その結果、Spring が自動生成したコンストラクタにより、DIの漏れがコンパイル時に気づきやすくなります。
E. @Builder(引数地獄を避ける:読みやすい生成)
フィールドが多いクラスで new の引数が増えてきたら、@Builder が便利です。
「名前付き引数っぽく」書けるため、初心者でも読み間違いを減らせます。
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CreateUserRequest {
private String name;
private int age;
}
// 利用例(どの値が何か分かりやすい)
var req = CreateUserRequest.builder()
.name("Taro")
.age(20)
.build();
F. @Slf4j(ログ用:System.out.println の卒業)
@Slf4j を付けると、log というロガーが自動生成されます。
REST API では標準出力よりログが基本です。
@Slf4j
@RestController
public class HealthController {
@GetMapping("/health")
public String health() {
log.info("health called");
return "OK";
}
}
log.info:通常の処理の節目(起動/受付/完了など)log.warn:想定外だが継続できる(入力不備など)log.error:処理継続が難しい(例外、DB障害など)
Spring Boot では、application.properties に以下の設定を追加するだけで、ログをファイルに出力できます。
# ログをファイルに出力する設定
logging.file.name=app.log
logging.file.path=logs
これで、アプリの実行ディレクトリに logs/app.log が生成され、ログが書き込まれるようになります。
# ログの出力例(app.log)
2024-06-01 12:00:00.000 INFO 12345 --- [ main] c.e.demo.DemoApplication : Starting DemoApplication using Java 17
2024-06-01 12:00:01.000 INFO 12345 --- [ main] c.e.demo.DemoApplication : Started DemoApplication in 1.234 seconds (JVM running for 1.567)
# ログのローテーション設定例(app.log.2024-06-01 などに日付ごとに分割)
logging.file.name=app.log
logging.file.path=logs
logging.file.rotation=10MB
logging.file.rotation.strategy=TIME_BASED
logging.file.rotation.time-based.rotation-pattern=app.log.%d{yyyy-MM-dd}
まとめ:初心者のおすすめ使い分け
| 目的 | おすすめ | 理由 |
|---|---|---|
| DTO(Request/Response) | @Data でもOK | 短く書ける。equals/hashCode も大きな問題になりにくい |
| Service のDI | @RequiredArgsConstructor + final | コンストラクタ注入が安全でテストもしやすい |
| 値オブジェクト | Record / @Value | 不変にしてバグを減らせる |
| ログ | @Slf4j | println をやめて運用可能なログへ |
作成したjarファイルを配布するには?
Spring Bootアプリは、ビルドしてできた .jar をそのまま別PC/別サーバーへ渡して実行できます(Javaが入っていれば動きます)。ここでは初心者向けに、実際の配布手順を具体的にまとめます。
1) まずはjarを作成する(ローカル)
Gradleの場合(一般的なSpring Bootプロジェクト)
# プロジェクト直下で
./gradlew clean bootJar
# Windowsなら
gradlew.bat clean bootJar
作成物は通常 build/libs/ に出ます。
Mavenの場合
mvn -DskipTests clean package
作成物は通常 target/ に出ます(Spring Bootの実行jar)。
2) jarを相手に渡す(配布)
jarは1ファイルなので、方法は何でもOKです。
- 社内: 共有フォルダ、ファイルサーバー(NAS)、Teams/Slackの添付
- 外部: S3 / Google Drive / Dropbox / さくらのストレージ 等
- サーバーへ:
scpやWinSCPで転送
例:Linuxサーバーへscpで転送
# 例: build/libs/app-0.0.1-SNAPSHOT.jar をサーバーへ
scp build/libs/app-0.0.1-SNAPSHOT.jar user@your-server:/opt/myapp/
3) 相手側で実行する(Javaが必要)
相手の環境にJava(JRE/JDK)が入っていれば実行できます。確認:
java -version
実行は次の1行です。
java -jar app-0.0.1-SNAPSHOT.jar
4) 設定ファイルを外出しして配布する(application.properties / yml)
本番や配布では、DB接続など環境ごとに変わる設定をjarの外に置くのが定番です。代表的なやり方は次の3つです。
A. 起動ディレクトリに application.properties を置く(最も簡単)
jarと同じフォルダに application.properties(または application.yml)を置くと、Spring Bootが読み込みます。
/opt/myapp/
├─ app-0.0.1-SNAPSHOT.jar
└─ application.properties
cd /opt/myapp
java -jar app-0.0.1-SNAPSHOT.jar
B. --spring.config.location で指定する(確実)
java -jar app-0.0.1-SNAPSHOT.jar --spring.config.location=/opt/myapp/config/application.properties
C. 環境変数で渡す(Docker/CIでよく使う)
export SPRING_PROFILES_ACTIVE=prod
export SPRING_DATASOURCE_URL=jdbc:postgresql://dbhost:5432/app
java -jar app-0.0.1-SNAPSHOT.jar
5) ポート番号を変えて起動する
配布先でポート競合する場合は、起動時に上書きできます。
java -jar app-0.0.1-SNAPSHOT.jar --server.port=8081
6) バックグラウンド起動(Linuxで常駐させる最小例)
開発用途なら、次のようにバックグラウンド起動もできます(本番は systemd 化推奨)。
nohup java -jar app-0.0.1-SNAPSHOT.jar > logs/app.out 2>&1 &
tail -f logs/app.out
7) 配布用に「実行手順書」を同梱すると親切
初心者が受け取っても迷わないように、最低でも次をセットで渡すのがおすすめです。
- jar(アプリ本体)
- 設定ファイル(application.properties など、必要なら)
- README(起動方法、必要なJavaバージョン、ポート、停止方法)
仕様説明セクション:注文・請求ルールエンジン(Spring Boot版)
1. このサンプルで学ぶこと(Spring Boot / Javaでの置き換え)
このサンプルは、Webアプリの「通信」そのものではなく、業務ロジックの中で Java(+ Spring Boot)をどう使うかに焦点を当てています。 Controller は最小にして、計算の本体を Service に寄せます。目的は「文法の説明」ではなく、実際の業務ルールの中で文法がどう使われるかを理解することです。
final(TypeScriptのconst相当)と、再代入するローカル変数(var/int/BigDecimalなど)の使い分け- 基本型・参照型:
int,String,boolean,BigDecimal - DTO(入力/出力モデル):
recordもしくは POJO(getter/setter) enum(業務上の区分:税区分・会員ランク・配送地域)for-each(拡張 for)による配列(List)処理(TypeScriptのfor...of相当)switchによる条件分岐- Stream API:
map/reduce/collectによる集計(TypeScriptの map/reduce 相当) - Spring Boot の最小構成:
@RestController→@Service→ ドメイン計算 - 入力バリデーション:
@Valid, Bean Validation(実務で重要)
2. 業務の概要
ユーザーから渡された「注文データ」を元に、以下を計算します。
- 商品ごとの金額(行小計)と税額
- 顧客ランクやクーポンによる割引
- 配送地域による送料
- 最終的な請求金額
3. 入力データの考え方
入力は JSON(実務では HTTP request body)です。Spring Boot では @RequestBody で DTO にデシリアライズされます。
重要なポイント:
- 配列(注文行)は
List<OrderItem>として受け、for-eachで処理する - 区分値(税区分・会員ランク・地域)は
enumで表現する - 計算途中の値は、可能な限り
finalで保持する(不変に寄せてバグを減らす)
4. 主な業務ルール
税計算
- STANDARD:10%
- REDUCED:8%
- EXEMPT:0%
割引
- GOLD 会員:小計の 5%
- WELCOME10 クーポン:小計の 10%
送料
- TOKYO:500円
- OTHER:800円
- 小計 10,000円以上で送料無料
💡 なぜ enum を使うのか
税区分や会員ランクは「文字列」ではなく、業務上の決まった選択肢です。
enum を使うことで、タイプミスや想定外の値を防げます(Springのデシリアライズ時点で弾ける/検知できる)。
5. 入力 / 出力データ仕様(コメント付き)
入力(注文データ):業務ロジックに渡される入力データ(HTTP request body 相当)
// ※ 説明用のためコメント付き(実際の JSON ではコメント不可)
{
"customerRank": "GOLD", // 顧客ランク(enum: BRONZE / SILVER / GOLD)
"region": "TOKYO", // 配送地域(enum: TOKYO / OTHER)
"items": [ // 注文行の配列
{
"sku": "A001", // 商品コード
"unitPrice": 980, // 単価(実務では BigDecimal 推奨)
"qty": 2, // 数量(int)
"tax": "STANDARD" // 税区分(enum)
},
{
"sku": "B777",
"unitPrice": 1500,
"qty": 1,
"tax": "REDUCED"
}
],
"coupon": "WELCOME10" // クーポンコード(任意・optional)
}
💡 ポイント
- items は配列なので
for-eachで処理する - customerRank や tax は
enumで安全に扱う - coupon は あってもなくてもよい(
null/ optional)
出力(計算結果):APIレスポンスや画面表示用に使える
// 計算結果(出力)
{
"subtotal": 3460, // 税抜小計(全行の lineSubtotal 合計)
"taxTotal": 292, // 税額合計
"discount": 346, // 割引合計(会員 + クーポン)
"shipping": 500, // 送料
"grandTotal": 3906 // 最終請求金額
}
💡 計算の流れ
- 各行の lineSubtotal(単価×数量)を計算
- 税区分ごとに税額を算出
- 小計・税合計を集計(Stream の
map/reduceでも可) - 割引・送料を適用
- 最終合計を算出
⚠ 実務上の注意
TypeScript と違い Java の型は実行時にも存在しますが、外部入力(JSON)は信用できません。
Spring Boot では @Valid + Bean Validation で入力を検証してから処理します。
enum 変換に失敗した場合は 400 が返るため、APIとしての安全性が上がります。
コードサンプルセクション(Spring Boot / Java)
Controller は薄く、計算は Service に集約します。
enum / for-each / switch / Stream の map/reduce を「業務ルール」で使う例です。
// (1) Enum:業務上の区分
package com.example.billing.domain;
public enum CustomerRank { BRONZE, SILVER, GOLD }
public enum Region { TOKYO, OTHER }
public enum TaxCategory { STANDARD, REDUCED, EXEMPT }
// (2) DTO:入力モデル(@RequestBodyで受ける)
// Java 17+ なら record が読みやすい。Bean Validation も付ける。
package com.example.billing.api;
import com.example.billing.domain.*;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import java.util.List;
public record OrderInput(
@NotNull CustomerRank customerRank,
@NotNull Region region,
@NotEmpty @Valid List<OrderItem> items,
// optional(なくてもよい)
String coupon
) {}
public record OrderItem(
@NotBlank String sku,
@NotNull @DecimalMin(value = "0.00", inclusive = false)
BigDecimal unitPrice,
@Min(1) int qty,
@NotNull TaxCategory tax
) {}
// (3) DTO:出力モデル(レスポンス)
package com.example.billing.api;
import java.math.BigDecimal;
public record BillingResult(
BigDecimal subtotal,
BigDecimal taxTotal,
BigDecimal discount,
BigDecimal shipping,
BigDecimal grandTotal
) {}
// (4) Controller:最小(通信は薄く、業務ロジックはServiceへ)
package com.example.billing.api;
import com.example.billing.service.BillingService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/billing")
public class BillingController {
private final BillingService billingService;
public BillingController(BillingService billingService) {
this.billingService = billingService;
}
@PostMapping("/calculate")
public BillingResult calculate(@Valid @RequestBody OrderInput input) {
return billingService.calculate(input);
}
}
// (5) Service:業務ルール本体(enum / for-each / switch / map&reduce)
package com.example.billing.service;
import com.example.billing.api.*;
import com.example.billing.domain.*;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Map;
@Service
public class BillingService {
// 固定表は不変 Map(final)で持つ:変更されない業務定数
private static final Map<Region, BigDecimal> SHIPPING_FEE = Map.of(
Region.TOKYO, new BigDecimal("500"),
Region.OTHER, new BigDecimal("800")
);
private static final BigDecimal FREE_SHIPPING_THRESHOLD = new BigDecimal("10000");
private static final BigDecimal TAX_STANDARD = new BigDecimal("0.10");
private static final BigDecimal TAX_REDUCED = new BigDecimal("0.08");
private static final BigDecimal TAX_EXEMPT = new BigDecimal("0.00");
private static final BigDecimal DISCOUNT_GOLD_RATE = new BigDecimal("0.05");
private static final BigDecimal DISCOUNT_WELCOME10_RATE = new BigDecimal("0.10");
private static final String COUPON_WELCOME10 = "WELCOME10";
public BillingResult calculate(OrderInput input) {
// (A) for-each:注文行を処理して積み上げる(TypeScript for...of 相当)
BigDecimal subtotal = BigDecimal.ZERO;
BigDecimal taxTotal = BigDecimal.ZERO;
for (OrderItem item : input.items()) {
final BigDecimal lineSubtotal = item.unitPrice().multiply(BigDecimal.valueOf(item.qty()));
final BigDecimal lineTax = calculateTax(lineSubtotal, item.tax());
subtotal = subtotal.add(lineSubtotal);
taxTotal = taxTotal.add(lineTax);
}
// (B) Stream:map/reduce 相当(学習用に例示)
// 例:小計 = items.map(lineSubtotal).reduce(sum)
final BigDecimal subtotalByStream = input.items().stream()
.map(i -> i.unitPrice().multiply(BigDecimal.valueOf(i.qty()))) // map
.reduce(BigDecimal.ZERO, BigDecimal::add); // reduce
// subtotal と subtotalByStream は一致する想定(デバッグ用途)
// System.out.println("subtotal=" + subtotal + ", subtotalByStream=" + subtotalByStream);
// (C) 割引(会員 + クーポン)
final BigDecimal rankDiscount = calculateRankDiscount(subtotal, input.customerRank());
final BigDecimal couponDiscount = calculateCouponDiscount(subtotal, input.coupon());
BigDecimal discount = rankDiscount.add(couponDiscount);
// (D) 送料
final BigDecimal shipping = calculateShipping(subtotal, input.region());
// (E) 最終合計
final BigDecimal grandTotal = subtotal.add(taxTotal).subtract(discount).add(shipping);
// 端数処理(円単位想定:小数なし)
return new BillingResult(
roundYen(subtotal),
roundYen(taxTotal),
roundYen(discount),
roundYen(shipping),
roundYen(grandTotal)
);
}
private BigDecimal calculateTax(BigDecimal lineSubtotal, TaxCategory tax) {
// switch:enum の区分で税率を決める
final BigDecimal rate = switch (tax) {
case STANDARD -> TAX_STANDARD;
case REDUCED -> TAX_REDUCED;
case EXEMPT -> TAX_EXEMPT;
};
return roundYen(lineSubtotal.multiply(rate));
}
private BigDecimal calculateRankDiscount(BigDecimal subtotal, CustomerRank rank) {
// GOLD: 5%
if (rank == CustomerRank.GOLD) {
return roundYen(subtotal.multiply(DISCOUNT_GOLD_RATE));
}
return BigDecimal.ZERO;
}
private BigDecimal calculateCouponDiscount(BigDecimal subtotal, String coupon) {
// WELCOME10: 10%(coupon は optional)
if (coupon != null && coupon.equalsIgnoreCase(COUPON_WELCOME10)) {
return roundYen(subtotal.multiply(DISCOUNT_WELCOME10_RATE));
}
return BigDecimal.ZERO;
}
private BigDecimal calculateShipping(BigDecimal subtotal, Region region) {
// 小計 10,000円以上は送料無料
if (subtotal.compareTo(FREE_SHIPPING_THRESHOLD) >= 0) {
return BigDecimal.ZERO;
}
return SHIPPING_FEE.get(region);
}
private static BigDecimal roundYen(BigDecimal value) {
// 「円」想定で小数なし丸め(四捨五入)
return value.setScale(0, RoundingMode.HALF_UP);
}
}
// (6) 動作確認用の curl(例)
// POST /api/billing/calculate
// Content-Type: application/json
//
// curl -X POST http://localhost:8080/api/billing/calculate \
// -H "Content-Type: application/json" \
// -d '{
// "customerRank": "GOLD",
// "region": "TOKYO",
// "items": [
// { "sku": "A001", "unitPrice": 980, "qty": 2, "tax": "STANDARD" },
// { "sku": "B777", "unitPrice": 1500, "qty": 1, "tax": "REDUCED" }
// ],
// "coupon": "WELCOME10"
// }'
補足(実務寄り)
- 金額は
doubleではなくBigDecimalを使う(誤差対策) - 入力チェックは
@Valid+ Bean Validation(@NotNull,@Min,@DecimalMinなど)で行う - enum の不正値はデシリアライズ時に 400 になりやすい(安全)
- Controller を薄くして、業務計算は Service に集約するとテストしやすい
Jackson(ジャクソン)は、JSON と Java オブジェクトを相互変換(シリアライズ / デシリアライズ)するための Java ライブラリです。
Spring Boot では spring-boot-starter-web に含まれており、
@RequestBody / @ResponseBody を使った瞬間に自動的に動作します。
主な役割
- HTTPリクエストの JSON → Java DTO 変換
- Javaオブジェクトの Java → JSON 変換(HTTPレスポンス)
enum,BigDecimal,LocalDateなどの型変換- JSONがJavaの型に合わない場合の 入力エラー検出
JSON (HTTP body)
↓ Jackson
Java Object (DTO)
↓ 業務ロジック
Java Object
↓ Jackson
JSON (HTTP response)
これは避けられない事象ですか?
原則として 避けられません(そして多くの場合 避けるべきでもありません)。
@RequestBody で JSON を DTO に変換する以上、変換処理が必須で、その担当が Jackson だからです。
特に enum は「選べる値が決まっている」型なので、JSON に未知の文字列が来ると Jackson は変換できず、
業務ロジックに入る前に 400(Bad Request) になりやすいです。
これは「事故」ではなく、不正データを早期に遮断する安全装置です。
※ ただし、どうしても「外部APIが雑な値を返す」などの理由がある場合は、
カスタム変換(Deserializer / @JsonCreator など)で「ゆるく受けて内部で正規化」する設計は可能です。
これは回避ではなく「制御」です。
「ユーザー設定を丸ごと session に入れる」のは 普通ではない。
多くの現場では 状況に応じて分ける のが普通です。
まず前提整理(ここ超重要)
Spring Boot の HttpSession は:
- ユーザー単位でサーバーメモリを占有
- クラスタ構成だと セッション共有(Redis 等)が必要
- 中身が大きいほど メモリ & シリアライズコスト増
なので
👉 「設定ファイルだから session に入れとけ」は雑設計扱いされがちです。
パターン別に整理します
① ユーザー設定を session に入れるパターン
使われることはあるが、限定的
向いているケース
- ログイン後ほぼ 全画面で使う
- サイズが 小さい(数KB以下)
- 1リクエストごとに DB / API 呼ぶのが明らかに無駄
- 設定が 頻繁に変わらない
// ログイン時
session.setAttribute("userSetting", userSetting);
問題点
- セッション肥大化
- 設定変更時の session更新漏れ
- 分散環境で地獄(Redis必須)
👉 「ログインユーザーの言語・テーマ・権限レベル」くらいならアリ
👉 「設定ファイル丸ごと」はやりすぎ
② 画面から毎回「設定取得API」を呼ぶパターン
今どき一番多い
特徴
- SPA / Thymeleaf どちらでも自然
- ステートレス設計
- キャッシュ戦略が組める
GET /api/user/settings
メリット
- session を太らせない
- 設定変更に即追従できる
- スケールしやすい
デメリット
- 呼びすぎると無駄
👉 なので普通は キャッシュとセットです。
③ サーバー側でキャッシュ(おすすめ)
業務系ではこれが一番バランスいい
Controller
↓
Service
↓
Cache(Caffeine / Redis)
↓
DB
@Cacheable(value = "userSettings", key = "#userId")
public UserSetting getUserSetting(Long userId) {
return repository.findByUserId(userId);
}
- 画面は普通に API 呼ぶ
- サーバー側でキャッシュ
- session は最小限
👉 「session=認証情報だけ」にしておくと設計が綺麗
実務でよくある“正解寄り”の構成
| 情報 | 置き場所 |
|---|---|
| ログインID | session / SecurityContext |
| 権限・ロール | session(or JWT) |
| 言語・テーマ | session or cookie |
| ユーザー設定(詳細) | API + Cache |
| 設定ファイル丸ごと | ❌避ける |
「設定ファイル」という言葉が出てきた時の注意
もしあなたの言う 「ユーザー設定ファイル」が:
- JSON
- 画面レイアウト
- 業務ルール定義
- フィルタ条件
みたいな そこそこ大きい構造なら、
👉 session に入れた瞬間にレビューで突っ込まれる可能性高いです。
まとめ(判断基準)
こう考えると迷いません👇
session に入れていいのは「毎リクエスト必須・小さい・変わりにくいもの」だけ
それ以外は
✅ API取得 + サーバーキャッシュ
これが Spring Boot では「普通で怒られにくい」やり方です。