Docker 環境と Java Spring Boot で最小 API を実装する：MySQL

# Docker 環境と Java Spring Boot で最小 API を実装する：MySQL

こんにちは、@studio_meowtoon です。今回は、WSL の Ubuntu 24.04 で Java Spring Boot Web アプリケーションを作成し、最小限の REST API を実装する方法を紹介します。
![spring-boot_and_mysql_on_docker.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/d0b6d1ee-c7f4-f578-c39d-bb811a6242ae.png)

## 目的

Windows 11 の Linux でクラウド開発します。

こちらから記事の一覧がご覧いただけます。

https://qiita.com/studio_meowtoon/items/6119ab05d8b13527efd8

## 実現すること

ローカル環境の Ubuntu の Docker 環境で、Dockerfile からビルドした Java Spring Boot Web サービスのカスタムコンテナーと MySQL データベースコンテナーを起動します。

**JARファイル形式のアプリをコンテナーとして起動**
```mermaid
flowchart LR
    terminal --> app[app.jar]
    subgraph Ubuntu
        subgraph Docker
            subgraph app-container[api-todo-spring-boot]
                subgraph JVM
                    subgraph app[app.jar]
                        tomcat
                    end
                end
            end
            subgraph db-container[mysql-todo]
                subgraph mysql
                    db_todo
                end
            end
            app[app.jar] <--> db_todo
        end
    end
style Ubuntu fill:#FFBCA4, stroke:#E0460F
style Docker fill:#EEFDFF, stroke:#74B9DC
style JVM fill:#DCFFDC, stroke:#2F872F
style app-container fill:#FFD7E4, stroke:#E13C73
style db-container fill:#FFD7E4, stroke:#E13C73
style app fill:#ECECFF, stroke:#9674DC
style tomcat fill:#FFFFDE, stroke:#B4AE43
style mysql fill:#FFFFDE, stroke:#B4AE43
```

**実行環境**

|要素|概要|
|:--|:--|
|terminal|ターミナル|
|Ubuntu|OS|
|Docker|コンテナー実行環境|

**REST API サービス コンテナー**

|要素|概要|
|:--|:--|
|api-todo-spring-boot|REST API サービス カスタムコンテナー|
|JVM|Java 実行環境|
|app.jar|Java アプリケーション|
|tomcat|Web サーバー|

**データベース コンテナー**

|要素|概要|
|:--|:--|
|mysql-todo|データベースコンテナー|
|mysql|DB サーバー|
|db_todo|データベース|

## 技術トピック

### Spring Boot とは？

<details>
<summary>こちらを展開してご覧いただけます。</summary>

### [Spring Boot (スプリングブート)](https://spring.pleiades.io/spring-boot/docs/2.7.8/reference/html/getting-started.html#getting-started)

:::note info
Spring Boot は Java アプリケーションの迅速な開発とデプロイをサポートし、一般的な開発作業や設定にかかる手間を軽減するためのツールや機能を提供するフレームワークです。

|キーワード|内容|
|:--|:--|
|簡易なセットアップ|Spring Boot は、設定を最小限に抑え、デフォルトの設定を提供することで、アプリケーションのセットアップを簡単にします。アノテーションや自動構成により、煩雑な設定作業を減少させます。|
|マイクロサービス向け|Spring Boot は、マイクロサービスアーキテクチャをサポートしており、単一のアプリケーションから複数の独立したサービスを構築しやすくなっています。|
|埋め込みサーバー|Web アプリケーションをビルドする際、Tomcat や Jetty などのウェブサーバーをアプリケーションに内蔵できます。これにより、独立して実行可能な JAR ファイルを生成できます。|
|自動設定|Spring Boot は、アプリケーションのクラスパスや使用されるライブラリに基づいて自動的に設定を行います。これにより、煩雑な設定ファイルの作成を減少させ、開発者が本質的なコードに集中できます。|
|プロダクション準備|Spring Boot は、プロダクション環境での運用に必要な機能を提供します。メトリクス、ヘルスチェック、外部設定など、アプリケーションの監視と管理を容易にします。|
|豊富なスターター|Spring Boot は、アプリケーション開発においてよく使用される依存関係をまとめた「スターター」を提供しています。これにより、特定の用途に適した設定を簡単に取り入れることができます。|
|カスタマイズ可能|Spring Boot は、デフォルトの動作をカスタマイズするためのオプションを提供します。これにより、特定のニーズや要件に合わせた調整が可能です。|

:::

</details>

## 開発環境

+ Windows 11 Home 23H2 を使用しています。

:::note info
[WSL](https://learn.microsoft.com/ja-jp/windows/wsl/about) の Ubuntu を操作しますので macOS の方も参考にして頂けます。  
:::

<details>

WSL (Microsoft Store アプリ版) [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/cd0c4a607d451dbf5430)

```sh
> wsl --version
WSL バージョン: 2.2.4.0
カーネル バージョン: 5.15.153.1-2
WSLg バージョン: 1.0.61
```

Ubuntu [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/cd0c4a607d451dbf5430)

```sh
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04 LTS
Release:        24.04
Codename:       noble
```

Java JDK [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/4d11e94a2389758759cd)

```sh
$ java -version
openjdk version "17.0.12" 2024-07-16
OpenJDK Runtime Environment (build 17.0.12+7-Ubuntu-1ubuntu224.04)
OpenJDK 64-Bit Server VM (build 17.0.12+7-Ubuntu-1ubuntu224.04, mixed mode, sharing)
```

Maven [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/f4cdc4039158153365fb)

```sh
$ mvn -version
Apache Maven 3.8.7
Maven home: /usr/share/maven
Java version: 17.0.12, vendor: Ubuntu, runtime: /usr/lib/jvm/java-17-openjdk-amd64
```

Docker [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/3bceee25ac67cc73ebbe)

```sh
$ docker --version
Docker version 27.0.3, build 7d4bcd8
```

</details>

:::note warn
この記事では基本的に **Ubuntu のターミナルで操作**を行います。**Vim を使用してコピペする方法**をはじめて学ぶ人のために、以下の記事で手順を紹介しています。ぜひ挑戦してみてください。
:::

https://qiita.com/studio_meowtoon/items/7771ad48676926e41463

## 作成する REST API の仕様

|エンドポイント|HTTPメソッド|説明|リクエストBody|レスポンスBody|
|:--|:--|:--|:--|:--|
|/todos         |GET   |すべての ToDo アイテムを取得します。|None         |ToDo アイテムの配列|
|/todos/complete|GET   |完了した ToDo アイテムを取得します。|None         |ToDo アイテムの配列|
|/todos/{id}    |GET   |ID で ToDo アイテムを取得します。   |None         |ToDo アイテム|
|/todos         |POST  |新しい ToDo アイテムを追加します。  |ToDo アイテム|ToDo アイテム|
|/todos/{id}    |PUT   |既存の ToDo アイテムを更新します。  |ToDo アイテム|None|
|/todos/{id}    |DELETE|ID で ToDo アイテムを削除します。   |None         |None|

## データベース コンテナーの起動

こちらの記事で、ToDo アプリ用の RDBMS データベースを作成し、Docker コンテナーとして起動する手順をご確認いただけます。

https://qiita.com/studio_meowtoon/items/ce1c853893388c106d22

データベース コンテナーが起動していることを確認します。

```sh
$ docker start mysql-todo
$ docker ps
CONTAINER ID   IMAGE        COMMAND                   CREATED        STATUS       PORTS                                                  NAMES
63a3acec271c   mysql-base   "docker-entrypoint.s…"   20 hours ago   Up 3 hours   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   mysql-todo
```

:::note alert
コンテナー間通信するために **net-todo** という Docker ネットワークをあらかじめ作成しています。ご注意ください。
:::

## REST API を実装する手順

### プロジェクトの作成

プロジェクトフォルダーを作成します。  
※ ~/tmp/restapi-spring-boot をプロジェクトフォルダーとします。

```sh
$ mkdir -p ~/tmp/restapi-spring-boot
$ cd ~/tmp/restapi-spring-boot
```

### Model クラスの作成

:::note alert
このシリーズの記事で、**RDBMS** と **NoSQL** の両方で**同じ API 操作**を行います。そのため、ID フィールドは数値型ではなく文字列として定義しています。統一性を保ちつつ異なるデータストアで動作することを目指しています。ご了承ください。
:::

Todo エンティティクラスを作成します。

```sh
$ mkdir -p src/main/java/com/example/springboot/model
$ vim src/main/java/com/example/springboot/model/Todo.java
```

ファイルの内容

```java:model/Todo.java
package com.example.springboot.model;

import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;

import lombok.Data;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;

// ToDo エンティティを表すクラス
@Data
@Entity @Table(name="todos")
public class Todo {
    // RDBMS 本来の int 型のキー：API 入出力の JSON にはマッピングしない
    @JsonIgnore
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Id @Column(name="id", unique=true) Long _rdbms_id;

    // string 型の ID を定義：RDBMS 側にはマッピングしない
    @Transient String id;
    public String getId() { return Long.toString(_rdbms_id); }
    public void setId(String id) { _rdbms_id = Long.parseLong(id); }

    @JsonProperty("content")
    @Column(name="content") String content;

    @JsonProperty("created_date")
    @Column(name="created_date") 
    @Temporal(TemporalType.TIMESTAMP) Date createdDate;

    @JsonProperty("completed_date")
    @Column(name="completed_date") 
    @Temporal(TemporalType.TIMESTAMP) Date completedDate;
}
```

<details>
<summary>説明を開きます。</summary>

|要素|説明|
|:--|:--|
|@Data|Lombok ライブラリを使用する際に使用されるアノテーションです。Lombok は、Javaクラスの冗長なコードを削減するためのアノテーションベースのライブラリです。|
|@Entity|クラスが JPA エンティティ (データベース内のテーブルにマッピングされるオブジェクト) であることを示すアノテーションです。|
|@Table(name="todos")|エンティティが todos テーブルにマッピングされることを示すアノテーションです。テーブル名を指定しています。|
|@Id|プライマリキー(主キー)フィールドであることを示すアノテーションです。|
|@GeneratedValue(strategy=GenerationType.IDENTITY)|プライマリキーの値が自動的に生成される方法を指定するアノテーションです。IDENTITY 戦略は、データベースの自動増分カラムを使用してキーを生成することを示します。|
|@Column(name="id", unique=true)|フィールドがデータベースのカラムにマッピングされることを示すアノテーションです。unique=true はカラムの値が一意であることを指定しています。|
|@Transient|フィールドがデータベースのカラムにマッピングされないことを示すアノテーションです。トランジェントなフィールドです。|
|@Temporal(TemporalType.TIMESTAMP)|フィールドが日付と時刻を持つことを示すアノテーションです。TIMESTAMP型を使用します。|
|@JsonProperty("content")|JSON シリアライズ時にフィールド名を指定するためのアノテーションです。|
|@JsonIgnore|JSONシリアライズやデシリアライズする際に、特定のフィールドを無視するためのアノテーションです。|

:::note info
これらの要素を組み合わせることで、Todo クラスは JPA エンティティとして機能し、データベース内の todos テーブルと対応するオブジェクトとして使用されます。
:::

</details>

TodoRepository リポジトリインタフェースを作成します。

```sh
$ mkdir -p src/main/java/com/example/springboot/model
$ vim src/main/java/com/example/springboot/model/TodoRepository.java
```

ファイルの内容

```java:model/TodoRepository.java
package com.example.springboot.model;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

// ToDo エンティティに対する Repository インタフェース
public interface TodoRepository extends JpaRepository<Todo, Long> {
    List<Todo> findByCompletedDateIsNotNull();
}
```

<details>
<summary>説明を開きます。</summary>

:::note info
Spring Data JPA を使用してデータベースとのやり取りを行うためのリポジトリインタフェースです。このインタフェースを作成することで、基本的な CRUD 操作 (Create, Read, Update, Delete) や特定の条件でのデータ取得などを簡単に実装できるようになります。
:::

|要素|説明|
|:--|:--|
|TodoRepository インタフェースの宣言|インタフェース TodoRepository は JpaRepository インタフェースを拡張しています。JpaRepository は Spring Data JPA が提供する基本的なリポジトリインタフェースで、基本的な CRUD 操作が含まれています。|
|ジェネリクス型パラメーター|JpaRepository を拡張する際、ジェネリクス型パラメーターにエンティティクラスの型 (Todo) と、エンティティの主キーの型 (Long) を指定します。|
|findByCompletedDateIsNotNull メソッドの宣言|このメソッドは completedDate フィールドが非 null (つまり、完了したタスク) の Todo エンティティを取得するためのメソッドです。findByCompletedDateIsNotNull という命名規則にしたがっているため、Spring Data JPA が自動的にクエリを生成し、対応するデータベース操作を行います。|

</details>

### Application、Controller クラスの作成

Application クラスを作成します。

```sh
$ vim src/main/java/com/example/springboot/Application.java
```

ファイルの内容

```java:Application.java
package com.example.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.scheduling.annotation.EnableAsync;

@EntityScan("com.example.springboot.*")
@EnableJpaRepositories("com.example.springboot.*")
@SpringBootApplication
@EnableAsync
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
```

<details>
<summary>説明を開きます。</summary>

|要素|説明|
|:--|:--|
|@EntityScan("com.example.springboot.*")|エンティティクラスのパッケージを指定しています。指定したパッケージ内に存在するエンティティクラスがスキャンされ、データベースとのマッピングが行われます。|
|@EnableJpaRepositories("com.example.springboot.*")|Spring Data JPA リポジトリのパッケージを指定しています。指定したパッケージ内のリポジトリが有効化され、データベース操作のためのメソッドが提供されます。|
|@SpringBootApplication|Spring Boot アプリケーションのエントリーポイントとして指定されています。このアノテーションにより、Spring Boot アプリケーションの設定やコンポーネントスキャンが行われます。|
|@EnableAsync|Spring Framework のアプリケーションで非同期処理をサポートするためのアノテーションです。|

</details>

TodoController クラスを作成します。

```sh
$ vim src/main/java/com/example/springboot/TodoController.java
```

ファイルの内容

```java:TodoController.java
package com.example.springboot;

import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.example.springboot.model.Todo;
import com.example.springboot.model.TodoRepository;

@CrossOrigin( // CORS 設定：適切に修正してください。
    origins = "*", 
    methods = { RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE }
)
@RestController
@RequestMapping("/todos")
public class TodoController {

    private final TodoRepository _todoRepository;

    public TodoController(TodoRepository todoRepository) {
        _todoRepository = todoRepository;
    }

    // すべての ToDo アイテムを取得します。
    @GetMapping
    @Async public CompletableFuture<List<Todo>> getAllTodos() {
        List<Todo> todos = _todoRepository.findAll();
        return CompletableFuture.completedFuture(todos);
    }

    // 完了した ToDo アイテムを取得します。
    @GetMapping("/complete")
    @Async public CompletableFuture<List<Todo>> getCompleteTodos() {
        List<Todo> todos = _todoRepository.findByCompletedDateIsNotNull();
        return CompletableFuture.completedFuture(todos);
    }

    // ID で ToDo アイテムを取得します。
    @GetMapping("/{id}")
    @Async public CompletableFuture<Optional<Todo>> getTodo(@PathVariable String id) {
        Optional<Todo> todo = _todoRepository.findById(Long.parseLong(id));
        return CompletableFuture.completedFuture(todo);
    }

    // 新しい ToDo アイテムを追加します。
    @PostMapping
    @Async public CompletableFuture<Todo> createTodo(@RequestBody Todo todo) {
        todo.setCreatedDate(new Date());
        return CompletableFuture.completedFuture(_todoRepository.save(todo));
    }

    // 既存の ToDo アイテムを更新します。
    @PutMapping("/{id}")
    @Async public CompletableFuture<Todo> updateTodo(@PathVariable String id, @RequestBody Todo todo) {
        Optional<Todo> exist = _todoRepository.findById(Long.parseLong(id));
        if (exist.isPresent()) {
            Todo target = exist.get();
            target.setContent(todo.getContent());
            target.setCompletedDate(todo.getCompletedDate());
            return CompletableFuture.completedFuture(_todoRepository.save(target));
        }
        return CompletableFuture.completedFuture(null);
    }

    // ID で ToDo アイテムを削除します。
    @DeleteMapping("/{id}")
    @Async public CompletableFuture<Void> deleteTodo(@PathVariable String id) {
        _todoRepository.deleteById(Long.parseLong(id));
        return CompletableFuture.completedFuture(null);
    }
}
```

<details>
<summary>説明を開きます。</summary>

|要素|説明|
|:--|:--|
|@CrossOrigin|CORS (Cross-Origin Resource Sharing) の設定を行っています。異なるオリジンからのリクエストを許可するために、オリジンと許可する HTTP メソッドを指定しています。|
|RequestMethod|特定の HTTP メソッドに対してコントローラーのメソッドをマッピングするために使用されます。|
|@RestController|コントローラクラスであることを示すアノテーションです。HTTP リクエストを処理するエンドポイントを提供するために使用されます。|
|@RequestMapping("/todos")|クラスレベルでのリクエストマッピングを指定しています。このコントローラー内のエンドポイントは、/todos のパスにマッピングされます。|
|@GetMapping|HTTP GET リクエストに対するハンドラーメソッドを指定しています。/todos エンドポイントでリクエストがあった際に、getAllTodos() メソッドが実行されます。|
|@PostMapping|HTTP POST リクエストに対するハンドラーメソッドを指定しています。新しい ToDo アイテムを追加する際に、createTodo() メソッドが実行されます。|
|@PutMapping|HTTP PUT リクエストに対するハンドラーメソッドを指定しています。既存の ToDo アイテムを更新する際に、updateTodo() メソッドが実行されます。|
|@DeleteMapping|HTTP DELETE リクエストに対するハンドラーメソッドを指定しています。ToDo アイテムを削除する際に、deleteTodo() メソッドが実行されます。|
|@RequestBody|HTTP リクエストのボディ部分から送信されたデータを Java オブジェクトにマッピングするために使用されます。コントローラー内のメソッドの引数として使用され、リクエストのボディ部分のデータを Java オブジェクトに変換して提供します。|
|@PathVariable|URL パス内の変数部分をメソッドの引数として受け取るために使用されます。たとえば、/todos/{id} のような URL で、@PathVariable アノテーションを使用して {id} 部分の値をメソッドの引数として取得できます。|
|@Async|非同期メソッドを宣言する際に使用されます。Spring Framework によって提供される非同期処理の機能を利用して、メソッドが非同期に実行されることを示します。|
|CompletableFuture|Java の標準ライブラリで提供されるクラスで、非同期プログラミングや非同期処理の結果の取り扱いを容易にするための仕組みです。|

</details>

### Spring Boot の設定ファイルを追加

**application.properties** ファイルを作成します。

```
$ mkdir -p src/main/resources
$ vim src/main/resources/application.properties
```

ファイルの内容

```sh:application.properties
spring.datasource.url=jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASSWORD}
spring.jpa.show-sql=false
```

<details>
<summary>説明を開きます。</summary>

|要素|説明|
|:--|:--|
|spring.datasource.url|データベースへの接続 URL を指定します。＄{DB_HOST}、＄{DB_HOST} と ＄{DB_PORT} と ＄{DB_NAME} などのプレースホルダーを使用して、実際の値が環境変数から取得されます。|
|spring.datasource.username|データベースへの接続に使用するユーザー名を指定します。＄{DB_USER} プレースホルダーを使用して、実際の値が環境変数から取得されます。|
|spring.datasource.password|データベースへの接続に使用するパスワードを指定します。＄{DB_PASSWORD} プレースホルダーを使用して、実際の値が環境変数から取得されます。|
|spring.jpa.show-sql|JPA によるクエリのログを表示するかどうかを指定します。この例では false に設定されており、クエリのログは表示されません。|

</details>

### pom.xml の作成

pom.xml ファイルを作成します。

```sh
$ vim pom.xml
```

ファイルの内容

```xml:pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.15</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>restapi-spring-boot</artifactId>
    <version>1.0</version>
    <name>restapi-spring-boot</name>
    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>javax.persistence-api</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    <build>
        <finalName>app</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
```

<details>
<summary>説明を開きます。</summary>

|要素|説明|
|:--|:--|
|modelVersion|Maven モデルのバージョンを指定します。通常は 4.0.0 を指定します。|
|parent|このプロジェクトの親プロジェクトを指定します。spring-boot-starter-parent は、Spring Boot プロジェクトの親プロジェクトであり、バージョン情報やビルド設定が事前に設定されています。|
|groupId, artifactId, version, name|プロジェクトの Maven のグループ ID、アーティファクト ID、バージョン、プロジェクト名を指定します。|
|properties|プロジェクト全体で使用されるプロパティを指定します。たとえば、Java バージョンやプロジェクトのエンコーディングなどがここで設定されています。|
|dependencies|プロジェクトが依存するライブラリ(依存関係)を指定します。各 <dependency> タグでライブラリの情報を指定します。このプロジェクトでは、Spring Boot Web 開発に必要な依存関係 (spring-boot-starter-web、spring-boot-starter-data-jpa) や JPA 関連のクラスやインターフェイス (javax.persistence-api)、MySQL データベースとの接続に関する依存関係 (mysql-connector-java)、Lombok ライブラリ(lombok) などが指定されています。|
|build|ビルドに関連する設定を指定します。この中には、プロジェクトのファイナル名やビルドプラグインの設定が含まれています。このプロジェクトでは、spring-boot-maven-plugin を使用して Spring Boot アプリケーションをビルドするための設定が指定されています。|

</details>

### アプリの起動

環境変数を作成します。

```sh
export DB_HOST=localhost
export DB_PORT=3306
export DB_NAME=db_todo
export DB_USER=root
export DB_PASSWORD=password
```

:::note alert
この環境変数が**一時的なもの**であることに注意してください。
:::

アプリを起動します。  
※ アプリを停止するときは ctrl + C を押します。

:::note warn
この例では、アプリがリッスンする**ポート番号**を**5000**に設定して起動しています。
:::

```sh
$ mvn spring-boot:run \
    -Dspring-boot.run.arguments="--server.port=5000"
```

:::note info
ここまでの手順で、**Ubuntu** でアプリを起動することができました。
:::

## アプリの動作確認

別ターミナルから curl コマンドで確認します。

:::note warn
必要な場合、jq をインストールします。

```sh
$ sudo apt update
$ sudo apt install jq
```

:::

#### GET: /todos エンドポイントの動作確認

すべての ToDo アイテムを取得します。

```sh
$ curl -s http://localhost:5000/todos | jq '.'
```

レスポンス

```sh
[
// 省略
  {
    "id": "3",
    "content": "運動する",
    "created_date": "2024-08-11T11:02:02.000+00:00",
    "completed_date": "2024-08-11T11:02:02.000+00:00"
  },
  {
    "id": "4",
    "content": "本を読む",
    "created_date": "2024-08-11T11:02:02.000+00:00",
    "completed_date": null
  },
  {
    "id": "5",
    "content": "請求書を支払う",
    "created_date": "2024-08-11T11:02:02.000+00:00",
    "completed_date": null
  },
// 省略
```

:::note info
すべての ToDo アイテムを取得できています。
::: 

#### GET: /todos/complete エンドポイントの動作確認

完了した ToDo アイテムを取得します。

```sh
$ curl -s http://localhost:5000/todos/complete | jq '.'
```

レスポンス

```sh
[
  {
    "id": "1",
    "content": "食材を買う",
    "created_date": "2024-08-11T11:02:02.000+00:00",
    "completed_date": "2024-08-11T11:02:02.000+00:00"
  },
  {
    "id": "2",
    "content": "報告書を仕上げる",
    "created_date": "2024-08-11T11:02:02.000+00:00",
    "completed_date": "2024-08-11T11:02:02.000+00:00"
  },
  {
    "id": "3",
    "content": "運動する",
    "created_date": "2024-08-11T11:02:02.000+00:00",
    "completed_date": "2024-08-11T11:02:02.000+00:00"
  }
]
```

:::note info
完了した ToDo アイテムを取得できています。
:::

#### GET: /todos/{id} エンドポイントの動作確認

ID で ToDo アイテムを取得します。

```sh
$ curl -s http://localhost:5000/todos/8 | jq '.'
```

レスポンス

```sh
{
  "id": "8",
  "content": "コードを書く",
  "created_date": "2024-08-11T11:02:02.000+00:00",
  "completed_date": null
}
```

:::note info
ID で ToDo アイテムを取得できています。
::: 

#### POST: /todos エンドポイントの動作確認

新しい ToDo アイテムを追加します。

```sh
$ curl -s -X POST http://localhost:5000/todos \
    -H 'Content-Type: application/json; charset=utf-8' \
    -d \
'{
    "content": "昼寝をする"
}' | jq '.'
```

レスポンス

```sh
{
  "id": "9",
  "content": "昼寝をする",
  "created_date": "2024-08-12T03:17:51.572+00:00",
  "completed_date": null
}
```

:::note info
新しい ToDo アイテムが追加できています。
:::

#### PUT: /todos/{id} エンドポイントの動作確認

既存の ToDo アイテムを更新します。

```sh
$ curl -s -X PUT http://localhost:5000/todos/9 \
    -H 'Content-Type: application/json; charset=utf-8' \
    -d \
'{
    "content": "窓を開ける"
}' | jq '.'
```

レスポンス

```sh
{
  "id": "9",
  "content": "窓を開ける",
  "created_date": "2024-08-12T03:17:52.000+00:00",
  "completed_date": null
}
```

:::note warn
対象のレコードが更新されています。
:::

#### DELETE: /todos/{id} エンドポイントの動作確認

ID で ToDo アイテムを削除します。

```sh
$ curl -s -X DELETE http://localhost:5000/todos/9 | jq '.'
```

レスポンス

```sh
※なし
```

:::note warn
レスポンスはなしですが、DB のテーブルを確認すると対象のレコードが削除されています。
:::

:::note info
ここまでの手順で、最小限の **CRUD 操作**を行う **REST API** をアプリに実装できました。
:::

### コンテナーイメージの作成

Dockerfile を作成します。

```sh
$ vim Dockerfile
```

ファイルの内容

```Dockerfile:Dockerfile
# build the app.
FROM openjdk:17-jdk-slim as build-env

# set the working dir.
WORKDIR /app

# install build tools and libraries.
RUN apt-get update && apt-get install -y maven

# copy source code to the working dir.
COPY . .

# build the app.
RUN mvn clean package

# set up the container.
FROM debian:12-slim

# set the working dir.
WORKDIR /app

# install the openjdk-17-jre-headless and clean up unnecessary files.
RUN apt-get update && \
    apt-get install -y --no-install-recommends openjdk-17-jre-headless && \
    apt-get autoremove -y && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

# set environment variables.
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64/jre
ENV PATH=$PATH:$JAVA_HOME/bin

# copy the built app from the build-env.
COPY --from=build-env /app/target/*.jar app.jar

# expose the port.
EXPOSE 8080

# command to run the app using java.
ENTRYPOINT ["java","-jar","app.jar"]
```

<details>
<summary>説明を開きます。</summary>

|要素|説明|
|:--|:--|
|FROM openjdk:17-jdk-slim as build-env|ビルド環境のベースイメージとして、OpenJDK 17 の JDK 版を使用します。|
|WORKDIR /app|作業ディレクトリを /app に設定します。|
|RUN apt-get update && apt-get install -y maven|ビルドに必要なパッケージである Maven をインストールします。apt-get update でパッケージ情報を更新し、apt-get install -y maven で Maven をインストールします。|
|COPY . .|ソースコードと pom.xml が存在するディレクトリの内容を、作業ディレクトリにコピーします。|
|RUN mvn clean package|Maven を使用して、アプリケーションをビルドします。mvn clean package コマンドは、プロジェクトをクリーンにし、パッケージをビルドします。|
|FROM debian:12-slim|実行用のコンテナーのベースイメージとして、debian 12 の Slim 版を使用します。|
|WORKDIR /app|作業ディレクトリを /app に設定します。|
|RUN apt-get update && apt-get install -y --no-install-recommends openjdk-17-jre-headless && apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*|Java 17 の JRE (ヘッドレス版) をインストールします。--no-install-recommends オプションで推奨パッケージをインストールしないことを指定します。その後、不要なファイルをクリーンアップしてイメージサイズを最小限にします。|
|ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64/jre|環境変数 JAVA_HOME を設定します。これはJavaのインストールディレクトリを示します。|
|ENV PATH=＄PATH:＄JAVA_HOME/bin|PATH 環境変数に Java の実行可能ファイルパスを追加します。|
|COPY --from=build-env /app/target/*.jar app.jar|build-env ステージでビルドされた JAR ファイルを、実行用のコンテナー内の作業ディレクトリにコピーします。|
|EXPOSE 8080|コンテナーがポート 8080 を外部に公開することを示します。|
|ENTRYPOINT ["java","-jar","app.jar"]|コンテナー起動時に実行されるコマンドを指定します。JAR ファイルを Java で実行することで、アプリケーションが実行されます。|

</details>

Docker デーモンを起動します。

```sh
$ sudo service docker start
```

:::note warn
Docker 環境をお持ちでない場合は、以下の関連記事から Docker Engine のインストール手順をご確認いただけます。
:::

https://qiita.com/studio_meowtoon/items/3bceee25ac67cc73ebbe

コンテナーイメージをビルドします。

```sh
$ docker build \
    --no-cache \
    --tag api-todo-spring-boot:latest .
```

コンテナーイメージを確認します。

```sh
$ docker images | grep api-todo-spring-boot
api-todo-spring-boot    latest    2ac3a8c681cd    17 seconds ago    340MB
```

:::note info
ここまでの手順で、ローカル環境の **Docker** にアプリの**カスタムコンテナーイメージ**をビルドすることができました。
:::

### コンテナーを起動

ローカルでコンテナーを起動します。
※ コンテナーを停止するときは ctrl + C を押します。

:::note alert
コンテナー間通信するために **net-todo** という Docker ネットワークをあらかじめ作成しています。ご注意ください。
:::

```sh
$ docker run --rm \
    --publish 5000:8080 \
    --name api-local \
    --net net-todo \
    --env DB_HOST=mysql-todo \
    --env DB_PORT=3306 \
    --env DB_NAME=db_todo \
    --env DB_USER=root \
    --env DB_PASSWORD=password \
    api-todo-spring-boot
```

:::note info
ここまでの手順で、ローカル環境の **Docker** でアプリの**カスタムコンテナー**を起動することができました。
:::

### コンテナーの動作確認

別ターミナルから curl コマンドで確認します。
※ ID で ToDo アイテムを取得します。

```sh
$ curl -s http://localhost:5000/todos/8 | jq '.'
```

レスポンス

```sh
{
  "id": "8",
  "content": "コードを書く",
  "created_date": "2024-08-11T11:02:02.000+00:00",
  "completed_date": null
}
```

:::note info
ここまでの手順で、ターミナルにレスポンスが表示され、**JSON データ**を取得することができました。
:::

コンテナーの状態を確認してみます。

```sh
$ docker ps
CONTAINER ID   IMAGE                  COMMAND                   CREATED          STATUS             PORTS                                                  NAMES
f3641763145b   api-todo-spring-boot   "java -jar app.jar"       37 seconds ago   Up 37 seconds      0.0.0.0:5000->8080/tcp, :::5000->8080/tcp              api-local
5eb600ed3d54   mysql-base             "docker-entrypoint.s…"   16 hours ago     Up About an hour   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   mysql-todo
```

### コンテナーに接続

別ターミナルからコンテナーに接続します。

```sh
$ docker exec -it api-local /bin/bash
```

コンテナー接続後にディレクトリを確認します。
※ コンテナーから出るときは ctrl + D を押します。

```
# pwd
/app
# ls -lah
total 40M
drwxr-xr-x 1 root root 4.0K Aug 12 03:25 .
drwxr-xr-x 1 root root 4.0K Aug 12 03:26 ..
-rw-r--r-- 1 root root  40M Aug 12 03:25 app.jar
```

top コマンドで状況を確認します。

```
# apt update
# apt install procps
# top
top - 03:28:59 up  1:43,  0 user,  load average: 0.05, 0.08, 0.03
Tasks:   3 total,   1 running,   2 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.1 us,  0.1 sy,  0.0 ni, 99.7 id,  0.0 wa,  0.0 hi,  0.1 si,  0.0 st
MiB Mem :  15949.2 total,  12745.8 free,   1812.8 used,   1689.1 buff/cache
MiB Swap:   4096.0 total,   4096.0 free,      0.0 used.  14136.4 avail Mem

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
      1 root      20   0 9082716 362312  22892 S   0.3   2.2   0:15.57 java
     61 root      20   0    4188   3480   2972 S   0.0   0.0   0:00.01 bash
    251 root      20   0    8560   4628   2756 R   0.0   0.0   0:00.00 top
```

コンテナーの情報を表示してみます。

```cs
# cat /etc/*-release
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
```

:::note warn
このコンテナーは Debian GNU/Linux をベースに作成されています。つまり、Debian GNU/Linux と同じように扱うことができます。
:::

## おまけ：Swagger 装備

### ライブラリの追加

[springdoc-openapi](https://github.com/springdoc/springdoc-openapi) ライブラリを pom.xml に追記します。

```sh
$ vim pom.xml
```

ファイルの内容

```diff_java:pom.xml ※抜粋
<!-- 省略 -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
+   <dependency>
+       <groupId>org.springdoc</groupId>
+       <artifactId>springdoc-openapi-ui</artifactId>
+       <version>1.7.0</version>
+   </dependency>
    <dependency>
        <groupId>javax.persistence</groupId>
        <artifactId>javax.persistence-api</artifactId>
        <version>2.2</version>
    </dependency>
<!-- 省略 -->
```

### Swagger を確認

Web ブラウザで Swagger の URL を確認します。

```sh
http://localhost:5000/swagger-ui/index.html
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/7a1c516d-3b9b-94cf-9141-3179cf63fe6a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/e3a84a60-80b6-f4e1-5ac8-ef18f5f5f970.png)

:::note info
これらのツールを活用することにより、より手軽に API を操作できます。
:::

## まとめ

WSL Ubuntu の Docker 環境で、Java Spring Boot の最小 API を実装することができました。

:::note alert
この記事の実装例は1つのアプローチに過ぎず、必ずしも正しい方法とは限りません。他にも多様な方法がありますので、さまざまな情報を照らし合わせて検討してみてください。
:::

どうでしたか？　WSL Ubuntu で、Java Spring Boot Web アプリケーションを手軽に起動できます。ぜひお試しください。今後も Spring の開発環境などを紹介しますので、ぜひお楽しみにしてください。

## 推奨コンテンツ

https://qiita.com/studio_meowtoon/items/17ec4af37a8951b79ba7

https://qiita.com/studio_meowtoon/items/9d91e53d6ff4e5753b05