【作業ログ+解説】Spring 公式ガイド
はじめに
前回は Spring Boot の公式チュートリアルを実施しました。
今回は Spring ガイドからいくつか実施していきます。目標は、標準的な構成の Web アプリケーションの開発を体験することです。
Spring ガイド
ネクストステップとして以下がちょうど良さそうだ。
- REST API の作成
- JPA で MySQL データアクセス
- MockMvc と @MockBean で Web レイヤーテスト
リポジトリは以下に作成した。
REST API の作成
まずは基本的な REST API の作成からやっていこう。公式のサンプルリポジトリはこれだ。
Spring Initializr から開始
今回は Spring Initializr を使用してみよう。
Spring Boot プロジェクトの作成 - Spring Initializr と Spring 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 サーバーとしてのコードは追加されていないようだ。
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
メソッドが定義されている。このメソッド名は何だろう?メタ的なメソッド名だ。いったん置いておこう。
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 セクションで詳細を調べた。
package com.example.restservice;
public record Greeting(long id, String content) {
}
リソースコントローラーを作成する
/greeting
にアクセスしたらレスポンスを返すコントローラーを定義する。
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 により、すでにアプリケーションクラスは作成されている。
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 の登録、追加の構成クラスのインポートを有効にする
-
- 以下の3つのアノテーションを付与する
アプリケーションを実行してみよう。
❯ ./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 へアクセスする方法を見ていこう。公式のサンプルリポジトリはこれだ。
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.java
と TestcontainersConfiguration.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
ビルドファイルは以下の通りだ。特筆すべき点はなさそうだ。
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
ファイルも追加されている。中身をみてみよう。
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 を確認しよう。
最終的にこのようなファイルとなった。
services:
mysql:
image: 'mysql:9.4'
environment:
- 'MYSQL_DATABASE=mydatabase'
- 'MYSQL_ROOT_PASSWORD=verysecret'
- 'MYSQL_USER=myuser'
- 'MYSQL_PASSWORD=secret'
ports:
- '3306'
@Entity
モデルを作成する
エンティティモデルを作成しよう。
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 にクラスのテーブルを作成するように指示する
リポジトリを作成する
ユーザーレコードを操作するリポジトリを作成する。
package com.example.accessingdatamysql;
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<User, Integer> {
}
コード量が少なくて驚く。よもやクラスを継承するだけとは。裏側で一体どのような黒魔術が使われているのだろう。
CrudRepository
のジェネリクスの第二パラメーターは、テーブルの id
の型を表現しているらしい。
また、Spring により このインターフェースは userRepository
という Bean に自動的に実装されるらしい。先頭を小文字にすることでシンボル定義の競合を回避している。Bean というものが何物かわからないが、今は先に進もう。
コントローラーの作成
アプリケーションの HTTP リクエストを処理するコントローラーを作成しよう。
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 により作成されているはずだ。
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
@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
ファイルを編集する。
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
を修正しよう。
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 への接続には成功していそうだ。
アプリケーションの構築
アプリケーションを構築する方法が紹介されていた。詳細は割愛する。
- JAR ファイルのビルドと実行
- Cloud Native Buildpacks を使用して Docker コンテナーを構築して実行する
- ネイティブイメージの構築と実行
- Cloud Native Buildpacks を使用したネイティブイメージコンテナーの構築と実行
MockMvc と @MockBean で Web レイヤーテスト
最後に、Spring アプリケーションを構築し、JUnit でテストする方法を見ていこう。公式のサンプルリポジトリはこれだ。
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
シンプルなアプリケーションを作成する
/
にアクセスしたら固定文を返すコントローラーを追加しよう。
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 により作成されている。
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 によりすでに用意されている。
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 アプリケーションコンテキストを開始する
- Spring Boot にメイン構成クラス(たとえば
-
@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 されることを確認するテストだ。
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()
に変更し、テストが落ちることを確認してみよう。
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 リクエストのテスト
サーバーのテストを書いてみよう。
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");
}
}
-
@SpringBootTest.WebEnvironment
- Web 環境のモード
- デフォルトは
MOCK
-
@LocalServerPort
- 実行時に割り当てられた HTTP サーバーポートを挿入する
テストを実行してみよう。
❯ ./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 リクエストのテスト
今度はサーバーを直接起動するのではなく、サーバーをモックしたテストを書いてみよう。
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
依存関係ありの対象をテスト
サービスを呼び出すコントローラーのように、依存関係がある場合、どのようにテストできるのか見てみよう。
package com.example.testingweb;
import org.springframework.stereotype.Service;
@Service
public class GreetingService {
public String greet() {
return "Hello World";
}
}
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 により、依存関係が解決されて注入される
-
実際にどのようにテストするのか見てみよう。
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
- レコードクラス
- プレーンデータ集約をモデル化することに役立つ
- レコード宣言は、ヘッダーにその内容の説明を指定する
- 効果
- アクセサ、コンストラクタ、
equals
、hashCode
、toString
メソッドが自動的に作成される - レコードのフィールドは
final
である- このクラスが単純な「データキャリア」として機能することを意図している
- アクセサ、コンストラクタ、
record Rectangle(double length, double width) { }
JPA
- Java Persistence API
- オブジェクトリレーショナルマッピングの POJO Persipence モデルを提供する
以下の記事も読んでみたが、よくわかっていない。
- Introduction to the Java Persistence API - The Java EE 6 Tutorial
- What is JPA? Introduction to Java persistence | InfoWorld
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 など
- Guava
おわりに
いかがでしたでしょうか。Spring の開発を網羅したとはとても言えませんが、サーバーの起動から DB 接続、簡単なテストまで一通り実施しました。また周辺知識を調べるとともに、その範囲の深さを垣間見ることができました。Spring にはさまざまな機能が提供・搭載されており、その前葉を掴むにはかなりの時間がかかりそうです。
本当は Spring Boot リファレンス の内容も一通り読みたかったのですが、時間が足りませんでした。また次回読みたいと思います。
また余談として、当初はリポジトリ名を HitoroOhria/spring-boot-guides
にしていました。途中で間違いに気づき HitoroOhria/spring-guides
に修正しました。シェルの実行結果には一部修正前のログが残っています。
以上、読んでいただきありがとうございました。
Discussion