🎃

【作業ログ+解説】Spring 公式ガイド

に公開

はじめに

前回は Spring Boot の公式チュートリアルを実施しました。

https://zenn.dev/hiroto_ohira/articles/2eb00443ff9a4d

今回は Spring ガイドからいくつか実施していきます。目標は、標準的な構成の Web アプリケーションの開発を体験することです。

Spring ガイド

ネクストステップとして以下がちょうど良さそうだ。

  • REST API の作成
  • JPA で MySQL データアクセス
  • MockMvc と @MockBean で Web レイヤーテスト

リポジトリは以下に作成した。

https://github.com/HitoroOhria/spring-guides

REST API の作成

まずは基本的な REST API の作成からやっていこう。公式のサンプルリポジトリはこれだ。

https://github.com/spring-guides/gs-rest-service

Spring Initializr から開始

今回は Spring Initializr を使用してみよう。

Spring Boot プロジェクトの作成 - Spring InitializrSpring Boot | IntelliJ IDEA Documentation を参考にして進める。


1. プロジェクトの作成


2. プロジェクトの設定を記述


3. 必要な依存関係を選択

Spring Initializr によりさまざまなファイルが作成された。

$ tree  
.
└── rest-service
    ├── build.gradle
    ├── gradle
    │   └── wrapper
    │       ├── gradle-wrapper.jar
    │       └── gradle-wrapper.properties
    ├── gradlew
    ├── gradlew.bat
    ├── HELP.md
    ├── rest-service.iml
    ├── settings.gradle
    └── src
        ├── main
        │   ├── java
        │   │   └── com
        │   │       └── example
        │   │           └── restservice
        │   │               └── RestServiceApplication.java
        │   └── resources
        │       ├── application.properties
        │       ├── static
        │       └── templates
        └── test
            └── java
                └── com
                    └── example
                        └── restservice
                            └── RestServiceApplicationTests.java

18 directories, 11 files

rest-service.iml は IntelliJ のプロジェクトファイルだ。この設定で作成されたのかもしれない。

起点となる Java ファイルが作成されていた。

@SpringBootApplication アノテーションが付与されている。まだ @RestController など REST API サーバーとしてのコードは追加されていないようだ。

RestServiceApplication.java
package com.example.restservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RestServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(RestServiceApplication.class, args);
    }

}

テストファイルも追加されていた。

JUnit Jupiter がインポートされている。Spring Boot に初めから組み込まれているテスティングライブラリだ。

contextLoads メソッドが定義されている。このメソッド名は何だろう?メタ的なメソッド名だ。いったん置いておこう。

RestServiceApplicationTests.java
package com.example.restservice;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class RestServiceApplicationTests {

    @Test
    void contextLoads() {
    }

}

リソース表現クラスを作成する

API からは次のようなレスポンスを返す。

{
    "id": 1,
    "content": "Hello, World!"
}

そのためのオブジェクトを定義する。record という定義は初めて知った。Record Class セクションで詳細を調べた。

Greeting.java
package com.example.restservice;

public record Greeting(long id, String content) {
}

リソースコントローラーを作成する

/greeting にアクセスしたらレスポンスを返すコントローラーを定義する。

GreetingController.java
package com.example.restservice;

import java.util.concurrent.atomic.AtomicLong;

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

@RestController
public class GreetingController {

    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();

    @GetMapping("/greeting")
    public Greeting greeting(@RequestParam(defaultValue = "World") String name) {
        return new Greeting(counter.incrementAndGet(), String.format(template, name));
    }
}

AtomicLong とは何だろう?コード見る限り、一意な値を表現しやすい数値型のようだ。

  • @RestController アノテーション
    • @Controller アノテーションと @ResponseBody アノテーションを付与する
    • このコントローラーは、すべてのメソッドがビューではなくドメインオブジェクトを返す
      • このコントローラーの @RequestMapping メソッドは、自動的にレスポンスの Body として返り値がシリアライズされる
      • JSON へのシリアライズには jackson が使用される
  • @RequestMapping アノテーション
    • リクエストをコントローラーメソッドにマッピングする
    • ここでは、/greeting への HTTP GET リクエストを greeting() メソッドにマッピングしている
  • @ReqestParam アノテーション
    • リクエストパラメーターをコントローラーのメソッドの引数にバインドする
    • ここでは、クエリ文字列パラメーター name の値を greeting() メソッドの name パラメーターにバインドしている

サービスを実行する

Spring Initilizr により、すでにアプリケーションクラスは作成されている。

RestServiceApplication.java
package com.example.restservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RestServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(RestServiceApplication.class, args);
    }

}
  • @SpringBootApplication アノテーション
    • 以下の3つのアノテーションを付与する
      • @EnableAutoConfigration アノテーション
        • Spring Boot の自動構成メカニズムを有効にする
      • @ComponentScan アノテーション
        • アプリケーションが配置されているパッケージで @Component スキャンを有効にする
      • @SpringBootConfiguration アノテーション
        • コンテキストの追加、Bean の登録、追加の構成クラスのインポートを有効にする

アプリケーションを実行してみよう。

❯ ./gradlew bootRun

> Task :bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.5.4)

2025-08-11T18:35:15.722+09:00  INFO 35238 --- [rest-service] [  restartedMain] c.e.restservice.RestServiceApplication   : Starting RestServiceApplication using Java 21.0.2 with PID 35238 (/Users/user/ghq/github.com/HitoroOhria/spring-boot-guides/rest-service/build/classes/java/main started by user in /Users/user/ghq/github.com/HitoroOhria/spring-boot-guides/rest-service)
2025-08-11T18:35:15.722+09:00  INFO 35238 --- [rest-service] [  restartedMain] c.e.restservice.RestServiceApplication   : No active profile set, falling back to 1 default profile: "default"
2025-08-11T18:35:15.737+09:00  INFO 35238 --- [rest-service] [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2025-08-11T18:35:15.737+09:00  INFO 35238 --- [rest-service] [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2025-08-11T18:35:16.031+09:00  INFO 35238 --- [rest-service] [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
2025-08-11T18:35:16.038+09:00  INFO 35238 --- [rest-service] [  restartedMain] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2025-08-11T18:35:16.038+09:00  INFO 35238 --- [rest-service] [  restartedMain] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.43]
2025-08-11T18:35:16.049+09:00  INFO 35238 --- [rest-service] [  restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2025-08-11T18:35:16.049+09:00  INFO 35238 --- [rest-service] [  restartedMain] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 312 ms
2025-08-11T18:35:16.160+09:00  INFO 35238 --- [rest-service] [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 35729
2025-08-11T18:35:16.168+09:00  INFO 35238 --- [rest-service] [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2025-08-11T18:35:16.172+09:00  INFO 35238 --- [rest-service] [  restartedMain] c.e.restservice.RestServiceApplication   : Started RestServiceApplication in 0.592 seconds (process running for 0.736)
<==========---> 80% EXECUTING [5s]
> :bootRun

http://localhost:8080/greeting にアクセスしてみよう。レスポンスが表示されている。

リクエストパラメーター name を付与してみよう。http://localhost:8080/greeting?name=taro にアクセスすると、レスポンスの値が変わる。さらに id キーの値が 1 から 2 に変更された。

JPA で MySQL データアクセス

次に Spring Boot アプリケーションから DB へアクセスする方法を見ていこう。公式のサンプルリポジトリはこれだ。

https://github.com/spring-guides/gs-accessing-data-mysql/

MySQL データベースの設定

このガイドでは Spring Boot Docker Compose Support を使用するようだ。次の操作を実行する依存関係 spring-boot-docker-compose を追加するとのこと。

  • 作業ディレクトリで compose.yml ファイルを検索
  • 検出された compose.yml を使用して docker compose up を呼び出す
  • サポートされているコンテナごとにサービス接続 Bean を作成する
  • アプリケーションのシャットダウン時に docker compose stop を呼び出す

ここまでできるのか。手厚い。

Spring Initializr から開始

進め方は前回と同じなので割愛する。依存関係が変化しており、以下が必要なようだ。

  • Spring Web
  • Spring Data JPA
  • MySQL Driver
  • Docker Compose Support
  • Testcontainers

次のようなプロジェクトが作成された。

テストファイルが多い。TestAccessingDataMysqlApplication.javaTestcontainersConfiguration.java が追加されている。テストで Docker の MySQL コンテナを使用するコードが書かれているようだ。テストコードレベルでもここまで統合できることには驚いた。

$ tree                                                   
.
├── build.gradle
├── compose.yaml
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── example
    │   │           └── accessingdatamysql
    │   │               └── AccessingDataMysqlApplication.java
    │   └── resources
    │       ├── application.properties
    │       ├── static
    │       └── templates
    └── test
        └── java
            └── com
                └── example
                    └── accessingdatamysql
                        ├── AccessingDataMysqlApplicationTests.java
                        ├── TestAccessingDataMysqlApplication.java
                        └── TestcontainersConfiguration.java

17 directories, 12 files

ビルドファイルは以下の通りだ。特筆すべき点はなさそうだ。

build.gradle
plugins {
	id 'java'
	id 'org.springframework.boot' version '3.5.4'
	id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(21)
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	developmentOnly 'org.springframework.boot:spring-boot-docker-compose'
	runtimeOnly 'com.mysql:mysql-connector-j'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.boot:spring-boot-testcontainers'
	testImplementation 'org.testcontainers:junit-jupiter'
	testImplementation 'org.testcontainers:mysql'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
	useJUnitPlatform()
}

compose.yaml ファイルも追加されている。中身をみてみよう。

compose.yaml
services:
  mysql:
    image: 'mysql:latest'
    environment:
      - 'MYSQL_DATABASE=mydatabase'
      - 'MYSQL_PASSWORD=secret'
      - 'MYSQL_ROOT_PASSWORD=verysecret'
      - 'MYSQL_USER=myuser'
    ports:
      - '3306'

mysql のバージョンは latest ではなく最新の 9.4 で固定しよう。環境変数によりデータベースやユーザーの作成が行われる。詳しくは mysql の Docker Hub を確認しよう。

最終的にこのようなファイルとなった。

compose.yaml
services:
  mysql:
    image: 'mysql:9.4'
    environment:
      - 'MYSQL_DATABASE=mydatabase'
      - 'MYSQL_ROOT_PASSWORD=verysecret'
      - 'MYSQL_USER=myuser'
      - 'MYSQL_PASSWORD=secret'
    ports:
      - '3306'

@Entity モデルを作成する

エンティティモデルを作成しよう。

User.java
package com.example.accessingdatamysql;

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

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    private String name;

    private String email;

    public String getId() {
        return id;
    }

    public void setId(Integer 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;
    }
}
  • @Entity アノテーション
    • Hibernate にクラスのテーブルを作成するように指示する

リポジトリを作成する

ユーザーレコードを操作するリポジトリを作成する。

UserRepository.java
package com.example.accessingdatamysql;

import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<User, Integer> {
}

コード量が少なくて驚く。よもやクラスを継承するだけとは。裏側で一体どのような黒魔術が使われているのだろう。

CrudRepository のジェネリクスの第二パラメーターは、テーブルの id の型を表現しているらしい。

また、Spring により このインターフェースは userRepository という Bean に自動的に実装されるらしい。先頭を小文字にすることでシンボル定義の競合を回避している。Bean というものが何物かわからないが、今は先に進もう。

コントローラーの作成

アプリケーションの HTTP リクエストを処理するコントローラーを作成しよう。

MainController.java
package com.example.accessingdatamysql;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping(path = "demo")
public class MainController {

    @Autowired
    private UserRepository userRepository;

    public @ResponseBody String addNewUser(@RequestParam String name, @RequestParam String email) {
        User u = new User();
        u.setName(name);
        u.setEmail(email);

        userRepository.save(u);

        return "Saved";
    }

    public @ResponseBody Iterable<User> getAllUsers() {
        return userRepository.findAll();
    }
}

「REST API の作成」と比べて、さまざまな変更が加えらている。

  • @RestController アノテーションではなく @Controller アノテーションが使用されている
    • @RestController アノテーションは、@Controller アノテーションと @ResponseBody アノテーションを付与するものだった
    • 今回は、メソッドごとに @ResponseBody アノテーションが付与されている
    • 違いとして、@ResponseBody アノテーションを付与しないメソッドの定義も視野に入れているのだろうか?
      • 今回のケースでは @RestController アノテーションで十分なように見えるが...
  • @Autowired アノテーションが使用されている
    • 依存関係を自動的に解決し、依存性を注入する
  • userRepository.findAll()Iterable<User> を返している
    • てっきり配列の User[] を返すのかと思った
    • Iterable<User> でも配列としてシリアライズしてくれるようだ

アプリケーションクラスを作成する

アプリケーションクラスを作成しよう。すでに Spring Initializr により作成されているはずだ。

AccessingDataMysqlApplication.java
package com.example.accessingdatamysql;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AccessingDataMysqlApplication {

	public static void main(String[] args) {
		SpringApplication.run(AccessingDataMysqlApplication.class, args);
	}

}

この例では、AccessingDataMysqlApplication クラスを変更する必要はないとのこと。メインクラスに @SpringBootApplication アノテーションが追加されており、Spring Boot へメインクラスであることを伝えている。

アプリケーションの実行

アプリケーションを実行してみよう。

$ ./gradlew bootRun

> Task :bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.5.4)

2025-08-11T19:27:20.419+09:00  INFO 48966 --- [accessing-data-mysql] [           main] c.e.a.AccessingDataMysqlApplication      : Starting AccessingDataMysqlApplication using Java 21.0.2 with PID 48966 (/Users/user/ghq/github.com/HitoroOhria/spring-boot-guides/accessing-data-mysql/build/classes/java/main started by user in /Users/user/ghq/github.com/HitoroOhria/spring-boot-guides/accessing-data-mysql)
2025-08-11T19:27:20.420+09:00  INFO 48966 --- [accessing-data-mysql] [           main] c.e.a.AccessingDataMysqlApplication      : No active profile set, falling back to 1 default profile: "default"
2025-08-11T19:27:20.438+09:00  INFO 48966 --- [accessing-data-mysql] [           main] .s.b.d.c.l.DockerComposeLifecycleManager : Using Docker Compose file /Users/user/ghq/github.com/HitoroOhria/spring-boot-guides/accessing-data-mysql/compose.yaml
2025-08-11T19:27:22.679+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  mysql Pulling 
2025-08-11T19:27:25.577+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  45a43a0d58dc Pulling fs layer 
2025-08-11T19:27:25.577+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  d7e901e4e4c1 Pulling fs layer 
2025-08-11T19:27:25.577+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  19592870864a Pulling fs layer 
2025-08-11T19:27:25.578+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  739cdd12ec77 Pulling fs layer 
2025-08-11T19:27:25.578+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  bc12642f0976 Pulling fs layer 
2025-08-11T19:27:25.578+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  3aab55028fec Pulling fs layer 
2025-08-11T19:27:25.578+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  da99ef17bcd1 Pulling fs layer 
2025-08-11T19:27:25.578+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  6bd5a6bb76bb Pulling fs layer 
2025-08-11T19:27:25.578+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  c8792e397394 Pulling fs layer 
2025-08-11T19:27:25.578+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  734e4b6b9573 Pulling fs layer 
2025-08-11T19:27:26.044+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  3aab55028fec Download complete 
2025-08-11T19:27:26.338+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  45a43a0d58dc Download complete 
2025-08-11T19:27:26.338+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  d7e901e4e4c1 Downloading [================>                                  ]  2.097MB/6.447MB
2025-08-11T19:27:26.338+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  bc12642f0976 Download complete 
2025-08-11T19:27:26.440+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  d7e901e4e4c1 Downloading [================>                                  ]  2.097MB/6.447MB
2025-08-11T19:27:26.441+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  c8792e397394 Downloading [>                                                  ]  1.049MB/167.2MB
2025-08-11T19:27:26.538+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  d7e901e4e4c1 Downloading [================>                                  ]  2.097MB/6.447MB
2025-08-11T19:27:26.539+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  19592870864a Download complete
...
2025-08-11T19:27:44.038+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  c8792e397394 Download complete 
025-08-11T19:27:47.005+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  45a43a0d58dc Pull complete 
2025-08-11T19:27:47.007+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  c8792e397394 Pull complete 
2025-08-11T19:27:47.009+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  mysql Pulled 
2025-08-11T19:27:47.085+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  Network accessing-data-mysql_default  Creating
2025-08-11T19:27:47.110+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  Network accessing-data-mysql_default  Created
2025-08-11T19:27:47.113+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  Container accessing-data-mysql-mysql-1  Creating
2025-08-11T19:27:47.416+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  Container accessing-data-mysql-mysql-1  Created
2025-08-11T19:27:47.433+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  Container accessing-data-mysql-mysql-1  Starting
2025-08-11T19:27:47.548+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  Container accessing-data-mysql-mysql-1  Started
2025-08-11T19:27:47.548+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  Container accessing-data-mysql-mysql-1  Waiting
2025-08-11T19:27:48.052+09:00  INFO 48966 --- [accessing-data-mysql] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   :  Container accessing-data-mysql-mysql-1  Healthy
2025-08-11T19:27:53.488+09:00  INFO 48966 --- [accessing-data-mysql] [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2025-08-11T19:27:53.503+09:00  INFO 48966 --- [accessing-data-mysql] [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 12 ms. Found 1 JPA repository interface.
2025-08-11T19:27:53.654+09:00  INFO 48966 --- [accessing-data-mysql] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
2025-08-11T19:27:53.660+09:00  INFO 48966 --- [accessing-data-mysql] [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2025-08-11T19:27:53.660+09:00  INFO 48966 --- [accessing-data-mysql] [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.43]
2025-08-11T19:27:53.681+09:00  INFO 48966 --- [accessing-data-mysql] [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2025-08-11T19:27:53.681+09:00  INFO 48966 --- [accessing-data-mysql] [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 426 ms
2025-08-11T19:27:53.729+09:00  INFO 48966 --- [accessing-data-mysql] [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2025-08-11T19:27:53.877+09:00  INFO 48966 --- [accessing-data-mysql] [           main] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@790d8fdd
2025-08-11T19:27:53.877+09:00  INFO 48966 --- [accessing-data-mysql] [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2025-08-11T19:27:53.898+09:00  INFO 48966 --- [accessing-data-mysql] [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2025-08-11T19:27:53.916+09:00  INFO 48966 --- [accessing-data-mysql] [           main] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 6.6.22.Final
2025-08-11T19:27:53.926+09:00  INFO 48966 --- [accessing-data-mysql] [           main] o.h.c.internal.RegionFactoryInitiator    : HHH000026: Second-level cache disabled
2025-08-11T19:27:54.031+09:00  INFO 48966 --- [accessing-data-mysql] [           main] o.s.o.j.p.SpringPersistenceUnitInfo      : No LoadTimeWeaver setup: ignoring JPA class transformer
2025-08-11T19:27:54.071+09:00  INFO 48966 --- [accessing-data-mysql] [           main] org.hibernate.orm.connections.pooling    : HHH10001005: Database info:
        Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
        Database driver: undefined/unknown
        Database version: 9.4
        Autocommit mode: undefined/unknown
        Isolation level: undefined/unknown
        Minimum pool size: undefined/unknown
        Maximum pool size: undefined/unknown
2025-08-11T19:27:54.343+09:00  INFO 48966 --- [accessing-data-mysql] [           main] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-08-11T19:27:54.344+09:00  INFO 48966 --- [accessing-data-mysql] [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2025-08-11T19:27:54.411+09:00  WARN 48966 --- [accessing-data-mysql] [           main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2025-08-11T19:27:54.538+09:00  INFO 48966 --- [accessing-data-mysql] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2025-08-11T19:27:54.542+09:00  INFO 48966 --- [accessing-data-mysql] [           main] c.e.a.AccessingDataMysqlApplication      : Started AccessingDataMysqlApplication in 34.277 seconds (process running for 34.423)
<==========---> 80% EXECUTING [1m 23s]
> :bootRun

最初に色々とダウンロードしている。

さらにログを見ると Container accessing-data-mysql-mysql-1 Creating などが表示され、Docker コンテナを作成していることがわかる。実際にコンテナも作成されていた。

$ docker compose ps   
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                                           NAMES
628c4f12e397   mysql:latest   "docker-entrypoint.s…"   3 minutes ago   Up 3 minutes   0.0.0.0:60914->3306/tcp, [::]:60914->3306/tcp   accessing-data-mysql-mysql-1

アプリケーションをテストする

アプリケーションをテストしてみよう。

$ curl http://localhost:8080/demo/add -d name=First -d email=someemail@someemailprovider.com 
{"timestamp":"2025-08-11T10:53:35.043+00:00","status":500,"error":"Internal Server Error","path":"/demo/add"}

ふむ。エラーが返ってきてしまう。500 はサーバーエラーだ。ログを見てみよう。

java.sql.SQLSyntaxErrorException: Table 'mydatabase.user_seq' doesn't exist
        at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:112) ~[mysql-connector-j-9.3.0.jar:9.3.0]
        at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:114) ~[mysql-connector-j-9.3.0.jar:9.3.0]
        at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:990) ~[mysql-connector-j-9.3.0.jar:9.3.0]
...

2025-08-11T19:53:35.030+09:00  WARN 56539 --- [accessing-data-mysql] [nio-8080-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1146, SQLState: 42S02
2025-08-11T19:53:35.030+09:00 ERROR 56539 --- [accessing-data-mysql] [nio-8080-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper   : Table 'mydatabase.user_seq' doesn't exist
2025-08-11T19:53:35.039+09:00 ERROR 56539 --- [accessing-data-mysql] [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.InvalidDataAccessResourceUsageException: error performing isolated work [Table 'mydatabase.user_seq' doesn't exist] [n/a]; SQL [n/a]] with root cause

java.sql.SQLSyntaxErrorException: Table 'mydatabase.user_seq' doesn't exist
        at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:112) ~[mysql-connector-j-9.3.0.jar:9.3.0]
        at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:114) ~[mysql-connector-j-9.3.0.jar:9.3.0]
        at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:990) ~[mysql-connector-j-9.3.0.jar:9.3.0]

Table 'mydatabase.user_seq' doesn't exist」が本体のエラーメッセージだろう。

たしかにテーブルがいつ作成されるのかは気になる。作成するなら初回の Docker コンテナ作成時やアプリケーション起動時だ。しかし、アプリケーション起動時のログではテーブルが作成されていそうな気配は感じられなかった。

また存在しないテーブル名が user_seq というのも気になる。単純な user テーブルではないようだ。_seq の命名から何か連番で格納するようなテーブルだと推測できるが、何者かは見当がつかない。

調べると以下の記事を見つけた。どうやら @GeneratedValue(strategy = GenerationType.AUTO) は使用するべきではないようだ。代わりに @GeneratedValue(strategy = GenerationType.IDENTITY) を使用することを提案している。

My Java Spring Boot application adds the suffix '_seq' to table users - Stack Overflow

User.java
@Entity
public class User {

    @Id
-   @GeneratedValue(strategy = GenerationType.AUTO)
+   @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

}

この状態でアプリケーションを停止し、もう一度起動する。再度テストしてみよう。

$ curl http://localhost:8080/demo/add -d name=First -d email=someemail@someemailprovider.com 
{"timestamp":"2025-08-11T10:53:35.043+00:00","status":500,"error":"Internal Server Error","path":"/demo/add"}

ふむ、同じく 500 エラーレスポンスだ。ログはどうだろう?

java.sql.SQLSyntaxErrorException: Table 'mydatabase.user' doesn't exist
        at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:112) ~[mysql-connector-j-9.3.0.jar:9.3.0]
        at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:114) ~[mysql-connector-j-9.3.0.jar:9.3.0]
        at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:990) ~[mysql-connector-j-9.3.0.jar:9.3.0]

エラーメッセージが変わっている。どうやら mydatabase.user_seq テーブルへのアクセスはスキップされたようだ。代わりに「mydatabase.user テーブルが存在しない」というエラーメッセージが表示されている。

さて、いよいよわからない。どのようにしてエンティティモデルからテーブルを作成するのだろうか?

LLM に聞いたら application.properties にデータベースの設定が不足していることが原因らしい。公式リポジトリの application.properties を見るとたしかに差分がありそうだ。

spring.application.name=accessing-data-mysql

+ # MySQL Database Configuration
+ spring.jpa.hibernate.ddl-auto=update
+ spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:3306/db_example
+ spring.datasource.username=springuser
+ spring.datasource.password=ThePassword
+ spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

気になる点として、compose.yaml で作成しているユーザー・パスワードと、application.properties で設定しているユーザー・パスワードが異なる。また MYSQL_HOST の値はどうやって解決しているのだろう?いったん試してみよう。

今度はどうだろう?良さそうだ!

$ curl http://localhost:8080/demo/add -d name=First -d email=someemail@someemailprovider.com
Saved

作成したユーザーを確認してみよう。こちらも良さそうだ。

$ curl http://localhost:8080/demo/all 
[{"id":1,"name":"First","email":"someemail@someemailprovider.com"}]

アプリケーション構築の準備

アプリケーションに外部の MySQL データベースへ接続できるように設定を変更するらしい。以前は Spring Boot Docker Compose Support による自動接続の設定があったが、それを手動で行うのだと。

まずは compose.yaml ファイルを編集する。

compose.yaml
services:
  mysql:
    image: 'mysql:9.4'
+   container_name: 'guide-mysql'
    environment:
      - 'MYSQL_DATABASE=mydatabase'
      - 'MYSQL_ROOT_PASSWORD=verysecret'
      - 'MYSQL_USER=myuser'
      - 'MYSQL_PASSWORD=secret'
    ports:
-     - '3306'
+     - '3306:3306'

コンテナ名の指定はあまり意味はないように思える。MySQL コンテナの外部へ公開するポートを 3306 で固定した。

次に application.properties を修正しよう。

application.properties
spring.application.name=accessing-data-mysql

# MySQL Database Configuration
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase
spring.datasource.username=myuser
spring.datasource.password=secret
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.show-sql: true

ふむ、正しい値がセットされているうように見える。

動作確認してみよう。

$ docker compose up                             
[+] Running 1/1
 ✔ Container accessing-data-mysql-mysql-1
Attaching to guide-mysql
...

$ docker compose ps
NAME          IMAGE       COMMAND                  SERVICE   CREATED              STATUS              PORTS
guide-mysql   mysql:9.4   "docker-entrypoint.s…"   mysql     About a minute ago   Up About a minute   0.0.0.0:3306->3306/tcp, [::]:3306->3306/tcp

$ ./gradlew bootRun
...
> :bootRun

サーバーの起動には成功していそうだ。リクエストを投げてみよう。

$ curl http://localhost:8080/demo/add -d name=First -d email=someemail@someemailprovider.com
Saved

$ curl http://localhost:8080/demo/all
[{"id":1,"name":"First","email":"someemail@someemailprovider.com"},{"id":2,"name":"First","email":"someemail@someemailprovider.com"}]

docker compose down しなかったため、前回のデータが MySQL コンテナに残っている。サーバーから MySQL への接続には成功していそうだ。

アプリケーションの構築

アプリケーションを構築する方法が紹介されていた。詳細は割愛する。

  1. JAR ファイルのビルドと実行
  2. Cloud Native Buildpacks を使用して Docker コンテナーを構築して実行する
  3. ネイティブイメージの構築と実行
  4. Cloud Native Buildpacks を使用したネイティブイメージコンテナーの構築と実行

MockMvc と @MockBean で Web レイヤーテスト

最後に、Spring アプリケーションを構築し、JUnit でテストする方法を見ていこう。公式のサンプルリポジトリはこれだ。

https://github.com/spring-guides/gs-testing-web

Spring Initializr から開始

例によって Spring Initializr から開始する。詳細は割愛する。今回は依存関係は Spring Web のみで良いようだ。

作成されたプロジェクトは次のとおり。シンプルな内容だ。

$ tree                                                   
.
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── example
    │   │           └── testingweb
    │   │               └── TestingWebApplication.java
    │   └── resources
    │       ├── application.properties
    │       ├── static
    │       └── templates
    └── test
        └── java
            └── com
                └── example
                    └── testingweb
                        └── TestingWebApplicationTests.java

17 directories, 9 files

シンプルなアプリケーションを作成する

/ にアクセスしたら固定文を返すコントローラーを追加しよう。

HomeController.java
package com.example.testingweb;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HomeController {

    @RequestMapping(path = "/")
    public @ResponseBody String greeting() {
        return "Hello World";
    }
}

アプリケーションの実行

毎度の如く、メインクラスは Spring Initializr により作成されている。

TestingWebApplication.java
package com.example.testingweb;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TestingWebApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestingWebApplication.class, args);
    }

}

アプリケーションを実行しよう。

❯ ./gradlew bootRun

> Task :bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.5.4)

2025-08-11T21:02:38.281+09:00  INFO 82201 --- [testing-web] [           main] c.e.testingweb.TestingWebApplication     : Starting TestingWebApplication using Java 21.0.2 with PID 82201 (/Users/user/ghq/github.com/HitoroOhria/spring-boot-guides/testing-web/build/classes/java/main started by user in /Users/user/ghq/github.com/HitoroOhria/spring-boot-guides/testing-web)
2025-08-11T21:02:38.284+09:00  INFO 82201 --- [testing-web] [           main] c.e.testingweb.TestingWebApplication     : No active profile set, falling back to 1 default profile: "default"
2025-08-11T21:02:38.796+09:00  INFO 82201 --- [testing-web] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
2025-08-11T21:02:38.803+09:00  INFO 82201 --- [testing-web] [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2025-08-11T21:02:38.803+09:00  INFO 82201 --- [testing-web] [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.43]
2025-08-11T21:02:38.823+09:00  INFO 82201 --- [testing-web] [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2025-08-11T21:02:38.823+09:00  INFO 82201 --- [testing-web] [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 503 ms
2025-08-11T21:02:38.944+09:00  INFO 82201 --- [testing-web] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2025-08-11T21:02:38.947+09:00  INFO 82201 --- [testing-web] [           main] c.e.testingweb.TestingWebApplication     : Started TestingWebApplication in 0.987 seconds (process running for 1.316)
<==========---> 80% EXECUTING [4s]
> :bootRun

リクエストを投げよう。成功だ。

$ curl "http://localhost:8080/"        
Hello World

アプリケーションをテストする

テストの追加

テストファイルを追加しよう。Spring Initializr によりすでに用意されている。

TestingWebApplicationTests.java
package com.example.testingweb;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class TestingWebApplicationTests {

    @Test
    void contextLoads() {
    }

}
  • @SpringBootTest アノテーション
    • Spring Boot にメイン構成クラス(たとえば @SpringBootApplication を持つもの)を探すように指示する
    • それを使用して Spring アプリケーションコンテキストを開始する
  • @Test アノテーション
    • メソッドがテストであることを示す

テストを実行してみよう。

$ ./gradlew test
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended

BUILD SUCCESSFUL in 1s
4 actionable tasks: 2 executed, 2 up-to-date

対象を指定してテストもできるようだ。こちらも試してみよう。

❯ ./gradlew test  --tests 'TestingWebApplicationTests'
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended

BUILD SUCCESSFUL in 2s
4 actionable tasks: 1 executed, 3 up-to-date

コントローラーの DI とアサーションの追加

アサーションする例を見ていこう。コントローラーを DI し、実際に DI されることを確認するテストだ。

SmokeTest.java
package com.example.testingweb;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
public class SmokeTest {

    @Autowired
    private HomeController controller;

    @Test
    void contextLoads() throws Exception {
        assertThat(controller).isNotNull();
    }
}
  • assertThat
    • 引数を比較メソッドで検証する

テストを実行すると成功する。

$ ./gradlew test                                      
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended

BUILD SUCCESSFUL in 2s
4 actionable tasks: 2 executed, 2 up-to-date

比較メソッドを isNull() に変更し、テストが落ちることを確認してみよう。

SmokeTest.java
package com.example.testingweb;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
public class SmokeTest {

    @Autowired
    private HomeController controller;

    @Test
    void contextLoads() throws Exception {
-       assertThat(controller).isNotNull();
+       assertThat(controller).isNull();
    }
}

テストを実行すると失敗する。

./gradlew test
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended

> Task :test FAILED

SmokeTest > contextLoads() FAILED
    org.opentest4j.AssertionFailedError at SmokeTest.java:17

2 tests completed, 1 failed

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/user/ghq/github.com/HitoroOhria/spring-boot-guides/testing-web/build/reports/tests/test/index.html

* Try:
> Run with --scan to get full insights.

BUILD FAILED in 1s
4 actionable tasks: 2 executed, 2 up-to-date

テストが失敗したファイル・行数とレポートのリンクが表されている。手厚い内容だ。

サーバーを起動した HTTP リクエストのテスト

サーバーのテストを書いてみよう。

HttpRequestTest.java
package com.example.testingweb;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class HttpRequestTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void greetingShouldReturnDefaultMessage() throws Exception {
        assertThat(this.restTemplate.getForObject("http://localhost:" + port + "/", String.class)).contains("Hello World");
    }
}

テストを実行してみよう。

❯ ./gradlew test
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2025-08-11T22:22:04.869+09:00  INFO 525 --- [testing-web] [ionShutdownHook] o.s.b.w.e.tomcat.GracefulShutdown        : Commencing graceful shutdown. Waiting for active requests to complete
2025-08-11T22:22:04.871+09:00  INFO 525 --- [testing-web] [tomcat-shutdown] o.s.b.w.e.tomcat.GracefulShutdown        : Graceful shutdown complete

BUILD SUCCESSFUL in 2s
4 actionable tasks: 2 executed, 2 up-to-date

o.s.b.w.e.tomcat.GracefulShutdown によるログが追加されている。どうやら Tomcat ならびに Spring Boot には、デフォルトで Graceful Shutdown が実装されているらしい。

サーバーをモックした HTTP リクエストのテスト

今度はサーバーを直接起動するのではなく、サーバーをモックしたテストを書いてみよう。

TestingWebApplicationTest.java
package com.example.testingweb;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
public class TestingWebApplicationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void shouldReturnDefaultMessage() throws Exception {
        this.mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk()).andExpect(content().string(containsString("Hello World")));
    }
}
  • @AutoConfigureMockMvc
    • MockMvc の自動構成を有効にする
  • MockMvc
    • Spring MVC アプリケーションのテストをサポートする
    • 実行中のサーバーの代わりに、モックしたリクエスト・レスポンスを利用する
    • MockMvcTester を介して、レスポンスを検証することができる

テストに成功する。

❯ ./gradlew test
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2025-08-11T22:29:27.563+09:00  INFO 2827 --- [testing-web] [ionShutdownHook] o.s.b.w.e.tomcat.GracefulShutdown        : Commencing graceful shutdown. Waiting for active requests to complete
2025-08-11T22:29:27.566+09:00  INFO 2827 --- [testing-web] [tomcat-shutdown] o.s.b.w.e.tomcat.GracefulShutdown        : Graceful shutdown complete

BUILD SUCCESSFUL in 2s
4 actionable tasks: 2 executed, 2 up-to-date

依存関係ありの対象をテスト

サービスを呼び出すコントローラーのように、依存関係がある場合、どのようにテストできるのか見てみよう。

GreetingService.java
package com.example.testingweb;

import org.springframework.stereotype.Service;

@Service
public class GreetingService {

    public String greet() {
        return "Hello World";
    }
}
GreetingController.java
package com.example.testingweb;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class GreetingController {

    private final GreetingService service;

    public GreetingController(GreetingService service) {
        this.service = service;
    }

    @RequestMapping("/greeting")
    public @ResponseBody String greeting() {
        return service.greet();
    }
}
  • GreetingController の実装
    • GreetingService に依存している
  • @Service アノテーション
    • クラスが「サービス」であることを示す
    • もしくはクラスが「ビジネスサービスファサード」であることを示す
  • コンストラクタのシグネチャから自動注入
    • GreetingController のコンストラクタは GreetingService を受け取っている
    • おそらく @WebMvcTest アノテーションおよび Spring により、依存関係が解決されて注入される

実際にどのようにテストするのか見てみよう。

WebMockTest.java
package com.example.testingweb;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.Matchers.containsString;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(GreetingController.class)
public class WebMockTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private GreetingService service;

    void greetingShouldReturnMessageFromService() throws Exception {
        when(service.greet()).thenReturn("Hello Mock");
        this.mockMvc.perform(get("/greeting")).andDo(print()).andExpect(status().isOk()).andExpect(content().string(containsString("Hello Mock")));
    }
}
  • @SpringBootTest アノテーションがない
    • これにより、Spring Boot にメイン構成クラスを探索させない
    • アプリケーション全体をテスト対象とすることを避けている
  • @WebMvcTest アノテーション
    • MVC テストに関連する自動構成のみを有効にする
    • パラメータでテスト対象のコントローラーを指定する
  • @MockBean アノテーション
    • Spring にモックを追加するアノテーション

テストは成功する。

$ ./gradlew test

> Task :compileTestJava
/Users/user/ghq/github.com/HitoroOhria/spring-boot-guides/testing-web/src/test/java/com/example/testingweb/WebMockTest.java:21: warning: [removal] MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
    @MockBean
     ^
1 warning
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2025-08-11T22:55:43.028+09:00  INFO 9910 --- [testing-web] [ionShutdownHook] o.s.b.w.e.tomcat.GracefulShutdown        : Commencing graceful shutdown. Waiting for active requests to complete
2025-08-11T22:55:43.031+09:00  INFO 9910 --- [testing-web] [tomcat-shutdown] o.s.b.w.e.tomcat.GracefulShutdown        : Graceful shutdown complete

[Incubating] Problems report is available at: file:///Users/user/ghq/github.com/HitoroOhria/spring-boot-guides/testing-web/build/reports/problems/problems-report.html

BUILD SUCCESSFUL in 2s
4 actionable tasks: 2 executed, 2 up-to-date

しかし、@MockBean アノテーションは現在は非推奨のようだ。警告が発生している。

深掘り

Java

AtomicLong

  • AtomicLong
    • アトミックに更新可能な long 値
    • アトミック変数のプロパティについては、java.util.concurrent.atomic パッケージを参照すること

Record Class

  • レコードクラス
    • プレーンデータ集約をモデル化することに役立つ
    • レコード宣言は、ヘッダーにその内容の説明を指定する
  • 効果
    • アクセサ、コンストラクタ、equalshashCodetoString メソッドが自動的に作成される
    • レコードのフィールドは final である
      • このクラスが単純な「データキャリア」として機能することを意図している
record Rectangle(double length, double width) { }

JPA

  • Java Persistence API
    • オブジェクトリレーショナルマッピングの POJO Persipence モデルを提供する

以下の記事も読んでみたが、よくわかっていない。

Spring

Spring Initializr

  • Spring Initializr
    • Spring プロジェクトの高速なスターター
  • 提供するもの
    • Java、Kotlin、Groovy 向けの基本的な言語生成
    • Apache Maven および Gradle の実装によるビルドシステムの抽象化
    • .gitignore サポート
    • カスタムリソース生成用の複数のフックポイント

Spring Data JPA

  • Spring Data JPA
    • JPA ベースの(Java Persistence API)リポジトリを簡単に実装できるようにする
    • 大規模な Spring Data ファミリーの一部である
  • 仕組み
    • 開発者がリポジトリインターフェイスを作成すると、Spring は自動的にコードを繋ぎ込む

Repository や ORM を提供する大規模なライブラリ、という理解でいる。

IOC コンテナと Bean

  • 本章で説明すること
    • Spring Framework における制御の反転(IOC)原則の実装
  • 依存性の注入(DI)
    • IOC の特殊な形式である
  • オブジェクトの依存関係が定義される箇所
    • コンストラクタの引数
    • ファクトリメソッドへの引数
    • オブジェクト作成後に設定されたプロパティ
      • オブジェクトインスタンスの作成後
      • ファクトリメソッドから返却された後
  • IOC コンテナ
    • Bean を作成する時にオブジェクトの依存性を注入する

要するに、Spring Boot ではオブジェクトを Bean としてマークしており、依存性を注入しているということであっているだろうか。オブジェクトの依存関係は、コンストラクタやファクトリメソッドの引数から判定されると。

application.properties ファイル

  • application.properties ファイル
    • その名の通り、Spring アプリケーションのプロパティのリスト

このファイルにいろいろな設定値を書くことになりそうだ。

Hibernate

さまざまなプロジェクトを展開している団体?という理解であっているのだろうか?

Hibernate ORM

  • Hibernate ORM
    • Hibernateは、Java で書かれたプログラムに関係データを表現する Object/Relational Mapping ツールである
  • 特徴
    • 複雑なクエリを簡単に作成し、その結果を操作できるようにする
    • メモリで作成された変更とデータベースと簡単に同期させる
    • トランザクションの ACID 特性を尊重する
    • 基本的な永続化ロジックがすでに記述された後で、パフォーマンスの最適化を可能にする

AssertJ

  • AssertJ
    • 豊富なアサーションのセットを持つ
    • 役立つエラーメッセージを提供し、テストコードの読みやすさを向上させる
  • さまざまなアサーションの提供
    • Guava
      • Multimap, Table, Optional, ByteSource など
    • Joda Time
      • DateTime, LocalDatime など
    • Neo4j
      • Node, Path, Relationship など

おわりに

いかがでしたでしょうか。Spring の開発を網羅したとはとても言えませんが、サーバーの起動から DB 接続、簡単なテストまで一通り実施しました。また周辺知識を調べるとともに、その範囲の深さを垣間見ることができました。Spring にはさまざまな機能が提供・搭載されており、その前葉を掴むにはかなりの時間がかかりそうです。

本当は Spring Boot リファレンス の内容も一通り読みたかったのですが、時間が足りませんでした。また次回読みたいと思います。

また余談として、当初はリポジトリ名を HitoroOhria/spring-boot-guides にしていました。途中で間違いに気づき HitoroOhria/spring-guides に修正しました。シェルの実行結果には一部修正前のログが残っています。

以上、読んでいただきありがとうございました。

参考文献

Discussion