Spring Boot REST API 入門

はい、承知いたしました。Spring Bootを使ったREST API開発の入門について、約5000語の詳細な記事を記述します。


Spring Boot REST API 入門:モダンなWeb API開発を始めよう

はじめに

現代のソフトウェア開発において、異なるシステム間での連携や、フロントエンド(Webブラウザ、モバイルアプリ)とバックエンド間のデータ通信は不可欠です。その中心的な役割を担っているのが、RESTful API(以降、REST API)です。REST APIは、シンプルで標準的なHTTPプロトコルをベースに、リソース指向のアーキテクチャスタイルを提供するため、広く普及しています。

本記事では、Java開発で最も人気のあるフレームワークの一つであるSpring Bootを使って、効率的かつ堅牢なREST APIを開発する方法を、入門者向けに詳細に解説します。Spring Bootが提供する様々な機能を利用することで、煩雑な設定から解放され、ビジネスロジックの実装に集中できるようになります。

この記事を通じて、以下のことを学ぶことができます。

  • REST APIの基本的な考え方
  • Spring Bootを使ったREST APIプロジェクトの作成方法
  • HTTPメソッドとURIを使ったAPIエンドポイントの実装
  • リクエストパラメータ、パス変数、リクエストボディの扱い方
  • レスポンスの生成とHTTPステータスコード
  • データベースとの連携(Spring Data JPA)
  • サービス層を使ったビジネスロジックの分離
  • 例外処理と入力値検証
  • データ転送オブジェクト(DTO)の活用
  • 簡単なAPIテスト方法

対象読者は、Javaプログラミングの基本的な知識があり、これからSpring Bootを使ってWeb API開発を始めたいと考えている方です。

早速、モダンなREST API開発の世界へ踏み出しましょう!

1. REST APIとは?なぜ重要か?

REST(Representational State Transfer)は、ソフトウェアアーキテクチャスタイルの一つであり、Webの基本原則に基づいています。RESTfulなシステムでは、データや機能は「リソース」として表現され、URI(Uniform Resource Identifier)によって一意に識別されます。クライアントは、HTTPメソッド(GET, POST, PUT, DELETEなど)を使って、これらのリソースに対して操作を行います。

REST APIの主な特徴は以下の通りです。

  • ステートレス: 各リクエストは独立しており、サーバーは過去のリクエストに関する情報を保持しません。
  • クライアント・サーバー: クライアントとサーバーは分離されており、それぞれ独立して進化できます。
  • キャッシュ可能: クライアントはレスポンスをキャッシュし、パフォーマンスを向上させることができます。
  • 統一インターフェース: リソースへのアクセス方法が統一されています(HTTPメソッド)。
  • 階層化システム: システムは複数の層に分割され、各層は互いの内部構造を知る必要がありません。

なぜREST APIが重要なのでしょうか?

  • 普及と互換性: HTTPという標準プロトコルに基づいているため、様々なプログラミング言語やプラットフォームから簡単にアクセスできます。
  • シンプルさ: SOAPなどの他のAPIスタイルに比べてシンプルで、学習コストが低いとされています。
  • スケーラビリティ: ステートレスであるため、複数のサーバーでリクエストを分散処理しやすく、システムの水平スケーリングに適しています。
  • 柔軟性: クライアントとサーバーが分離しているため、それぞれ独立して開発・更新が可能です。

これらの理由から、Webアプリケーションのバックエンド、モバイルアプリケーションのAPI、サービス間連携など、幅広い分野でREST APIは利用されています。

2. Spring Bootとは?なぜREST API開発に適しているか?

Spring Bootは、Springフレームワークをベースにした、アプリケーション開発を容易にするためのフレームワークです。特に、設定の簡略化、開発速度の向上、運用性の向上に重点が置かれています。

Spring BootがREST API開発に特に適している理由は以下の通りです。

  • 自動設定 (Auto-configuration): 依存関係やクラスパスに基づいて、Springの設定を自動的に行います。これにより、XML設定ファイルや複雑なJavaConfigクラスを書く必要がほとんどなくなります。Web開発に必要なDispatcherServletや組み込みサーバー(Tomcat, Jettyなど)も自動的に設定されます。
  • スターター依存関係 (Starter Dependencies): 関連するライブラリ群をまとめて提供する依存関係です。例えば、spring-boot-starter-web を追加するだけで、Webアプリケーション開発に必要なSpring MVC,組み込みTomcat, JSON処理ライブラリなどがまとめて含まれます。
  • 組み込みサーバー: Tomcat, Jetty, UndertowといったWebサーバーが同梱されており、外部にサーバーをインストールすることなく、jarファイル一つでアプリケーションを実行できます。
  • 生産性の高さ: アノテーションベースの設定、開発ツール(DevTools)によるホットリロードなど、開発者の生産性を向上させる機能が豊富です。
  • Springエコシステムとの連携: Spring Data, Spring Securityなど、Springの他のプロジェクトとシームレスに連携できます。これは、データベースアクセスやセキュリティ機能など、API開発において不可欠な要素を容易に実装できることを意味します。

Spring Bootを使うことで、REST API開発における煩雑なインフラ設定から解放され、APIエンドポイントの実装やビジネスロジックに集中できるため、迅速かつ効率的に開発を進めることができます。

3. 開発環境の準備

Spring Bootアプリケーションを開発するには、以下のものが必要です。

  1. Java Development Kit (JDK): Spring Bootのバージョンによって推奨されるJDKバージョンが異なります。最新のLTSバージョン(例: Java 11, 17, 21)を使用するのがおすすめです。Oracle JDKやOpenJDKなど、好きなディストリビューションを選択できます。
  2. ビルドツール: MavenまたはGradle。プロジェクトの依存関係管理やビルドを行います。どちらを使っても構いませんが、本記事ではMavenを中心に説明します。
  3. IDE (統合開発環境): Spring Boot開発を効率的に行うために必須です。以下のいずれかがおすすめです。
    • Spring Tool Suite (STS): EclipseベースでSpring開発に特化したIDEです。
    • IntelliJ IDEA (Community Edition or Ultimate Edition): Spring Bootの強力なサポートがあります。Community Editionでも基本的な開発は可能です。
    • VS Code: Spring Bootの拡張機能を入れることで開発が可能です。
  4. APIクライアント: 開発したAPIをテストするために使用します。
    • Postman: GUIベースで使いやすいAPI開発・テストツールです。
    • curl: コマンドラインからAPIを呼び出すツールです(macOS, Linuxには標準搭載、WindowsにもPowerShellなどで利用可能)。
    • IDEの組み込み機能(例: IntelliJ IDEAのHTTP Client)。

これらのツールをインストール・設定しておきましょう。JDKとMaven/Gradleのインストール後、コマンドプロンプトやターミナルでバージョン確認ができれば準備完了です。

bash
java -version
mvn -version # または gradle -v

4. Spring Bootプロジェクトの作成

最も簡単なSpring Bootプロジェクトの作成方法は、WebブラウザからSpring Initializrを利用することです。

  1. Webブラウザで https://start.spring.io/ にアクセスします。
  2. プロジェクト設定を行います。

    • Project: Maven Project または Gradle Project を選択します。本記事では Maven Project を選択します。
    • Language: Java を選択します。
    • Spring Boot: 最新の安定版を選択します。
    • Project Metadata:
      • Group: パッケージ名の接頭辞(例: com.example
      • Artifact: プロジェクト名(例: demo-api
      • Name: プロジェクト名(Artifactと同じでOK)
      • Description: プロジェクトの説明(任意)
      • Package name: GroupとArtifactを組み合わせたパッケージ名(例: com.example.demoapi
      • Packaging: Jar を選択します(Webアプリケーションとして自己完結的に実行するため)。
      • Java: 使用するJavaのバージョンを選択します(インストール済みのJDKバージョンと合わせる)。
    • Dependencies: プロジェクトで利用する依存関係を追加します。REST API開発では必須の依存関係は以下の通りです。
      • Spring Web: RESTfulアプリケーションを構築するための依存関係です。Spring MVCと組み込みTomcatが含まれます。
      • Spring Boot DevTools: 開発効率を上げるためのツールです(コード変更時の自動再起動など)。開発中のみ有効にするのが一般的です。
      • 後ほど、データベース連携のために Spring Data JPAH2 Database (または他のデータベースドライバー)を追加します。
  3. 設定が終わったら、「Generate」ボタンをクリックします。プロジェクトがZIPファイルとしてダウンロードされます。

  4. ダウンロードしたZIPファイルを解凍し、お好みのIDEでプロジェクトを開きます。

IDEでプロジェクトを開くと、Mavenが依存関係をダウンロードします。プロジェクト構造は以下のようになっているはずです。

demo-api
├── .mvn
│ └── wrapper
│ ├── maven-wrapper.jar
│ └── maven-wrapper.properties
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── demoapi
│ │ │ └── DemoApiApplication.java <-- アプリケーションのエントリポイント
│ │ └── resources
│ │ ├── application.properties <-- アプリケーション設定ファイル
│ │ ├── static <-- 静的コンテンツ(HTML, CSSなど)
│ │ └── templates <-- サーバーサイドテンプレート
│ └── test
│ └── java
│ └── com
│ └── example
│ └── demoapi
│ └── DemoApiApplicationTests.java <-- テストクラス
├── .gitignore
├── mvnw <-- Maven Wrapper (Linux/macOS)
├── mvnw.cmd <-- Maven Wrapper (Windows)
└── pom.xml <-- Mavenプロジェクト設定ファイル

pom.xml ファイルを開くと、Spring Initializrで選択した依存関係が記述されています。spring-boot-starter-webspring-boot-starter-test などが含まれていることを確認してください。

src/main/java/.../DemoApiApplication.java は、Spring Bootアプリケーションのエントリポイントとなるクラスです。@SpringBootApplication アノテーションが付与されており、main メソッドからアプリケーションが起動されます。

5. 最初のRESTエンドポイントを作成する

いよいよ、REST APIの最初のエンドポイントを作成します。REST APIでは、クライアントからのリクエストを受け付けて処理し、レスポンスを返す役割を担うのが「コントローラー(Controller)」です。

Spring Bootでは、Spring MVCの機能を使ってコントローラーを実装します。REST APIの場合は、特に @RestController アノテーションが付与されたクラスを使用するのが一般的です。@RestController@Controller@ResponseBody を組み合わせたアノテーションで、このクラス内のメソッドが返す値が、そのままHTTPレスポンスボディとして返されることを意味します(通常はJSON形式に変換されます)。

プロジェクトのパッケージ(例: com.example.demoapi)配下に、新しいJavaクラスを作成しましょう。例えば HelloController.java という名前にします。

“`java
package com.example.demoapi;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController // このクラスがRESTコントローラーであることを示す
public class HelloController {

// HTTP GETリクエストを "/hello" パスにマッピングするメソッド
@GetMapping("/hello")
public String hello() {
    return "Hello, Spring Boot REST API!"; // レスポンスとして返す文字列
}

}
“`

このコードでは、以下のことを行っています。

  • @RestController: このクラスがRESTfulなリクエストを処理するコントローラーであることを宣言します。
  • @GetMapping("/hello"): HTTP GETメソッドによる /hello へのリクエストを、この hello() メソッドにマッピングします。@RequestMapping(method = RequestMethod.GET, value = "/hello") と同じ意味ですが、より簡潔に記述できます。他のHTTPメソッドに対応するアノテーションとして、@PostMapping, @PutMapping, @DeleteMapping, @PatchMapping などがあります。
  • hello() メソッド: リクエストが来たときに実行されるメソッドです。ここでは単純な文字列を返しています。@RestController のおかげで、この文字列がHTTPレスポンスボディとして返送されます。

アプリケーションの実行:

IDEから DemoApiApplication.javamain メソッドを実行するか、ターミナルからMaven Wrapperを使って実行します。

“`bash

プロジェクトのルートディレクトリで実行

./mvnw spring-boot:run
“`

アプリケーションが起動すると、組み込みTomcatがデフォルトポートの8080で起動します。ログに以下のような出力が表示されるはずです。

... Tomcat started on port(s): 8080 (http) with context path '' ...

APIのテスト:

起動したら、APIクライアントを使って作成したエンドポイントにアクセスしてみましょう。

  • curlの場合:
    bash
    curl http://localhost:8080/hello

    ターミナルに "Hello, Spring Boot REST API!" と表示されれば成功です。

  • Postmanの場合:

    • HTTPメソッドを「GET」に設定します。
    • URLに http://localhost:8080/hello を入力します。
    • 「Send」ボタンをクリックします。
    • レスポンスボディに "Hello, Spring Boot REST API!" が表示されれば成功です。HTTPステータスコードが 200 OK であることも確認してください。

これで、最初のREST APIエンドポイントが完成しました!

6. パス変数とリクエストパラメータ

APIでは、リクエストURLの一部やクエリパラメータから情報を取得することがよくあります。Spring Bootでは、それぞれ @PathVariable@RequestParam アノテーションを使って簡単に実現できます。

6.1. パス変数 (@PathVariable)

URIの一部を可変な値として受け取りたい場合に使用します。例えば、ユーザーIDを指定してユーザー情報を取得する /users/{userId} のようなURIを考えます。

“`java
// … (imports)
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(“/users”) // クラスレベルでベースパスを設定
public class UserController {

// GET /users/{userId} のリクエストを処理
@GetMapping("/{userId}")
public String getUserById(@PathVariable Long userId) { // {userId} 部分の値を Long 型の userId 変数にバインド
    // 通常はここでデータベースなどから userId に対応するユーザー情報を取得する
    return "Fetching user with ID: " + userId;
}

// ... 他のユーザー関連のエンドポイント

}
“`

  • @RequestMapping("/users"): このコントローラー内の全てのエンドポイントのベースパスを /users に設定します。これにより、メソッドレベルの @GetMapping などでは相対パスを指定できます。
  • @GetMapping("/{userId}"): パスの一部 {userId} をプレースホルダーとして定義します。
  • @PathVariable Long userId: {userId} プレースホルダーに対応するパスの値を取得し、userId という名前の Long 型変数にバインドします。パス変数名と引数名が一致していれば、アノテーション引数は省略可能です (@PathVariable Long userId)。名前が異なる場合は @PathVariable("userId") Long id のように指定します。

テスト:

http://localhost:8080/users/123 にGETリクエストを送ると、Fetching user with ID: 123 というレスポンスが返るはずです。

6.2. リクエストパラメータ (@RequestParam)

クエリパラメータ(URLの ?key1=value1&key2=value2 の部分)から値を取得したい場合に使用します。例えば、検索キーワードを指定する /products/search?keyword=laptop&category=electronics のようなURIを考えます。

HelloController.java に以下のメソッドを追加してみましょう。

“`java
// … (imports)
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

// ... hello() メソッド

// GET /greeting?name=... のリクエストを処理
@GetMapping("/greeting")
public String greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
    // クエリパラメータ "name" の値を取得。指定がなければ "World" を使用。
    return "Hello, " + name + "!";
}

}
“`

  • @GetMapping("/greeting"): /greeting パスへのGETリクエストをマッピングします。
  • @RequestParam(value = "name", defaultValue = "World") String name: クエリパラメータ name の値を取得し、String 型の name 変数にバインドします。
    • value = "name": 取得したいクエリパラメータ名を指定します(引数名と同じ場合は省略可能)。
    • defaultValue = "World": クエリパラメータが指定されなかった場合のデフォルト値を設定します。
    • required = true (デフォルト): クエリパラメータが必須であることを指定します。必須でない場合は required = false を指定します。この場合、パラメータがなくてもエラーになりません。

テスト:

  • http://localhost:8080/greeting?name=Alice にGETリクエストを送ると、Hello, Alice! というレスポンスが返るはずです。
  • http://localhost:8080/greeting にGETリクエストを送ると(nameパラメータなし)、Hello, World! というレスポンスが返るはずです(defaultValue が使われるため)。

@PathVariable はリソースを識別するのに使い(例: /users/123)、@RequestParam はリソースに対する追加情報や絞り込み条件などに使う(例: /products/search?keyword=laptop)という使い分けが一般的です。

7. リクエストボディの処理

POSTやPUTリクエストでは、クライアントからサーバーにJSONやXMLなどのデータが送られることがよくあります。この「リクエストボディ」のデータを取得し、Javaオブジェクトにマッピングするには、@RequestBody アノテーションを使用します。

Spring Bootでは、Jacksonなどのライブラリが自動的に設定されており、JSON形式のリクエストボディをPOJO(Plain Old Java Object)に簡単にマッピングできます。

例として、新しいユーザーを作成するPOST APIを考えてみましょう。クライアントからはユーザー情報をJSON形式で送信してもらい、サーバー側ではそれを受け取って処理します。

まず、ユーザー情報を表すPOJOクラスを作成します。User.java という名前で作成します。

“`java
package com.example.demoapi.model; // model パッケージを作成してそこに配置するのが一般的

// JSON <-> Java オブジェクト変換のために Jackson ライブラリが必要ですが、
// spring-boot-starter-web に含まれています。

public class User {
private Long id;
private String name;
private String email;

// デフォルトコンストラクタ (Jackson がオブジェクト生成に必要)
public User() {
}

// コンストラクタ
public User(Long id, String name, String email) {
    this.id = id;
    this.name = name;
    this.email = email;
}

// ゲッターとセッター
public Long getId() {
    return id;
}

public void setId(Long id) {
    this.id = id;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public String getEmail() {
    return email;
}

public void setEmail(String email) {
    this.email = email;
}

@Override
public String toString() {
    return "User{" +
           "id=" + id +
           ", name='" + name + '\'' +
           ", email='" + email + '\'' +
           '}';
}

}
“`

次に、この User オブジェクトをリクエストボディとして受け取るコントローラーメソッドを作成します。UserController.java に以下のメソッドを追加します。

“`java
package com.example.demoapi;

import com.example.demoapi.model.User; // 作成したUserクラスをインポート
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(“/users”)
public class UserController {

// ... getUserById() メソッド

// POST /users のリクエストを処理
@PostMapping
public String createUser(@RequestBody User user) { // リクエストボディのJSONをUserオブジェクトにマッピング
    // 通常はここで受け取った user オブジェクトをデータベースに保存するなどの処理を行う
    System.out.println("Received user: " + user); // デモとしてログに出力
    return "User created: " + user.getName(); // 作成されたユーザー名を含むレスポンス
}

}
“`

  • @PostMapping: /users パスへのHTTP POSTメソッドのリクエストをマッピングします。
  • @RequestBody User user: HTTPリクエストボディの内容を User オブジェクトとして受け取ります。Spring Bootは、リクエストの Content-Type ヘッダー(例: application/json)を読み取り、適切なメッセージコンバーター(JSONの場合はJackson)を使って、リクエストボディのJSONデータを User クラスのインスタンスに自動的に変換してくれます。

テスト:

Postmanやcurlを使って、/users にPOSTリクエストを送ってみましょう。

  • Postmanの場合:

    • HTTPメソッドを「POST」に設定します。
    • URLに http://localhost:8080/users を入力します。
    • 「Body」タブを選択し、「raw」を選びます。
    • ドロップダウンリストで「JSON」を選択します。
    • テキストエリアに以下のJSONデータを入力します。
      json
      {
      "name": "Alice",
      "email": "[email protected]"
      }
    • 「Send」ボタンをクリックします。
    • レスポンスボディに "User created: Alice" が表示されれば成功です。アプリケーションのログにも Received user: User{id=null, name='Alice', email='[email protected]'} のような出力があるはずです(id は今回はクライアントから送っていないのでnullです)。
  • curlの場合:
    bash
    curl -X POST -H "Content-Type: application/json" -d '{"name": "Bob", "email": "[email protected]"}' http://localhost:8080/users

    ターミナルに "User created: Bob" と表示されれば成功です。

@RequestBody は、クライアントから構造化されたデータを受け取る際に非常に便利です。通常、受け取ったデータはデータベースに保存したり、他のサービスと連携させたりといった処理を行います。

8. レスポンスの返却

APIはクライアントからのリクエストを処理した後、結果をHTTPレスポンスとして返します。レスポンスには、ステータスコード、ヘッダー、ボディなどが含まれます。

Spring Bootの @RestController では、メソッドの戻り値が自動的にHTTPレスポンスボディに変換されます。

  • 文字列: 単純な文字列が返されます(Content-Type: text/plain)。
  • POJO: JSON形式に変換されて返されます(Content-Type: application/json)。これは @RequestBody と同様、Jacksonによって行われます。
  • コレクション (List, Mapなど): JSON形式の配列やオブジェクトに変換されて返されます。

8.1. POJOをJSONとして返す

ユーザー作成APIの例を少し修正して、作成したユーザー情報をクライアントに返すようにしてみましょう。通常、データベースに保存された後のIDが付与されたユーザー情報を返すのが一般的です。

UserController.javacreateUser メソッドを以下のように変更します。戻り値の型を String から User に変更します。

“`java
package com.example.demoapi;

import com.example.demoapi.model.User;
import org.springframework.web.bind.annotation.*;

import java.util.concurrent.atomic.AtomicLong; // ダミーID生成用

@RestController
@RequestMapping(“/users”)
public class UserController {

private final AtomicLong counter = new AtomicLong(); // ダミーのIDジェネレーター

// GET /users/{userId}
@GetMapping("/{userId}")
public User getUserById(@PathVariable Long userId) {
    // TODO: 実際はデータベースから取得
    // ダミーデータとして、IDと対応するユーザーを返す
    return new User(userId, "User" + userId, "user" + userId + "@example.com");
}

// POST /users
@PostMapping
// 作成されたユーザー情報を返すように戻り値の型を User に変更
public User createUser(@RequestBody User user) {
    // TODO: 実際はデータベースに保存し、自動生成されたIDを設定

    // ダミーとして、受け取ったユーザー情報に新しいIDをセットして返す
    Long newId = counter.incrementAndGet(); // 新しいIDを生成
    user.setId(newId); // IDをセット

    System.out.println("Created dummy user: " + user);
    // UserオブジェクトがJSONに変換されてレスポンスボディとして返される
    return user;
}

}
“`

テスト:

先ほどと同じJSONデータで http://localhost:8080/users にPOSTリクエストを送ります。

json
{
"name": "Alice",
"email": "[email protected]"
}

レスポンスボディには、IDが付与されたユーザー情報のJSONが返されるはずです。

json
{
"id": 1, // またはカウンターに応じた新しいID
"name": "Alice",
"email": "[email protected]"
}

HTTPステータスコードはデフォルトで 200 OK となります。ユーザー作成の場合は、リソースが新規作成されたことを示す 201 Created を返すのがRESTfulな慣習です。これは次に説明する ResponseEntity を使うことで実現できます。

8.2. ResponseEntity を使った詳細な制御

ResponseEntity クラスを使うと、HTTPステータスコード、ヘッダー、レスポンスボディをより細かく制御できます。

UserController.javacreateUser メソッドを ResponseEntity<User> を返すように変更します。

“`java
package com.example.demoapi;

import com.example.demoapi.model.User;
import org.springframework.http.HttpStatus; // ステータスコード用
import org.springframework.http.ResponseEntity; // ResponseEntity用
import org.springframework.web.bind.annotation.*;

import java.net.URI; // Locationヘッダー用
import java.util.concurrent.atomic.AtomicLong;

@RestController
@RequestMapping(“/users”)
public class UserController {

private final AtomicLong counter = new AtomicLong();

// GET /users/{userId}
@GetMapping("/{userId}")
// ResponseEntity を使ってステータスコードやヘッダーを制御することも可能
public ResponseEntity<User> getUserById(@PathVariable Long userId) {
    // TODO: 実際はデータベースから取得
    User foundUser = new User(userId, "User" + userId, "user" + userId + "@example.com");

    // ユーザーが見つからなかった場合の例
    // if (foundUser == null) {
    //     return ResponseEntity.notFound().build(); // 404 Not Found
    // }

    // 成功時は 200 OK と共にユーザー情報を返す
    return ResponseEntity.ok(foundUser); // ok() は HttpStauts.OK (200) を設定するショートカット
}


// POST /users
@PostMapping
// ResponseEntity を返すように変更
public ResponseEntity<User> createUser(@RequestBody User user) {
    // TODO: 実際はデータベースに保存し、自動生成されたIDを設定

    Long newId = counter.incrementAndGet(); // 新しいIDを生成
    user.setId(newId); // IDをセット

    System.out.println("Created dummy user: " + user);

    // ユーザー作成成功時は 201 Created を返すのが一般的
    // created() は HttpStauts.CREATED (201) を設定するショートカット
    // Location ヘッダーに新規作成されたリソースのURIを含めるのがRESTfulな慣習
    URI location = URI.create("/users/" + newId); // 新規リソースのURIを作成

    return ResponseEntity.created(location) // 201 Created と Location ヘッダーを設定
                         .body(user);      // レスポンスボディに作成したユーザー情報を設定
}

// DELETE /users/{userId} の例(後ほどデータベース連携セクションで実装)
@DeleteMapping("/{userId}")
public ResponseEntity<Void> deleteUser(@PathVariable Long userId) {
    // TODO: 実際はデータベースから userId に対応するユーザーを削除

    // 削除成功時は 204 No Content を返すのが一般的
    return ResponseEntity.noContent().build(); // 204 No Content
}

}
“`

  • ResponseEntity.ok(foundUser): HTTPステータスコードを 200 OK に設定し、foundUser オブジェクトをレスポンスボディとして返します。
  • ResponseEntity.created(location).body(user): HTTPステータスコードを 201 Created に設定し、Location ヘッダーに指定されたURI(新規作成されたリソースのURI)を含め、user オブジェクトをレスポンスボディとして返します。
  • ResponseEntity.notFound().build(): HTTPステータスコードを 404 Not Found に設定し、ボディは空で返します。build() はボディがない場合に利用します。
  • ResponseEntity.noContent().build(): HTTPステータスコードを 204 No Content に設定し、ボディは空で返します。削除成功時など、レスポンスボディを返さない場合に利用します。

ResponseEntity を使うことで、APIの応答をより正確に表現し、クライアントに適切な情報(ステータスコード、リソースの場所など)を伝えることができます。

9. データの永続化(データベース連携)

ほとんどのREST APIは、永続的なデータを扱います。Spring Bootでは、Spring Data JPAを使うことで、リレーショナルデータベースとの連携を非常に簡単に実現できます。

JPA (Jakarta Persistence API) はJavaのORM (Object-Relational Mapping) 標準仕様です。Spring Data JPAは、JPAの上に構築されており、リポジトリパターンを使ったデータアクセス層の実装を簡素化します。

ここでは、簡単なインメモリデータベースであるH2 Databaseを使って、ユーザー情報を保存・取得・更新・削除するAPIを実装してみましょう。

9.1. 依存関係の追加

pom.xml に以下の依存関係を追加します。

“`xml


org.springframework.boot
spring-boot-starter-data-jpa


com.h2database
h2
runtime

“`

  • spring-boot-starter-data-jpa: Spring Data JPAと、デフォルトのJPA実装であるHibernateが含まれます。
  • h2: インメモリデータベースであるH2 Databaseのドライバーです。scope>runtime</scope> は、実行時のみ必要であることを示します。

依存関係を追加したら、Mavenプロジェクトをリロードします(IDEで自動的に行われることもあります)。

9.2. エンティティの作成

データベースのテーブルに対応するJavaクラスを作成します。これを「エンティティ(Entity)」と呼びます。model パッケージに User.java が既にあるので、これをJPAエンティティとして設定します。

“`java
package com.example.demoapi.model;

import jakarta.persistence.Entity; // JPA Entity アノテーション
import jakarta.persistence.GeneratedValue; // ID自動生成用
import jakarta.persistence.GenerationType; // ID自動生成戦略用
import jakarta.persistence.Id; // 主キー用

@Entity // このクラスがJPAのエンティティであることを示す
public class User {
@Id // 主キーであることを示す
@GeneratedValue(strategy = GenerationType.IDENTITY) // IDがデータベースによって自動生成されることを示す
private Long id; // 主キーとなるフィールド

private String name;
private String email;

// デフォルトコンストラクタ (JPA がオブジェクト生成に必要)
public User() {
}

// コンストラクタ (IDなし - 新規作成用)
public User(String name, String email) {
    this.name = name;
    this.email = email;
}

// コンストラクタ (IDあり - 更新や取得結果用)
public User(Long id, String name, String email) {
    this.id = id;
    this.name = name;
    this.email = email;
}

// ゲッターとセッター
// ... (省略 - 前のコードと同じ)

@Override
public String toString() {
    return "User{" +
           "id=" + id +
           ", name='" + name + '\'' +
           ", email='" + email + '\'' +
           '}';
}

}
“`

  • @Entity: このクラスがデータベースのテーブルに対応するエンティティであることを示します。デフォルトではクラス名がテーブル名になります(User クラスなら user テーブル)。@Table(name = "users") のように明示的にテーブル名を指定することもできます。
  • @Id: このフィールドが主キーであることを示します。
  • @GeneratedValue(strategy = GenerationType.IDENTITY): 主キーの値がデータベースによって自動的に生成されることを示します。GenerationType.IDENTITY は、データベースのAUTO_INCREMENT機能を利用します。

9.3. リポジトリの作成

データアクセス層の実装は「リポジトリ(Repository)」インターフェースを定義することで行います。Spring Data JPAは、このインターフェースを基に、基本的なCRUD操作(Create, Read, Update, Delete)を自動的に提供してくれます。

repository パッケージを作成し、UserRepository.java インターフェースを作成します。

“`java
package com.example.demoapi.repository;

import com.example.demoapi.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; // 明示的に@Repositoryを付けることも可能

// JpaRepository<エンティティの型, 主キーの型> を継承する
// Spring Data JPA がこのインターフェースの実装を自動的に提供する
public interface UserRepository extends JpaRepository {

// JpaRepository を継承するだけで、以下のメソッドが利用可能になる:
// save(entity): エンティティの保存 (新規作成または更新)
// findById(id): IDを指定してエンティティを取得 (Optional<Entity> を返す)
// findAll(): 全てのエンティティを取得 (List<Entity> を返す)
// deleteById(id): IDを指定してエンティティを削除
// delete(entity): エンティティを指定して削除
// existsById(id): IDが存在するか確認

// さらに、特定の命名規則に従ったメソッドを定義することで、
// Spring Data JPA が自動的にクエリを生成してくれる(例: findByName(String name))
// これは入門編としては省略

}
“`

  • JpaRepository<User, Long>: JpaRepository を継承することで、基本的なCRUDメソッドが自動的に提供されます。型パラメータとして、対象となるエンティティのクラス (User) と、その主キーの型 (Long) を指定します。

@Repository アノテーションを付けることも推奨されますが、JpaRepository を継承している場合は省略可能です。Springによってコンポーネントスキャンされ、Beanとして管理されるようになります。

9.4. サービスの導入(ビジネスロジックの分離)

API開発では、コントローラーが直接リポジトリを呼び出すのではなく、間に「サービス(Service)」層を挟むのが一般的な設計パターンです。

なぜサービス層が必要か?

  • 関心事の分離 (Separation of Concerns):
    • コントローラーはリクエストの受付、パラメータの解析、レスポンスの返却など、HTTPに関することに責任を持ちます。
    • サービスは、複数のリポジトリを使った複雑なデータ操作、ビジネスルールの適用、バリデーション、他のサービス連携など、アプリケーションの中核となるビジネスロジックに責任を持ちます。
    • リポジトリは、データベースへのCRUD操作という低レベルなデータアクセスに責任を持ちます。
      このように役割を明確に分けることで、コードの可読性、保守性、再利用性が向上します。
  • テスト容易性: ビジネスロジックがサービス層に集中しているため、HTTPやデータベースに依存しない単体テストが容易になります。
  • トランザクション管理: トランザクション境界をサービス層で設定するのが一般的です。

service パッケージを作成し、UserService.java クラスを作成します。

“`java
package com.example.demoapi.service;

import com.example.demoapi.model.User;
import com.example.demoapi.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired; // 依存性注入用
import org.springframework.stereotype.Service; // サービスコンポーネントを示す
import org.springframework.transaction.annotation.Transactional; // トランザクション管理用

import java.util.List;
import java.util.Optional; // 検索結果が存在しない場合を扱う

@Service // このクラスがサービスクラスであることを示す
public class UserService {

private final UserRepository userRepository; // UserRepository への参照

// コンストラクタインジェクションによる依存性注入
@Autowired // Spring が UserRepository のインスタンスを注入してくれる
public UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
}

// 全ユーザーを取得
public List<User> getAllUsers() {
    return userRepository.findAll();
}

// IDでユーザーを取得
public Optional<User> getUserById(Long id) {
    return userRepository.findById(id);
}

// 新規ユーザーを作成または既存ユーザーを更新
@Transactional // このメソッド全体をトランザクションとして実行する
public User saveUser(User user) {
    return userRepository.save(user); // save() は新規作成または更新を行う
}

// IDでユーザーを削除
@Transactional
public void deleteUser(Long id) {
    userRepository.deleteById(id);
}

// IDが存在するか確認
public boolean existsById(Long id) {
    return userRepository.existsById(id);
}

}
“`

  • @Service: このクラスがビジネスロジックを担うサービスコンポーネントであることを示します。Springによって管理されるBeanとなります。
  • @Autowired: コンストラクタに付与することで、Springコンテナが UserRepository のインスタンスを生成し、このコンストラクタに注入してくれます(依存性注入 – Dependency Injection)。フィールドインジェクション (@Autowired private UserRepository userRepository;) も可能ですが、コンストラクタインジェクションがテスト容易性や依存関係の明確化の点で推奨されます。
  • @Transactional: このアノテーションが付与されたメソッドが実行される際に、Springが自動的にデータベーストランザクションを開始・コミット・ロールバックしてくれます。データ変更を伴うメソッド(saveUser, deleteUser など)に付与するのが一般的です。

9.5. コントローラーからのサービス呼び出し

最後に、UserController を修正して、直接リポジトリを使うのではなく、作成した UserService を呼び出すようにします。

“`java
package com.example.demoapi;

import com.example.demoapi.model.User;
import com.example.demoapi.service.UserService; // UserService をインポート
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException; // 例外処理で使う(後述)

import java.net.URI;
import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping(“/users”)
public class UserController {

private final UserService userService; // UserService への参照

// コンストラクタインジェクションで UserService を注入
@Autowired
public UserController(UserService userService) {
    this.userService = userService;
}

// GET /users - 全ユーザー取得
@GetMapping
public List<User> getAllUsers() {
    return userService.getAllUsers(); // サービス層のメソッドを呼び出し
}

// GET /users/{userId} - IDでユーザー取得
@GetMapping("/{userId}")
public ResponseEntity<User> getUserById(@PathVariable Long userId) {
    Optional<User> user = userService.getUserById(userId); // サービス層のメソッドを呼び出し

    // ユーザーが見つかった場合は 200 OK と共にユーザー情報を返す
    // 見つからなかった場合は 404 Not Found を返す
    return user.map(ResponseEntity::ok) // Optional が値を持つ場合は map で処理
               .orElseGet(() -> ResponseEntity.notFound().build()); // 値を持たない場合は 404 を返す
    // または、見つからなかった場合にカスタム例外を投げる方法もある(後述の例外処理を参照)
}

// POST /users - 新規ユーザー作成
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
    // IDはデータベース側で生成されるため、クライアントからのIDは無視する
    user.setId(null);
    User createdUser = userService.saveUser(user); // サービス層のメソッドを呼び出し

    // 新規作成されたリソースのURIを作成し、Locationヘッダーに含める
    URI location = URI.create("/users/" + createdUser.getId());

    // 201 Created と Location ヘッダー、作成されたユーザー情報を返す
    return ResponseEntity.created(location).body(createdUser);
}

// PUT /users/{userId} - ユーザー更新
@PutMapping("/{userId}")
public ResponseEntity<User> updateUser(@PathVariable Long userId, @RequestBody User userDetails) {
    // 更新対象のユーザーが存在するか確認
    if (!userService.existsById(userId)) {
        return ResponseEntity.notFound().build(); // 存在しない場合は 404 Not Found
    }

    // 更新対象のIDをセット(クライアントから送られてきたボディのIDは無視)
    userDetails.setId(userId);
    User updatedUser = userService.saveUser(userDetails); // サービス層のメソッドを呼び出し(IDが存在すれば更新になる)

    // 更新成功時は 200 OK と共に更新されたユーザー情報を返す
    return ResponseEntity.ok(updatedUser);
}

// DELETE /users/{userId} - ユーザー削除
@DeleteMapping("/{userId}")
public ResponseEntity<Void> deleteUser(@PathVariable Long userId) {
    // 削除対象のユーザーが存在するか確認
    if (!userService.existsById(userId)) {
        return ResponseEntity.notFound().build(); // 存在しない場合は 404 Not Found
    }

    userService.deleteUser(userId); // サービス層のメソッドを呼び出し

    // 削除成功時は 204 No Content を返す
    return ResponseEntity.noContent().build();
}

}
“`

これで、基本的なユーザー情報のCRUD操作を行うREST APIが完成しました。アプリケーションを再起動し、APIクライアントから各エンドポイントをテストしてみてください。

H2 Databaseはインメモリで動作するため、アプリケーションを停止するとデータは消えます。開発中は便利ですが、永続化が必要な場合はMySQL, PostgreSQLなどの外部データベースに接続するように application.properties を設定する必要があります。

9.6. H2 Database コンソールの利用 (Optional)

H2 Databaseを使用している場合、開発中は組み込みのWebコンソールを利用してデータベースの内容を確認できます。

application.properties (または application.yml) に以下を追加します。

“`properties

application.properties

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.datasource.url=jdbc:h2:mem:testdb # In-memory database name (default is testdb)
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

JPAが起動時にテーブルを自動生成する設定 (開発・検証用)

spring.jpa.hibernate.ddl-auto=update # または create, create-drop, none
“`

設定を追加してアプリケーションを再起動すると、http://localhost:8080/h2-console にアクセスできるようになります。JDBC URLには jdbc:h2:mem:testdbapplication.propertiesで設定した名前)、ユーザー名 sa (デフォルト)、パスワードなしで接続できます。接続後、user テーブルの内容を確認したり、SQLクエリを実行したりできます。

spring.jpa.hibernate.ddl-auto は、アプリケーション起動時にデータベーススキーマをどう扱うかを指定します。
* create: 起動時に既存のテーブルを削除し、再作成します。データは失われます。
* create-drop: 起動時に作成し、アプリケーション終了時に削除します。
* update: エンティティクラスの変更を検知し、テーブルスキーマを更新します。データの保持を試みますが、複雑な変更には対応できないことがあります。
* none: スキーマの自動変更を行いません。本番環境では通常 none を使い、スキーマ変更はマイグレーションツールで行います。

開発中は updatecreate-drop が便利です。

10. 例外処理

API開発において、例外処理は非常に重要です。予期せぬエラーが発生した場合や、クライアントのリクエストが無効な場合に、APIは適切なHTTPステータスコードとエラー情報を返す必要があります。

Spring Bootでは、さまざまな方法で例外を処理できます。

10.1. 特定のコントローラー内の例外処理 (@ExceptionHandler)

特定のコントローラー内で発生した特定の例外を捕捉して処理するには、メソッドに @ExceptionHandler アノテーションを付与します。

UserController.java に、ユーザーが見つからなかった場合に UserNotFoundException というカスタム例外を投げるように修正し、それを捕捉するメソッドを追加してみましょう。

まず、カスタム例外クラスを作成します。exception パッケージを作成し、UserNotFoundException.java を作成します。

“`java
package com.example.demoapi.exception;

// RuntimeException を継承するのが一般的
public class UserNotFoundException extends RuntimeException {

public UserNotFoundException(Long id) {
    super("Could not find user with id: " + id);
}

}
“`

次に、UserController.java を修正します。

“`java
package com.example.demoapi;

import com.example.demoapi.exception.UserNotFoundException; // 作成した例外をインポート
import com.example.demoapi.model.User;
import com.example.demoapi.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.net.URI;
import java.util.List;

@RestController
@RequestMapping(“/users”)
public class UserController {

private final UserService userService;

@Autowired
public UserController(UserService userService) {
    this.userService = userService;
}

// ... getAllUsers(), createUser(), updateUser() メソッドは省略

// GET /users/{userId} - IDでユーザー取得 (例外処理を適用)
@GetMapping("/{userId}")
public User getUserById(@PathVariable Long userId) {
    // サービス層から Optional<User> を取得し、存在しない場合はカスタム例外を投げる
    return userService.getUserById(userId)
                      .orElseThrow(() -> new UserNotFoundException(userId));
}

// DELETE /users/{userId} - ユーザー削除 (例外処理を適用)
@DeleteMapping("/{userId}")
public ResponseEntity<Void> deleteUser(@PathVariable Long userId) {
    // 削除対象のユーザーが存在するか確認し、存在しない場合はカスタム例外を投げる
    if (!userService.existsById(userId)) {
        throw new UserNotFoundException(userId);
    }

    userService.deleteUser(userId);

    return ResponseEntity.noContent().build();
}

// UserController 内で発生した UserNotFoundException を捕捉するハンドラーメソッド
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND) // HTTPステータスコードを 404 Not Found に設定
public String handleUserNotFoundException(UserNotFoundException ex) {
    // 例外メッセージをレスポンスボディとして返す
    return ex.getMessage();
}

}
“`

  • getUserById および deleteUser メソッドで、ユーザーが見つからない場合に UserNotFoundException を投げるように変更しました。Optional.orElseThrow() は、Optional が空の場合に指定した例外を投げます。
  • @ExceptionHandler(UserNotFoundException.class): このメソッドが UserNotFoundException がスローされたときに呼び出される例外ハンドラーであることを示します。
  • @ResponseStatus(HttpStatus.NOT_FOUND): 例外が発生した場合のHTTPステータスコードを 404 Not Found に設定します。
  • ハンドラーメソッドの戻り値 (String) がレスポンスボディとして返されます。

テスト:

存在しないユーザーID (/users/999) に対してGETまたはDELETEリクエストを送ってみてください。HTTPステータスコードが 404 Not Found となり、レスポンスボディに Could not find user with id: 999 のようなメッセージが返されるはずです。

10.2. グローバルな例外処理 (@ControllerAdvice)

@ExceptionHandler は特定のコントローラー内でのみ有効ですが、複数のコントローラーで共通の例外処理を行いたい場合があります。例えば、アプリケーション全体で UserNotFoundExceptionAccessDeniedException などを一元的に処理したい場合です。

このような場合は、@ControllerAdvice アノテーションが付与されたクラスを作成し、その中に @ExceptionHandler メソッドを定義します。これにより、アプリケーション内のどのコントローラーからスローされた例外でも捕捉できるようになります。

exception パッケージに GlobalExceptionHandler.java クラスを作成します。

“`java
package com.example.demoapi.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice; // グローバル例外ハンドリング用
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest; // リクエスト情報用
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; // Spring のデフォルトハンドラーを拡張する場合

// @ControllerAdvice を付けることで、アプリケーション全体で有効な例外ハンドラーとなる
// ResponseEntityExceptionHandler を継承すると、Spring MVC のデフォルト例外ハンドリングをカスタマイズできる
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

// UserController の @ExceptionHandler は削除し、こちらに移動 (またはそのまま残しても良いが、グローバルが優先される場合がある)
@ExceptionHandler(UserNotFoundException.class)
// ResponseEntity を使って、ステータスコードだけでなくカスタムエラーボディも返す
public ResponseEntity<Object> handleUserNotFoundException(UserNotFoundException ex, WebRequest request) {
    // 例外発生時のレスポンスボディとして返すエラー情報を保持するクラスを定義する
    // ここでは簡単のため Map または カスタムクラス (ErrorDetails など) を使う
    // 例: Map<String, Object> body = new LinkedHashMap<>(); body.put("message", ex.getMessage()); ...
    // より良いエラーレスポンス形式については後述のDTOを参照

    // 簡単な文字列で返す場合:
    // return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);

    // JSONでエラー情報を返す場合(ErrorDetailsクラスを別途定義するのが一般的)
    // 例:
    ErrorDetails errorDetails = new ErrorDetails(HttpStatus.NOT_FOUND.value(), ex.getMessage(), request.getDescription(false));
    return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);

}

// その他の一般的な例外 (例: NullPointerException, IllegalArgumentException など) を捕捉するハンドラー
// 特定の例外を指定しない @ExceptionHandler(Exception.class) も定義可能だが、慎重に
// RuntimeException を捕捉する例
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<Object> handleRuntimeException(RuntimeException ex, WebRequest request) {
     ErrorDetails errorDetails = new ErrorDetails(HttpStatus.INTERNAL_SERVER_ERROR.value(), "An unexpected error occurred", request.getDescription(false));
     ex.printStackTrace(); // 開発中はスタックトレースを出力しておくと便利
     return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR); // 500 Internal Server Error
}

// エラー情報のレスポンスボディ形式を定義するクラス(内部クラスまたは別途ファイルで)
private static class ErrorDetails {
    private int status;
    private String message;
    private String details; // 例: リクエストURIなど

    public ErrorDetails(int status, String message, String details) {
        this.status = status;
        this.message = message;
        this.details = details;
    }

    // ゲッター (JSON変換に必要)
    public int getStatus() { return status; }
    public String getMessage() { return message; }
    public String getDetails() { return details; }
}

}
“`

  • @ControllerAdvice: このクラスが、複数のコントローラーにまたがる共通処理(例外ハンドリング、データバインディングなど)を提供することを示します。
  • ハンドラーメソッドの引数に WebRequest request を加えることで、リクエストに関する詳細情報(リクエストURIなど)を取得できます。
  • ResponseEntity を返すと、ステータスコードだけでなく、ボディの内容も柔軟に設定できます。エラー発生時のレスポンスボディの形式は、クライアントにとって分かりやすいように統一するのが良い設計です。上記の例では簡単な内部クラス ErrorDetails を使っていますが、実際にはより詳細な情報(タイムスタンプ、エラーコードなど)を含むクラスを定義することが多いです。

@ControllerAdvice@ExceptionHandler を組み合わせることで、アプリケーション全体の例外処理を体系的に管理できます。

11. 入力値の検証(バリデーション)

APIが受け取るクライアントからの入力データは、必ず検証が必要です。無効なデータがシステムに渡されると、データの不整合やセキュリティ上の問題を引き起こす可能性があります。

Javaでは、Bean Validation (Jakarta Bean Validation) という標準仕様があり、アノテーションを使ってオブジェクトのプロパティに対する制約(nullでない、最小/最大サイズ、パターンマッチングなど)を定義できます。Spring Bootは、この仕様をサポートしており、REST APIのリクエストボディやメソッド引数の検証を簡単に行えます。

11.1. 依存関係の追加

Bean Validationの機能を利用するために、以下の依存関係を pom.xml に追加します。

“`xml


org.springframework.boot
spring-boot-starter-validation

“`

11.2. POJO (DTO) への制約アノテーション付与

検証ルールを、対象となるクラス(通常はリクエストボディとして受け取るDTOやエンティティ)のフィールドにアノテーションとして定義します。

User.java (または後述のDTOクラス) にバリデーション制約を追加してみましょう。

“`java
package com.example.demoapi.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

// バリデーションアノテーションのインポート
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank; // Null, 空文字列, スペースのみ を許可しない
import jakarta.validation.constraints.Size; // 文字列のサイズ制約

@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotBlank(message = "Name is mandatory") // name は必須入力
@Size(max = 50, message = "Name cannot exceed 50 characters") // name は最大50文字
private String name;

@NotBlank(message = "Email is mandatory") // email は必須入力
@Email(message = "Email should be valid") // email は有効なメールアドレス形式
private String email;

// ... コンストラクタ、ゲッター、セッターは省略

}
“`

  • @NotBlank: 文字列がnullではなく、かつ空白文字以外の文字を一つ以上含むことを要求します。
  • @Size(max = 50): 文字列の最大長を指定します。
  • @Email: 文字列が有効なメールアドレス形式であることを要求します。
  • message = "...": 検証失敗時のエラーメッセージを指定できます。

他にも @NotNull, @Min, @Max, @Pattern, @Future, @Past など、様々な制約アノテーションがあります。

11.3. コントローラーでの検証有効化 (@Valid)

コントローラーメソッドの引数(リクエストボディとして受け取るオブジェクトなど)に対して検証を実行するには、その引数に @Valid アノテーションを付与します。

UserController.javacreateUser メソッドに @Valid を追加します。

“`java
package com.example.demoapi;

import com.example.demoapi.model.User;
import com.example.demoapi.service.UserService;
import jakarta.validation.Valid; // @Valid アノテーションのインポート
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException; // 検証エラー時の例外
import org.springframework.web.bind.annotation.*;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

@RestController
@RequestMapping(“/users”)
public class UserController {

private final UserService userService;

@Autowired
public UserController(UserService userService) {
    this.userService = userService;
}

// ... 他のメソッド

// POST /users - 新規ユーザー作成 (バリデーション有効化)
@PostMapping
// @Valid を付けることで、受信した User オブジェクトに対するバリデーションが実行される
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
    user.setId(null);
    User createdUser = userService.saveUser(user);

    URI location = URI.create("/users/" + createdUser.getId());
    return ResponseEntity.created(location).body(createdUser);
}

// PUT /users/{userId} - ユーザー更新 (バリデーション有効化)
@PutMapping("/{userId}")
// @Valid を付けることで、受信した userDetails オブジェクトに対するバリデーションが実行される
public ResponseEntity<User> updateUser(@PathVariable Long userId, @Valid @RequestBody User userDetails) {
    if (!userService.existsById(userId)) {
        throw new UserNotFoundException(userId); // 存在しない場合はカスタム例外
    }

    userDetails.setId(userId);
    User updatedUser = userService.saveUser(userDetails);

    return ResponseEntity.ok(updatedUser);
}

// バリデーションエラー (MethodArgumentNotValidException) を捕捉するハンドラー
// GlobalExceptionHandler に移動しても良い
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) // HTTPステータスコードを 400 Bad Request に設定
public Map<String, String> handleValidationExceptions(MethodArgumentNotValidException ex) {
    // フィールド名とエラーメッセージのマップを作成して返す
    Map<String, String> errors = new HashMap<>();
    ex.getBindingResult().getFieldErrors().forEach(error ->
        errors.put(error.getField(), error.getDefaultMessage()));
    return errors;
}

// ... GlobalExceptionHandler の @ExceptionHandler(UserNotFoundException.class) など

}
“`

  • @Valid @RequestBody User user: リクエストボディのJSONを User オブジェクトに変換した後、そのオブジェクトに対して User クラスに定義されたバリデーション制約を適用します。
  • 検証に失敗した場合、Springは MethodArgumentNotValidException をスローします。この例外を捕捉するハンドラーメソッドを定義することで、クライアントにエラーの詳細を返すことができます。上記の例では、エラーが発生したフィールド名とエラーメッセージのマップをJSONで返しています。これを @ControllerAdvice に移動してグローバルに処理することも多いです。

テスト:

http://localhost:8080/users にPOSTリクエストを送信し、以下のいずれかの無効なJSONをボディとして送ってみてください。

  • name フィールドがない、または空文字列
  • email フィールドがない、または無効な形式(例: invalid-email
  • name フィールドが50文字を超える

json
{
"name": "",
"email": "invalid-email"
}

レスポンスとして、HTTPステータスコードが 400 Bad Request となり、レスポンスボディに以下のようなJSON形式のエラー情報が返されるはずです。

json
{
"name": "Name is mandatory",
"email": "Email should be valid"
}

入力値検証は、APIの信頼性と堅牢性を高めるために不可欠なステップです。

12. DTO (Data Transfer Object) の活用

これまでの例では、データベースエンティティである User クラスを、リクエストボディの受け取りやレスポンスボディの返却にも利用しました。しかし、これは大規模なアプリケーションや、データベーススキーマとAPIの要求が異なる場合に問題となることがあります。

エンティティをAPIで直接使用する問題点:

  • 情報漏洩のリスク: エンティティクラスには、APIクライアントに見せる必要のない情報(例: パスワードハッシュ、内部的な管理フィールド)が含まれている可能性があります。そのまま返すと、これらの情報が漏洩するリスクがあります。
  • 結合度の増加: APIの仕様がエンティティの構造に強く依存してしまい、データベーススキーマを変更しにくくなります。また、APIの仕様だけを変更したい場合でも、エンティティを変更する必要が生じる可能性があります。
  • 柔軟性の欠如: APIで返したい情報がエンティティの一部だけだったり、複数のエンティティから情報を組み合わせて返したい場合に、エンティティをそのまま使うと不便です。
  • バリデーション: エンティティにバリデーションアノテーションを直接付けると、データベース制約とAPI入力制約が混在して分かりにくくなることがあります。

これらの問題を解決するために、「DTO (Data Transfer Object)」を使用します。DTOは、APIの入出力のために特化された、シンプルなPOJOクラスです。

  • リクエスト用DTO: クライアントから受け取るリクエストボディの構造を定義します。バリデーションアノテーションは通常こちらに付与します。
  • レスポンス用DTO: クライアントに返すレスポンスボディの構造を定義します。公開しても安全な情報のみを含めます。

例として、ユーザー作成・更新用のリクエストDTO (UserRequest.java) と、ユーザー情報返却用のレスポンスDTO (UserResponse.java) を作成してみましょう。

dto パッケージを作成し、以下のクラスを作成します。

“`java
// UserRequest.java
package com.example.demoapi.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

// クライアントからのユーザー作成・更新リクエストボディ用DTO
public class UserRequest {

@NotBlank(message = "Name is mandatory")
@Size(max = 50, message = "Name cannot exceed 50 characters")
private String name;

@NotBlank(message = "Email is mandatory")
@Email(message = "Email should be valid")
private String email;

// ゲッターとセッター (JacksonがJSON<->Java変換に必要)
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }

// デフォルトコンストラクタ (Jacksonがオブジェクト生成に必要)
public UserRequest() {}

// コンストラクタ(任意)
public UserRequest(String name, String email) {
    this.name = name;
    this.email = email;
}

}
“`

“`java
// UserResponse.java
package com.example.demoapi.dto;

// クライアントへのユーザー情報返却用DTO
// ここではエンティティと同じ構造だが、不要なフィールドは含めないのが原則
public class UserResponse {
private Long id;
private String name;
private String email;

// ゲッターとセッター
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }

// デフォルトコンストラクタ
public UserResponse() {}

// コンストラクタ(エンティティから変換する際に便利)
public UserResponse(Long id, String name, String email) {
    this.id = id;
    this.name = name;
    this.email = email;
}

}
“`

次に、UserControllerUserService を修正して、これらのDTOを使うようにします。エンティティとDTOの間でデータをコピーする「マッピング」処理が必要になります。

12.1. エンティティとDTO間のマッピング

マッピングを手動で行うこともできますが、複雑になると煩雑です。ModelMapperやMapStructといったライブラリを使うと、マッピング処理を自動化または簡素化できます。ここでは手動マッピングの例を示しますが、実務ではライブラリの利用も検討しましょう。

UserService.java に、エンティティとDTOを相互に変換するメソッドを追加します。

“`java
package com.example.demoapi.service;

import com.example.demoapi.dto.UserRequest; // リクエストDTOをインポート
import com.example.demoapi.dto.UserResponse; // レスポンスDTOをインポート
import com.example.demoapi.model.User;
import com.example.demoapi.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors; // ストリーム処理でDTOリストに変換用

@Service
public class UserService {

private final UserRepository userRepository;

@Autowired
public UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
}

// Entity -> ResponseDTO への変換メソッド
private UserResponse convertToDto(User user) {
    UserResponse userResponse = new UserResponse();
    userResponse.setId(user.getId());
    userResponse.setName(user.getName());
    userResponse.setEmail(user.getEmail());
    return userResponse;
}

// RequestDTO -> Entity への変換メソッド
private User convertToEntity(UserRequest userRequest) {
    User user = new User();
    user.setName(userRequest.getName());
    user.setEmail(userRequest.getEmail());
    // IDはリクエストDTOには含まれない、または無視する
    return user;
}

// 全ユーザーを取得 (ResponseDTOのリストを返す)
public List<UserResponse> getAllUsers() {
    // エンティティのリストを取得し、ストリームAPIを使ってDTOのリストに変換
    return userRepository.findAll().stream()
                         .map(this::convertToDto) // 各UserエンティティをUserResponse DTOに変換
                         .collect(Collectors.toList()); // リストに収集
}

// IDでユーザーを取得 (Optional<ResponseDTO> を返す)
public Optional<UserResponse> getUserById(Long id) {
    return userRepository.findById(id) // エンティティを取得
                         .map(this::convertToDto); // 存在すればDTOに変換
}

// 新規ユーザーを作成または既存ユーザーを更新 (RequestDTOを受け取り、ResponseDTOを返す)
@Transactional
public UserResponse saveUser(UserRequest userRequest, Long id) { // 更新の場合はidも受け取る
    User user;
    if (id == null) { // 新規作成の場合
        user = convertToEntity(userRequest);
    } else { // 更新の場合
        // まず既存のユーザーを取得
        user = userRepository.findById(id)
                             .orElseThrow(() -> new com.example.demoapi.exception.UserNotFoundException(id)); // 存在しない場合は例外
        // DTOの内容をエンティティに反映
        user.setName(userRequest.getName());
        user.setEmail(userRequest.getEmail());
    }
    User savedUser = userRepository.save(user); // 保存または更新
    return convertToDto(savedUser); // 保存後のエンティティをDTOに変換して返す
}

// IDでユーザーを削除
@Transactional
public void deleteUser(Long id) {
    // 削除前に存在チェックが必要ならここで行う(コントローラーで行っても良い)
    if (!userRepository.existsById(id)) {
        throw new com.example.demoapi.exception.UserNotFoundException(id);
    }
    userRepository.deleteById(id);
}

// IDが存在するか確認 (主にコントローラーでの事前チェック用)
public boolean existsById(Long id) {
    return userRepository.existsById(id);
}

}
“`

12.2. コントローラーでのDTOの利用

UserController を修正して、リクエスト引数に UserRequest を、戻り値に List<UserResponse>ResponseEntity<UserResponse> を使うようにします。

“`java
package com.example.demoapi;

import com.example.demoapi.dto.UserRequest; // リクエストDTOをインポート
import com.example.demoapi.dto.UserResponse; // レスポンスDTOをインポート
import com.example.demoapi.exception.UserNotFoundException;
import com.example.demoapi.service.UserService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@RestController
@RequestMapping(“/users”)
public class UserController {

private final UserService userService;

@Autowired
public UserController(UserService userService) {
    this.userService = userService;
}

// GET /users - 全ユーザー取得 (List<UserResponse> を返す)
@GetMapping
public List<UserResponse> getAllUsers() {
    return userService.getAllUsers();
}

// GET /users/{userId} - IDでユーザー取得 (ResponseEntity<UserResponse> を返す)
@GetMapping("/{userId}")
public ResponseEntity<UserResponse> getUserById(@PathVariable Long userId) {
    // Service層は Optional<UserResponse> を返すように変更したので、そのまま利用
    return userService.getUserById(userId)
                      .map(ResponseEntity::ok)
                      .orElseThrow(() -> new UserNotFoundException(userId));
}

// POST /users - 新規ユーザー作成 (UserRequest を受け取り、ResponseEntity<UserResponse> を返す)
@PostMapping
public ResponseEntity<UserResponse> createUser(@Valid @RequestBody UserRequest userRequest) {
    // Service層の saveUser メソッドに RequestDTO と null (新規作成のため) を渡す
    UserResponse createdUser = userService.saveUser(userRequest, null);

    URI location = URI.create("/users/" + createdUser.getId());
    return ResponseEntity.created(location).body(createdUser);
}

// PUT /users/{userId} - ユーザー更新 (UserRequest を受け取り、ResponseEntity<UserResponse> を返す)
@PutMapping("/{userId}")
public ResponseEntity<UserResponse> updateUser(@PathVariable Long userId, @Valid @RequestBody UserRequest userDetails) {
    // Service層の saveUser メソッドに RequestDTO と userId を渡す
    UserResponse updatedUser = userService.saveUser(userDetails, userId);
    return ResponseEntity.ok(updatedUser);
}

// DELETE /users/{userId} - ユーザー削除
@DeleteMapping("/{userId}")
public ResponseEntity<Void> deleteUser(@PathVariable Long userId) {
    // Service層の deleteUser メソッド内で存在チェックと例外スローを行うように変更
    userService.deleteUser(userId);
    return ResponseEntity.noContent().build();
}

// MethodArgumentNotValidException のハンドラーは DTO のフィールド名を使うように調整が必要
// GlobalExceptionHandler にて共通化するのが望ましい
 @ExceptionHandler(MethodArgumentNotValidException.class)
 @ResponseStatus(HttpStatus.BAD_REQUEST)
 public Map<String, String> handleValidationExceptions(MethodArgumentNotValidException ex) {
    Map<String, String> errors = new HashMap<>();
    ex.getBindingResult().getFieldErrors().forEach(error ->
        // DTOのフィールド名とエラーメッセージをマップに詰める
        errors.put(error.getField(), error.getDefaultMessage()));
    return errors;
 }

// ... UserNotFoundException のハンドラー (GlobalExceptionHandler に移動済み)

}
“`

DTOを導入することで、コントローラーはAPIの入出力形式に集中でき、サービス層はエンティティを使ったビジネスロジックに集中できます。エンティティとAPI仕様が分離され、コードの保守性と進化が容易になります。少し記述が増えますが、これは中規模以上のアプリケーションでは必須とも言える設計パターンです。

13. アプリケーション設定

Spring Bootアプリケーションの設定は、通常 src/main/resources ディレクトリにある application.properties または application.yml ファイルで行います。

これらのファイルには、アプリケーションが実行時に必要とする様々な設定値を記述します。

  • サーバーポートの変更:
    properties
    server.port=8090

    または
    yaml
    server:
    port: 8090
  • データベース接続設定:
    properties
    spring.datasource.url=jdbc:mysql://localhost:3306/mydb
    spring.datasource.username=myuser
    spring.datasource.password=mypassword
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.jpa.hibernate.ddl-auto=update
    spring.jpa.show-sql=true # SQLログを出力
  • カスタム設定: 独自のプロパティを定義し、Javaコードから @Value アノテーションや @ConfigurationProperties アノテーションを使って取得することもできます。
    properties
    my.greeting=Hello from application.properties!

    “`java
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;

    @Component
    public class MyService {
    @Value(“${my.greeting}”) // application.properties の値を注入
    private String greeting;

    public String getGreeting() {
        return greeting;
    }
    

    }
    “`

application.yml はYAML形式で記述するため、階層構造を分かりやすく表現できます。どちらを使用しても機能的には変わりませんが、YAML形式が好まれる傾向にあります。

14. プロファイル

アプリケーションは、開発環境、テスト環境、本番環境など、異なる環境で実行されることがよくあります。環境ごとに異なる設定(データベース接続先、ログレベルなど)を切り替えたい場合に「プロファイル(Profile)」機能が役立ちます。

Spring Bootでは、application-{profile}.properties または application-{profile}.yml というファイル名でプロファイルごとの設定を定義できます。

例えば、開発環境用の設定を application-dev.properties に記述します。

“`properties

application-dev.properties

server.port=8080
spring.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto=create-drop # 開発中は起動時にDBを初期化
“`

本番環境用の設定を application-prod.properties に記述します。

“`properties

application-prod.properties

server.port=80
spring.datasource.url=jdbc:mysql://prod-db-server:3306/prod_db
spring.datasource.username=produser
spring.datasource.password=prodpassword
spring.jpa.hibernate.ddl-auto=none # 本番では自動変更しない
spring.jpa.show-sql=false # 本番ではSQLログを出さない
logging.level.root=INFO # ログレベルを抑える
“`

application.properties は共通設定を記述するか、デフォルトプロファイルとして機能します。特定のプロファイルで定義された設定は、デフォルト設定を上書きします。

実行時に使用するプロファイルは、以下のいずれかの方法で指定します。

  • application.properties で指定:
    properties
    spring.profiles.active=dev
  • 環境変数で指定:
    bash
    SPRING_PROFILES_ACTIVE=prod java -jar your-api.jar
  • コマンドライン引数で指定:
    bash
    java -jar your-api.jar --spring.profiles.active=prod
  • Maven/Gradleで指定:
    bash
    # Maven
    ./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
    # Gradle
    ./gradlew bootRun --args='--spring.profiles.active=dev'

プロファイルを適切に使うことで、異なる環境設定を分離し、管理しやすくなります。

15. テスト

高品質なAPI開発には、自動テストが不可欠です。Spring Bootはテストを強力にサポートしており、JUnitなどのテストフレームワークと組み合わせて様々なレベルのテスト(単体テスト、統合テスト、コントローラーテストなど)を記述できます。

spring-boot-starter-test 依存関係には、Spring Bootアプリケーションのテストに必要なライブラリ(JUnit 5, Mockito, Spring Test, AssertJなど)がまとめて含まれています。Spring Initializrで Spring Web を選択していれば、自動的に含まれています。

15.1. コントローラーテスト (MockMvc)

データベースアクセスなどを含まない、コントローラー単体(厳密にはSpring MVCレイヤー)のテストを行う場合に、MockMvc を利用するのが便利です。HTTPリクエストを模倣してコントローラーメソッドを呼び出し、レスポンスの内容やステータスコードを検証できます。

src/test/java/... パッケージ配下にある、Spring Initializrで生成されたテストクラス (DemoApiApplicationTests.java など) を修正するか、新しいテストクラスを作成します。

例として、UserController の一部をテストするクラスを作成してみましょう。

“`java
package com.example.demoapi;

import com.example.demoapi.dto.UserResponse;
import com.example.demoapi.exception.UserNotFoundException;
import com.example.demoapi.model.User;
import com.example.demoapi.service.UserService; // サービス層をモック化するためインポート
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; // Web MVCレイヤーのみテスト
import org.springframework.boot.test.mock.mockito.MockBean; // サービス層をモック化
import org.springframework.http.MediaType; // HTTPメディアタイプ用
import org.springframework.test.web.servlet.MockMvc; // MockMvc をインポート
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; // リクエストビルダー
import org.springframework.test.web.servlet.result.MockMvcResultMatchers; // 結果マッチャー

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

// @WebMvcTest は Web MVC レイヤーのみをテストする
// コントローラーとそれに依存する最小限のコンポーネント(ViewResolver, ArgumentResolvers など)をロードする
// サービス層やリポジトリ層はロードしないため、MockBean でモック化する必要がある
@WebMvcTest(UserController.class) // テスト対象のコントローラーを指定
public class UserControllerTest {

@Autowired
private MockMvc mockMvc; // MockMvc が自動的に注入される

@MockBean // UserService をモック化し、Springコンテナに登録する
private UserService userService;

@Test
void getAllUsers_shouldReturnListOfUsers() throws Exception {
    // Arrange: テストの準備
    // UserService.getAllUsers() が呼ばれたときに返す値を定義
    List<UserResponse> users = Arrays.asList(
        new UserResponse(1L, "Alice", "[email protected]"),
        new UserResponse(2L, "Bob", "[email protected]")
    );
    // Mockito を使ってモックのメソッドの振る舞いを定義
    // when(...).thenReturn(...)
    org.mockito.Mockito.when(userService.getAllUsers()).thenReturn(users);

    // Act: テストの実行
    // MockMvcRequestBuilders を使って GET /users リクエストを作成し、perform() で実行
    mockMvc.perform(MockMvcRequestBuilders.get("/users")
            .contentType(MediaType.APPLICATION_JSON)) // リクエストの Content-Type
            // Assert: 結果の検証
            // MockMvcResultMatchers を使って結果を検証
            .andExpect(MockMvcResultMatchers.status().isOk()) // ステータスコードが 200 OK であること
            .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) // Content-Type が application/json であること
            .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(2)) // レスポンスJSONの配列サイズが 2 であること
            .andExpect(MockMvcResultMatchers.jsonPath("$[0].id").value(1)) // 1番目の要素の id が 1 であること
            .andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("Alice"))
            .andExpect(MockMvcResultMatchers.jsonPath("$[1].id").value(2))
            .andExpect(MockMvcResultMatchers.jsonPath("$[1].name").value("Bob"));

    // Verify: モックのメソッドが期待通りに呼び出されたか検証 (任意だが推奨)
    // Mockito を使ってメソッド呼び出しを検証
    // verify(...)
    org.mockito.Mockito.verify(userService).getAllUsers(); // userService.getAllUsers() が1回呼び出されたこと
}

@Test
void getUserById_withExistingId_shouldReturnUser() throws Exception {
    Long userId = 1L;
    UserResponse user = new UserResponse(userId, "Alice", "[email protected]");

    org.mockito.Mockito.when(userService.getUserById(userId)).thenReturn(Optional.of(user));

    mockMvc.perform(MockMvcRequestBuilders.get("/users/{userId}", userId) // パス変数に値を渡す
            .contentType(MediaType.APPLICATION_JSON))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(userId))
            .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Alice"));

    org.mockito.Mockito.verify(userService).getUserById(userId);
}

@Test
void getUserById_withNonExistingId_shouldReturnNotFound() throws Exception {
    Long userId = 999L;

    // 存在しない場合は Optional.empty() を返すようにモックを設定
    org.mockito.Mockito.when(userService.getUserById(userId)).thenReturn(Optional.empty());
    // ※サービス層で UserNotFoundException を投げるようにしている場合、MockBean もその例外を投げるように設定する

    mockMvc.perform(MockMvcRequestBuilders.get("/users/{userId}", userId)
            .contentType(MediaType.APPLICATION_JSON))
            // GlobalExceptionHandler で 404 にマッピングされていることを期待
            .andExpect(MockMvcResultMatchers.status().isNotFound()); // 404 Not Found であること

    org.mockito.Mockito.verify(userService).getUserById(userId);
}

// POST /users のテスト例 (作成)
@Test
void createUser_withValidData_shouldReturnCreatedUser() throws Exception {
    // リクエストボディとして送るデータ
    String userRequestJson = "{\"name\": \"Charlie\", \"email\": \"[email protected]\"}";
    // サービス層が返すであろう、IDが付与されたデータ
    UserResponse createdUser = new UserResponse(3L, "Charlie", "[email protected]");

    // Mockito: saveUser() が呼ばれたときに createdUser を返すように設定
    // any(UserRequest.class) は任意の UserRequest オブジェクトにマッチ
    org.mockito.Mockito.when(userService.saveUser(org.mockito.ArgumentMatchers.any(UserRequest.class), org.mockito.ArgumentMatchers.isNull(Long.class)))
               .thenReturn(createdUser);


    mockMvc.perform(MockMvcRequestBuilders.post("/users")
            .contentType(MediaType.APPLICATION_JSON) // リクエストの Content-Type
            .content(userRequestJson)) // リクエストボディの内容
            .andExpect(MockMvcResultMatchers.status().isCreated()) // ステータスコードが 201 Created であること
            .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(MockMvcResultMatchers.header().exists("Location")) // Locationヘッダーが存在すること
            // .andExpect(MockMvcResultMatchers.header().string("Location", "/users/3")) // 特定の値か検証
            .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(3))
            .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Charlie"));

    // Verify: saveUser() メソッドが正しい引数で呼び出されたか検証 (一部だけ検証する場合)
    org.mockito.Mockito.verify(userService).saveUser(org.mockito.ArgumentMatchers.any(UserRequest.class), org.mockito.ArgumentMatchers.isNull(Long.class));
}

// 無効なデータで POST /users のテスト例 (バリデーションエラー)
 @Test
 void createUser_withInvalidData_shouldReturnBadRequest() throws Exception {
     String invalidUserRequestJson = "{\"name\": \"\", \"email\": \"invalid-email\"}";

     // Service層は呼び出されないはずなので、MockBeanの verify は不要

     mockMvc.perform(MockMvcRequestBuilders.post("/users")
             .contentType(MediaType.APPLICATION_JSON)
             .content(invalidUserRequestJson))
             .andExpect(MockMvcResultMatchers.status().isBadRequest()) // ステータスコードが 400 Bad Request であること
             .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
             .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Name is mandatory")) // バリデーションエラーメッセージの検証
             .andExpect(MockMvcResultMatchers.jsonPath("$.email").value("Email should be valid"));

     // Service層のメソッドが呼び出されていないことを検証
     org.mockito.Mockito.verifyNoInteractions(userService);
 }

 // ... 他のメソッド (PUT, DELETE) のテストも同様に記述可能

}
“`

  • @WebMvcTest(UserController.class): Spring Bootのテストアノテーションで、Web MVCレイヤー(特に指定したコントローラー)のみをロードします。他のコンポーネント(サービス、リポジトリなど)はロードされません。
  • @MockBean UserService userService: テスト対象である UserController が依存している UserService をモック化します。実際の UserService の代わりに、Mockitoによって生成されたダミーオブジェクトが使われます。これにより、データベースアクセスなどの外部依存を排除し、コントローラー単体のロジック(リクエストマッピング、パラメータバインディング、バリデーション、サービス呼び出し、レスポンス生成など)に焦点を当てたテストが可能になります。
  • MockMvc: HTTPリクエストを模擬するためのオブジェクトです。mockMvc.perform(MockMvcRequestBuilders.get("/users")...) のように使います。
  • MockMvcRequestBuilders: GET, POSTなどのHTTPメソッドを使ったリクエストを作成するためのユーティリティクラスです。
  • MockMvcResultMatchers: レスポンスのステータス、ヘッダー、ボディなどの検証を行うためのユーティリティクラスです。jsonPath("$[0].id").value(1) のように、JsonPathを使ってJSONレスポンスの特定の要素の値も検証できます。
  • Mockitoの when(...).thenReturn(...): モックオブジェクトの特定のメソッドが特定の引数で呼び出された場合に、どのような値を返すかを定義します。
  • Mockitoの verify(...): モックオブジェクトの特定のメソッドが期待通りに呼び出されたかどうかを検証します。

15.2. 統合テスト (@SpringBootTest)

アプリケーション全体を起動してテストを行う場合は、@SpringBootTest アノテーションを使用します。このアノテーションは、Spring Bootアプリケーション全体(または一部)をロードしてテスト環境を構築します。組み込みサーバーを起動して実際のHTTPリクエストを送ることも、モック環境でテストすることも可能です。

@SpringBootTestMockMvc を組み合わせて、組み込みサーバーを起動せずにフルスタックに近いテストを行う例です。

“`java
package com.example.demoapi;

import com.example.demoapi.dto.UserRequest;
import com.fasterxml.jackson.databind.ObjectMapper; // JSON <-> オブジェクト変換用
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; // MockMvc を自動設定
import org.springframework.boot.test.context.SpringBootTest; // アプリケーション全体をロード
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles; // テストプロファイル指定
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.transaction.annotation.Transactional; // テストデータのクリーンアップ

// @SpringBootTest はアプリケーションコンテキスト全体をロードする
// webEnvironment = SpringBootTest.WebEnvironment.MOCK を指定すると、組み込みサーバーは起動せず MockMvc を使う
// webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT を指定すると、ランダムなポートでサーバーを起動し TestRestTemplate などを使う
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc // MockMvc を使用するための設定
@Transactional // 各テストメソッドの後にトランザクションをロールバックし、DBをクリーンアップ
@ActiveProfiles(“test”) // test プロファイルを有効にする(例: インメモリDBを使う設定)
public class UserIntegrationTest {

@Autowired
private MockMvc mockMvc;

@Autowired
private ObjectMapper objectMapper; // JSON変換用のオブジェクト

@Test
void createUserAndGetUser_shouldSucceed() throws Exception {
    // 1. ユーザー作成 (POST)
    UserRequest newUserRequest = new UserRequest("Diana", "[email protected]");
    String userRequestJson = objectMapper.writeValueAsString(newUserRequest); // DTO を JSON文字列に変換

    mockMvc.perform(MockMvcRequestBuilders.post("/users")
            .contentType(MediaType.APPLICATION_JSON)
            .content(userRequestJson))
            .andExpect(MockMvcResultMatchers.status().isCreated()) // 201 Created
            .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Diana"))
            .andExpect(MockMvcResultMatchers.jsonPath("$.email").value("[email protected]"))
            .andExpect(MockMvcResultMatchers.jsonPath("$.id").exists()); // IDが付与されていること

    // 作成されたユーザーのIDを取得する必要があるが、ここではシンプルに次のテストで仮のIDを使用するか、別途全件取得などで確認

    // 2. 全ユーザー取得 (GET /users) - 作成したユーザーが含まれているか検証
    mockMvc.perform(MockMvcRequestBuilders.get("/users")
            .contentType(MediaType.APPLICATION_JSON))
            .andExpect(MockMvcResultMatchers.status().isOk()) // 200 OK
            .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
            // 作成したユーザーが含まれているか、他のユーザーもいればリストサイズなどを検証
            .andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("Diana")); // ※順序は保証されない場合がある
}

@Test
void getUserById_withNonExistingId_shouldReturnNotFound() throws Exception {
    Long nonExistingId = 999L;

    mockMvc.perform(MockMvcRequestBuilders.get("/users/{userId}", nonExistingId)
            .contentType(MediaType.APPLICATION_JSON))
            .andExpect(MockMvcResultMatchers.status().isNotFound()); // 404 Not Found
}

// ... 他の統合テスト (PUT, DELETE など)

}
“`

  • @SpringBootTest: アプリケーション全体をロードし、完全なSpring ApplicationContextを構築します。Webレイヤー、サービスレイヤー、リポジトリレイヤーなど、すべてのコンポーネントがSpringによって管理され、実際に連携して動作します。
  • webEnvironment = SpringBootTest.WebEnvironment.MOCK: 実際の組み込みサーバーは起動せず、MockMvcを使ったテスト環境を構築します。高速に実行できます。
  • @AutoConfigureMockMvc: @SpringBootTest と組み合わせて使用する場合に、MockMvcオブジェクトを自動的に設定し、注入可能にします。
  • @Transactional: 各テストメソッドの実行後に、データベースへの変更を自動的にロールバックします。これにより、テスト間でデータベースの状態が独立し、テストの再現性が保証されます。通常、テスト用に別途インメモリデータベース(H2など)を使用し、application-test.properties などで設定します。
  • ObjectMapper: Jacksonライブラリの一部で、JavaオブジェクトとJSON文字列の相互変換を行います。リクエストボディを作成したり、レスポンスボディを解析したりする際に便利です。

統合テストは、個々のコンポーネント間の連携を含めたエンドツーエンドに近いシナリオを検証するのに役立ちます。実際のHTTPリクエスト/レスポンスフローに近い形でテストできるため、より現実的な問題を発見しやすいです。

これらのテスト手法を組み合わせることで、開発したAPIの品質を確保できます。

16. APIドキュメンテーション

開発したAPIは、クライアント開発者や他のAPI利用者がAPIの仕様(URI、HTTPメソッド、リクエスト/レスポンス形式、パラメータなど)を理解できるように、適切にドキュメント化する必要があります。APIドキュメンテーションは、APIの利用を促進し、開発効率を向上させます。

手動でドキュメントを作成するのは手間がかかり、APIの変更に追随しにくいという問題があります。そこで、コードから自動的にドキュメントを生成するツールが利用されます。Swagger (現在はOpenAPI Specificationという名称が一般的) は、APIを記述するための標準仕様であり、様々なツールやライブラリがこの仕様をサポートしています。

Spring Bootでは、Springdoc-openapiライブラリを使うことで、コードにアノテーションを付与したり設定を行ったりするだけで、OpenAPI仕様に準拠したAPIドキュメント(通常はJSON/YAML形式)を生成し、Swagger UIやRedocといったツールでインタラクティブなAPIドキュメント画面を提供できます。

16.1. 依存関係の追加

Swagger UIを含むSpringdoc-openapiの依存関係を pom.xml に追加します。

“`xml



org.springdoc
springdoc-openapi-starter-webmvc-ui
2.x.x

“`

依存関係を追加してプロジェクトをリロードしたら、特にJavaコードを何も変更しなくても、Spring Bootアプリケーションを起動するだけで、Springdoc-openapiがコントローラーやメソッド、DTOなどをスキャンし、OpenAPIドキュメントを自動生成してくれます。

16.2. Swagger UIへのアクセス

アプリケーション起動後、Webブラウザで以下のURLにアクセスすると、Swagger UIが表示されます。

http://localhost:8080/swagger-ui.html

ここで、定義したAPIエンドポイントの一覧や、各エンドポイントの詳細(パス、メソッド、パラメータ、リクエスト/レスポンス形式、ステータスコードなど)を確認できます。また、Swagger UI上から実際にAPIを試すこともできます。

16.3. ドキュメントのカスタマイズ (Optional)

デフォルトでもある程度のドキュメントが生成されますが、より詳細な情報(APIの説明、パラメータの説明、レスポンスの詳細、認証情報など)を追加したい場合は、OpenAPI仕様で定義されたアノテーションをコードに付与します。

例として、UserController.java や DTOクラス (UserRequest.java, UserResponse.java) にいくつかアノテーションを追加してみましょう。

“`java
package com.example.demoapi;

import com.example.demoapi.dto.UserRequest;
import com.example.demoapi.dto.UserResponse;
import com.example.demoapi.exception.UserNotFoundException;
import com.example.demoapi.service.UserService;
import io.swagger.v3.oas.annotations.Operation; // API操作の説明用
import io.swagger.v3.oas.annotations.Parameter; // パラメータの説明用
import io.swagger.v3.oas.annotations.responses.ApiResponse; // レスポンスの説明用
import io.swagger.v3.oas.annotations.responses.ApiResponses; // 複数のレスポンス説明用
import io.swagger.v3.oas.annotations.tags.Tag; // APIグループ化用
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@RestController
@RequestMapping(“/users”)
@Tag(name = “User API”, description = “ユーザー情報を管理するAPI”) // このコントローラーのAPIグループ名と説明
public class UserController {

private final UserService userService;

@Autowired
public UserController(UserService userService) {
    this.userService = userService;
}

// GET /users - 全ユーザー取得
@Operation(summary = "全ユーザー情報を取得する", description = "登録されている全てのユーザーのリストを返します。") // このAPI操作の説明
@GetMapping
public List<UserResponse> getAllUsers() {
    return userService.getAllUsers();
}

// GET /users/{userId} - IDでユーザー取得
@Operation(summary = "IDを指定してユーザー情報を取得する")
@ApiResponses(value = { // 複数のレスポンスを定義
        @ApiResponse(responseCode = "200", description = "ユーザーが見つかりました"),
        @ApiResponse(responseCode = "404", description = "指定されたIDのユーザーが見つかりません")
})
@GetMapping("/{userId}")
public ResponseEntity<UserResponse> getUserById(
        @Parameter(description = "取得したいユーザーのID", example = "1") // パラメータの説明と例
        @PathVariable Long userId) {
    return userService.getUserById(userId)
                      .map(ResponseEntity::ok)
                      .orElseThrow(() -> new UserNotFoundException(userId));
}

// POST /users - 新規ユーザー作成
@Operation(summary = "新しいユーザーを作成する")
@ApiResponses(value = {
        @ApiResponse(responseCode = "201", description = "ユーザーが正常に作成されました"),
        @ApiResponse(responseCode = "400", description = "リクエストボディの内容が無効です")
})
@PostMapping
public ResponseEntity<UserResponse> createUser(
        @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "作成するユーザーの情報") // リクエストボディの説明
        @Valid @RequestBody UserRequest userRequest) {
    UserResponse createdUser = userService.saveUser(userRequest, null);

    URI location = URI.create("/users/" + createdUser.getId());
    return ResponseEntity.created(location).body(createdUser);
}

// ... 他のメソッドも同様にアノテーションを追加

}
“`

DTOクラスにも @Schema アノテーションを使ってフィールドの説明などを追加できます。

これらのアノテーションを追加してアプリケーションを再起動し、Swagger UIを再度確認すると、ドキュメントがよりリッチになっていることが分かります。

APIドキュメンテーションの自動生成は、API開発において非常に有用な機能です。

17. まとめと次のステップ

本記事では、Spring Bootを使ってREST API開発を始めるための基本的なステップと重要な概念を詳細に解説しました。

具体的には、以下の内容を学びました。

  • REST APIとSpring Bootの基礎知識
  • Spring Initializrを使ったプロジェクト作成
  • @RestController, @RequestMapping, @GetMapping, @PostMapping などを使ったAPIエンドポイントの実装
  • @PathVariable, @RequestParam, @RequestBody を使ったリクエストデータの取得
  • POJOや ResponseEntity を使ったレスポンスの返却
  • Spring Data JPA、エンティティ、リポジトリを使ったデータベース連携
  • サービス層によるビジネスロジックの分離
  • @ExceptionHandler, @ControllerAdvice による例外処理
  • Bean Validation (@Valid, @NotBlank など) による入力値検証
  • DTOを使ったAPI入出力とエンティティの分離
  • application.properties/yml による設定管理とプロファイルの活用
  • @WebMvcTest, @SpringBootTest, MockMvc, Mockito を使ったAPIテスト
  • Springdoc-openapi (Swagger UI) によるAPIドキュメンテーション

これらは、Spring BootでREST APIを開発するための土台となる知識です。実際に手を動かしながらサンプルコードを試すことで、理解が深まるはずです。

次のステップとして、さらに以下の内容を学ぶことで、より実践的なAPI開発スキルを習得できます。

  • セキュリティ: Spring Securityを使った認証・認可、JWT (JSON Web Token) ベースの認証など。
  • パフォーマンス: キャッシュ、非同期処理、データベースクエリの最適化など。
  • ロギング: LogbackやLog4j 2を使った詳細なログ出力設定。
  • マイクロサービス: Service Discovery, API Gateway, Circuit Breakerなどのパターン。
  • デプロイ: JARファイルとしてアプリケーションをパッケージングし、サーバー(Tomcat, Jetty以外)やクラウド環境(AWS, Azure, Google Cloud, Herokuなど)にデプロイする方法。
  • モニタリング: Spring Boot Actuatorを使ったアプリケーションの状態監視。
  • クロスオリジンリソース共有 (CORS): 別ドメインからのAPI呼び出しを許可する設定。

Spring Bootは非常に機能豊富で柔軟なフレームワークです。公式ドキュメントやコミュニティの情報を活用しながら、ぜひあなたのAPI開発を進めていってください。

本記事が、あなたのSpring Bootを使ったREST API開発の旅の第一歩となることを願っています。Happy Coding!


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール