1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

やってみるSpring Boot

1
Last updated at Posted at 2026-10-12

前の記事「やってみるNuxt」で作った ToDoリストの裏側を、Spring Boot1 で作り直しました。画面は Nuxt のままです。API の2本(一覧を返す GET と、一覧を丸ごと置き換える PUT)を Java へ移し、データベースに保存するところまで足しました。最後は、できた jar を1本だけ AWS の EC22 に送って動かしています。

私が Java でサーバー側を書いていたのは20年ほど前です。そのころは Tomcat を別に入れ、war を置いて、起動を待っていました。

先に、やってみて分かったことを3つ書きます。

# 分かったこと
1 Tomcat を入れていません。できた jar(52MB)の中に tomcat-embed-core-11.0.24.jar が入っていて、java -jar 1本で起動します
2 SQL を1行も書いていません。表を作る SQL も、読み書きの SQL も、Spring が作って実行しています
3 iMac で作った jar を作り直さずに EC2 へ送ったら、そのまま動きました。EC2 に入れたのは Java だけです

この記事を読むと明日できること

start.spring.io のテンプレートから、JSON を返す API を Spring Boot で作り、H23 に保存して、できた jar を EC2 で動かせます。Mac 全体の設定は変えずに試せます。

想定している読者は、20年ほど前に Servlet・JSP・JDBC で Web アプリを書いていて、Spring Boot を書いてみたい人です。Nuxt の画面を使うのは3章と4-3 だけです。前の記事を読んでいない人は、そこを飛ばしても curl で5章まで追えます。新しい書き方が出るたびに、昔の書き方と表で並べました。

前提を書いておきます。

  • iMac(Intel)の macOS 13 で動かしました。始める前、この iMac に Java は入っていませんでした(java -version は Unable to locate a Java Runtime)。Maven も入っていません
  • 入った版は Java 17.0.20.1(Temurin4)、Spring Boot 4.1.1、Tomcat 11.0.24、Hibernate5 7.4.5、H2 2.4.240 です
  • 画面の側は「やってみるNuxt」で作った nuxt-study(Nuxt6 のプロジェクト。git で管理している)をそのまま使います。Node.js と npm は前の記事で入れたものです
  • 6章の EC2 だけお金がかかります。AWS のアカウントと、認証を済ませた aws コマンド、Terraform7 が要ります。ssh の鍵 aws-study-key は、前の記事「はじめてのEC2(3/3)」で aws ec2 create-key-pair で作ったものを使いました
  • 専門用語の説明は巻末にまとめました。本文の脚注番号を押すと説明へ飛べます

作ったもの

Chrome で開く画面は、前の記事と同じ ToDoリストです。違うのは、一覧の置き場所が Nuxt のメモリから Java の先のデータベースに替わったことです。画面を開いてから、1件足すまでを図にしました。

前の記事で作ったのは、画面と、Nuxt の server/api/ にある API の2本です。この記事では、その2本の中身を Java に置き換えました。Chrome から見た URL は前の記事と変わりません。Nuxt が受け取ったリクエストを、そのまま Java へ渡しています。

最後に残った、自分で書いた Java のファイルは3本です。途中の4章で TodoStore.java も作りましたが、5章で消しました。

todo-api/
├── pom.xml                         … 使う部品の一覧(2か所直した)
├── mvnw                            … Maven を取ってきて動かすスクリプト(最初から入っているまま)
└── src/main/
    ├── java/com/triponte/todoapi/
    │   ├── TodoApiApplication.java … main メソッド(最初から入っているまま)
    │   ├── TodoController.java     … GET と PUT を受ける
    │   ├── TodoRepository.java     … 表を読み書きする
    │   └── Todo.java               … 1件の形
    └── resources/
        └── application.properties  … 待ち受けとデータベースの設定

Nuxt の側は、server/api/ の2本と nuxt.config.ts を少しだけ直しました。

作った順番は次の6つです。

# やったこと 覚えた書き方
1 Java を Mac 全体を変えずに置く JAVA_HOME
2 プロジェクトを作り、GET で固定の2件を返す @RestController・@GetMapping・./mvnw
3 Nuxt の $fetch の送り先を Java にする runtimeConfig・環境変数
4 PUT を足す(止めると消える) @RequestBody・@Component
5 H2 に保存する(止めても残る) @Entity・JpaRepository・@Transactional
6 jar 1本を EC2 で動かす scp・ssh のトンネル

1. Java を Mac 全体を変えずに置く

Java は Temurin 17 を使いました。Spring Boot 4 は Java 17 以上が要ります。

Homebrew は使わず、公式の tar.gz を ~/.local/jdk/ に展開しただけにしました。前に Homebrew で別のものを入れたとき、関係ない Python まで入れ替わったことがあったからです。~/Library/Java/JavaVirtualMachines/ に置くと Mac 全体の java が変わるので、そこも避けました。

ターミナルで次を実行します。

mkdir -p ~/.local/jdk && cd ~/.local/jdk
U=https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.20.1%2B1/OpenJDK17U-jdk_x64_mac_hotspot_17.0.20.1_1.tar.gz
curl -sLo jdk17.tar.gz "$U"
shasum -a 256 jdk17.tar.gz
curl -sL "$U.sha256.txt"

最後の2行が出す値が同じかを確かめます。.sha256.txt は、配っている側が同じ版に付けているチェックサムのファイルです。私の iMac では、どちらも c01975da… で始まる同じ値でした。

Apple Silicon の Mac なら、URL の x64 を aarch64 に替えます。

同じなら展開します。

tar xzf jdk17.tar.gz && rm jdk17.tar.gz
~/.local/jdk/jdk-17.0.20.1+1/Contents/Home/bin/java -version

openjdk version "17.0.20.1" 2026-08-18 と出ました。フルパスを付けずに実行した java -version は、置いたあとも Unable to locate a Java Runtime のままです。Mac 全体の java は変わっていません。

2章から出てくる ./mvnw は、環境変数 JAVA_HOME が指す Java を使います。./mvnw を実行するタブでは、先にこれを実行しておきます。

export JAVA_HOME=~/.local/jdk/jdk-17.0.20.1+1/Contents/Home

export はそのターミナルのタブの中だけで効きます。新しいタブを開いたら、もう一度入力します。

# 昔 今
1 Java を入れる インストーラで入れ、Windows の環境変数に JAVA_HOME と PATH を足す tar.gz を展開するだけ。JAVA_HOME はコマンドを実行するタブの中だけで指定する
2 入れる版 1.4・5.0 17(Spring Boot 4 の下限)

2. プロジェクトを作り、GET で固定の2件を返す

2-1. start.spring.io でプロジェクトを作る

Spring Boot のプロジェクトのテンプレートは、start.spring.io というサイトが zip で作ります。画面で選ぶこともできますが、私は URL に選ぶものを並べて curl で取りました。

cd ~/Documents
curl -s -o todo-api.zip "https://start.spring.io/starter.zip?type=maven-project&language=java&bootVersion=4.1.1.RELEASE&baseDir=todo-api&groupId=com.triponte&artifactId=todo-api&name=todo-api&packageName=com.triponte.todoapi&javaVersion=17&dependencies=web"
unzip -q todo-api.zip && rm todo-api.zip

私は Google ドライブの中の作業フォルダに作りましたが、この記事では ~/Documents/todo-api に作った形で書きます。

URL で選んだものは次のとおりです。

# 項目 選んだもの
1 type maven-project(ビルドに Maven8 を使う)
2 bootVersion 4.1.1.RELEASE(start.spring.io の版の一覧にある書き方をそのまま使った)
3 javaVersion 17
4 packageName com.triponte.todoapi
5 dependencies web(Web の部品だけ。データベースは5章で足す)

2-2. pom.xml の版を直す

このまま ./mvnw を実行すると、親の POM9 が見つからずに止まりました。pom.xml の8行目(<parent> の中の <version>)が 4.1.1.RELEASE になっていて、Maven Central(ネット上の部品の置き場)にある版は 4.1.1 だったからです。4.1.1 に直しました。

cd todo-api
sed -i '' 's#<version>4.1.1.RELEASE</version>#<version>4.1.1</version>#' pom.xml
grep -n "<version>4.1.1" pom.xml

grep が 8: <version>4.1.1</version> の1行を返せば直っています。

直したあとの pom.xml の先頭はこうなります。

pom.xml
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>4.1.1</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

親の POM を継承すると、部品ごとの版を自分で書かなくて済みます。部品の欄には、spring-boot-starter-webmvc と、テスト用の spring-boot-starter-webmvc-test の2つが入っていました。前者は、Spring Boot 3 までの spring-boot-starter-web から名前が変わっています。

2-3. 待ち受けを 127.0.0.1 だけにする

src/main/resources/application.properties を次のように書きます。

src/main/resources/application.properties
spring.application.name=todo-api
# 待ち受けをこの Mac の中(127.0.0.1)だけにする
server.address=127.0.0.1
server.port=8080

server.address を書かないと、Spring Boot は全部の窓口(0.0.0.0)で待ち受けます。同じ Wi-Fi にいる別の機械からも届いてしまうので、この Mac の中だけにしました。

2-4. GET で固定の2件を返す

src/main/java/com/triponte/todoapi/TodoController.java を新しく作ります。

TodoController.java
package com.triponte.todoapi;

import java.util.List;
import java.util.Map;

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

// @RestController:メソッドの戻り値を、そのまま JSON にして返すクラス
@RestController
@RequestMapping("/api/todos")
public class TodoController {

    // GET /api/todos で呼ばれる。まだ固定の2件を返すだけ
    @GetMapping
    public List<Map<String, Object>> list() {
        return List.of(
                Map.of("text", "Java から返した1件目", "done", false),
                Map.of("text", "Java から返した2件目", "done", true));
    }
}

使った書き方は次の3つです。

# 書き方 何をするか
1 @RestController このクラスのメソッドが返したものを、JSON にしてレスポンスに書く
2 @RequestMapping("/api/todos") このクラスが受け持つ URL の先頭の部分
3 @GetMapping GET で来たときに、このメソッドを呼ぶ

@ で始まるものはアノテーション10です。Java 5 からある書き方で、20年前にもありました。List.of・Map.of は Java 9 から入った、中身を変えられない一覧を1行で作る書き方です。

戻り値を JSON にしているのは、spring-boot-starter-webmvc に入っている Jackson11 という部品です。

TodoController は、web.xml のようなところに登録していません。最初から入っている TodoApiApplication.java に付いている @SpringBootApplication が、起動のときに同じパッケージ(com.triponte.todoapi)の下を探し、@RestController の付いたクラスを見つけて使います。

2-5. 起動して curl で見る

./mvnw spring-boot:run

最初の1回は、Maven 本体と部品をネットから取ってくるので時間がかかります。取ってきた部品は ~/.m2/ に入り、2回目からはそこを使います。

起動のログに、次の行が出ました(行の先頭の時刻などを省いています)。

Tomcat initialized with port 8080 (http)
Starting Servlet engine: [Apache Tomcat/11.0.24]
Tomcat started on port 8080 (http) with context path '/'

Tomcat を入れていないのに、Tomcat が起動しています。部品の一覧に入っている spring-boot-starter-webmvc の依存に、Tomcat が入っていました。

./mvnw spring-boot:run を実行したタブは、動いているあいだ使えません。ターミナルの別のタブ(⌘T)で curl を実行します。

curl http://127.0.0.1:8080/api/todos

2件が、次の形の JSON で返りました。Map は中身の並び順が決まっていないので、text と done の順は入れ替わることがあります。

[{"text":"Java から返した1件目","done":false},{"text":"Java から返した2件目","done":true}]

動いているうちに lsof で待ち受けを見ると、127.0.0.1:8080 だけでした。

lsof -nP -iTCP:8080 -sTCP:LISTEN

止めるときは、./mvnw spring-boot:run を実行したタブで Ctrl+C を押します。

# 昔(Servlet) 今(Spring Boot)
1 サーバーを動かす Tomcat を別に入れ、war を webapps/ に置いて起動を待つ ./mvnw spring-boot:run。Tomcat は部品の1つとして入ってくる
2 URL と処理を結ぶ web.xml に <servlet> と <url-pattern> を書く メソッドに @GetMapping を付ける
3 JSON を返す doGet の中で response.getWriter() に文字列を組み立てて書く 戻り値の List がそのまま JSON になる
4 部品を集める jar を手で集めて WEB-INF/lib/ に置く pom.xml に名前を書くと ./mvnw が取ってくる

3. Nuxt の $fetch の送り先を Java にする

ここからは nuxt-study の側を直します。前の記事の npm run dev が動いていたら、Ctrl+C で止めておきます。前の記事のまま動いている形を残したいので、git のブランチ java-backend を作りました。

cd ~/Documents/nuxt-study
git checkout -b java-backend

3-1. nuxt.config.ts に Java の場所を書く欄を足す

devtools の行の末尾に , を付け、その下に runtimeConfig の4行を足します。足したあとはこうなります。

nuxt.config.ts
export default defineNuxtConfig({
  compatibilityDate: '2025-07-15',
  devtools: { enabled: true },
  // javaApi:Java の API の場所。空なら今までどおりメモリを使う
  runtimeConfig: {
    javaApi: ''
  }
})

runtimeConfig に書いた欄は、起動するときの環境変数で上書きできます。javaApi なら NUXT_JAVA_API です。Nuxt は、欄の名前を大文字にして単語の境目(javaApi なら a と A のあいだ)に _ を入れ、先頭に NUXT_ を付けた名前を探します。

3-2. GET を Java へ渡す

server/api/todos.get.ts を次のように書き換えます。../utils/todoStore は、前の記事で作った一覧の置き場です。

server/api/todos.get.ts
import { store } from '../utils/todoStore'

// NUXT_JAVA_API を付けて起動したときは Java の API に聞き、
// 付けないときはサーバーのメモリにある一覧を返す
export default defineEventHandler(async (event) => {
  const { javaApi } = useRuntimeConfig(event)
  if (javaApi) {
    return await $fetch(`${javaApi}/api/todos`)
  }
  return store.todos
})

2-5 で止めた Java を、./mvnw spring-boot:run でもう一度起動します。Java を動かしたまま、別のタブで Nuxt を起動します。

cd ~/Documents/nuxt-study
NUXT_JAVA_API=http://127.0.0.1:8080 npm run dev

Chrome で http://localhost:3000 を開くと、Java から返した2件が並びました。NUXT_JAVA_API を付けずに起動すると、前の記事と同じ空の一覧に戻ります。

# 昔 今
1 行き先を切り替える web.xml の <context-param> やプロパティファイルを書き換えて、war を作り直す 起動するときに環境変数を1つ付ける
2 前の形を残す フォルダを丸ごとコピーする git のブランチを作る。git checkout main(前の記事のブランチ)で戻る

4. PUT を足す(止めると消える)

4-1. 1件の形と、一覧の置き場を作る

TodoController.java と同じフォルダ(src/main/java/com/triponte/todoapi/)に、1件の形 Todo.java を作ります。Nuxt の type Todo = { text: string; done: boolean } と同じ形です。

Todo.java
package com.triponte.todoapi;

// 1件の形:やることの文字と、完了したかどうか
public class Todo {

    private String text;
    private boolean done;

    // JSON から作るときに使う(引数なしのコンストラクタが要る)
    public Todo() {
    }

    public Todo(String text, boolean done) {
        this.text = text;
        this.done = done;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public boolean isDone() {
        return done;
    }

    public void setDone(boolean done) {
        this.done = done;
    }
}

昔と同じ、getter と setter のクラスです。Java 16 からは record でも短く書けますが、この記事では Java 11 までの書き方に絞ったので使っていません。

同じフォルダに、一覧の置き場 TodoStore.java を作ります。

TodoStore.java
package com.triponte.todoapi;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Component;

// @Component:Spring が起動のときに1つだけ作り、使う側へ渡す
@Component
public class TodoStore {

    private List<Todo> todos = new ArrayList<>();

    public synchronized List<Todo> findAll() {
        return new ArrayList<>(todos);
    }

    // 一覧をまるごと置き換える
    public synchronized List<Todo> replaceAll(List<Todo> newTodos) {
        todos = new ArrayList<>(newTodos);
        return new ArrayList<>(todos);
    }
}

@Component を付けたクラスは、Spring が起動のときに1つだけ作ります。GET でも PUT でも、同じ1つを読み書きします。new ArrayList<>() の <> は Java 7 からの書き方で、左辺と同じ型を省いています。

4-2. TodoController に PUT を足す

TodoController.java を次のように書き換えます。

TodoController.java
package com.triponte.todoapi;

import java.util.List;

import org.springframework.web.bind.annotation.GetMapping;
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.RestController;

@RestController
@RequestMapping("/api/todos")
public class TodoController {

    private final TodoStore store;

    // Spring が TodoStore を1つ作って、ここへ渡す(自分で new しない)
    public TodoController(TodoStore store) {
        this.store = store;
    }

    @GetMapping
    public List<Todo> list() {
        return store.findAll();
    }

    // @RequestBody:リクエストの本文の JSON を List<Todo> に変えて受け取る
    @PutMapping
    public List<Todo> replace(@RequestBody List<Todo> todos) {
        return store.replaceAll(todos);
    }
}

コンストラクタの引数に TodoStore を書くと、Spring が作った1つを渡します12。new TodoStore() はどこにも書いていません。コンストラクタが1つだけなら、昔の Spring で付けていた @Autowired も要りません。

Java を Ctrl+C で止め、./mvnw spring-boot:run で起動し直してから、別のタブで確かめます。

curl -X PUT -H "Content-Type: application/json" \
  -d '[{"text":"牛乳を買う","done":false},{"text":"記事を書く","done":true}]' \
  http://127.0.0.1:8080/api/todos
curl http://127.0.0.1:8080/api/todos

送った2件が、次の GET で返りました。

4-3. Nuxt の PUT も Java へ渡す

server/api/todos.put.ts を次のように書き換えます。

server/api/todos.put.ts
import { store } from '../utils/todoStore'

// NUXT_JAVA_API を付けて起動したときは、送られてきた一覧をそのまま Java の API へ渡す
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  const { javaApi } = useRuntimeConfig(event)
  if (javaApi) {
    return await $fetch(`${javaApi}/api/todos`, { method: 'PUT', body })
  }
  store.todos = body
  return store.todos
})

Nuxt のタブで Ctrl+C を押し、NUXT_JAVA_API=http://127.0.0.1:8080 npm run dev で起動し直します。Nuxt の http://localhost:3000/api/todos に PUT で1件送ると、Java に直接 curl http://127.0.0.1:8080/api/todos で聞いても、その1件が返りました。画面で1件足したときも、同じ PUT が送られます。

4-4. Java だけ止めて起動し直す

Java のタブで Ctrl+C を押し、もう一度 ./mvnw spring-boot:run を実行します。curl で聞くと [] に戻っていました。一覧は Java の動いているあいだのメモリにしか無いからです。前の記事の Nuxt と同じ結果です。Nuxt は動かしたままでかまいませんが、開いている画面には前の一覧が残っているので、読み込み直してから使います。

# 昔(Servlet) 今(Spring Boot)
1 JSON を Java のオブジェクトにする request.getReader() で文字列を読み、自分で分解する 引数に @RequestBody List<Todo> と書く
2 共有するものを1つだけ作る static のフィールドか、自分で書いた Singleton @Component を付ける。Spring が1つ作って、コンストラクタの引数に渡す
3 1件の形 getter と setter のクラス 同じ書き方で動く

5. H2 に保存する(止めても残る)

5-1. 部品を2つ足す

Java を Ctrl+C で止めてから、pom.xml の <dependencies> の中、spring-boot-starter-webmvc の下に2つ足します。

pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

spring-boot-starter-data-jpa は、SQL を書かずに表を読み書きするための部品です(Spring Data JPA13)。h2 はデータベースそのものです。H2 は Java で書かれたデータベースで、jar の1つとして入ります。Mac に何かをインストールする必要はありません。

5-2. データベースのファイルの場所を書く

application.properties の下に足します。

src/main/resources/application.properties
spring.datasource.url=jdbc:h2:file:${user.home}/.todo-api/todo
spring.datasource.username=sa
spring.datasource.password=
# 表が無ければ作り、あればそのまま使う
spring.jpa.hibernate.ddl-auto=update
# 実行した SQL をログに出す
spring.jpa.show-sql=true

データベースは ~/.todo-api/todo.mv.db のファイル1つになります。プロジェクトの外です。私はプロジェクトを Google ドライブの中に置いていて、同期が書きかけのファイルを読むと壊れることがあるので、データベースだけドライブの外にしました。

ddl-auto=update を書かないと、止めたときに表が消えます。H2 のようにアプリの中で動くデータベースでは、初期値が「止めるときに表を消す(create-drop)」だからです。そうなると、止めたら消えるという4章と同じ結果になってしまいます。

5-3. Todo を表にする

Todo.java を次のように書き換えます。4章から足したのは、@Entity、id の欄と getId()、@JsonIgnore、それに import です。

Todo.java
package com.triponte.todoapi;

import com.fasterxml.jackson.annotation.JsonIgnore;

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

// 1件の形:やることの文字と、完了したかどうか
// @Entity:このクラス1つが表1つ(todo)、1つのオブジェクトが1行になる
@Entity
public class Todo {

    // 行の番号。データベースが1から順に振る。並べる順にも使う
    // @JsonIgnore:画面へは送らない(画面の Todo 型は text と done だけ)
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @JsonIgnore
    private Long id;

    private String text;
    private boolean done;

    // JSON から作るときと、データベースから読むときに使う(引数なしのコンストラクタが要る)
    public Todo() {
    }

    public Todo(String text, boolean done) {
        this.text = text;
        this.done = done;
    }

    public Long getId() {
        return id;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public boolean isDone() {
        return done;
    }

    public void setDone(boolean done) {
        this.done = done;
    }
}

import が jakarta.persistence になっています。昔 JPA を使っていた人なら javax.persistence と書いたところです。Java EE が Eclipse 財団に移ったときに、名前が jakarta に替わりました。Servlet も javax.servlet から jakarta.servlet になっています。setId が無いのは、id をデータベースが振るからです。

5-4. TodoStore を TodoRepository に替える

TodoStore.java を消し、TodoRepository.java を作ります。

TodoRepository.java
package com.triponte.todoapi;

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

// <Todo, Long>=Todo の表を、Long 型の id で扱う
public interface TodoRepository extends JpaRepository<Todo, Long> {
}

中身は空のインターフェースです。これだけで findAll・saveAll・deleteAllInBatch などが使えます。継承した JpaRepository に、これらのメソッドが決めてあります。中身は Spring が起動のときに作ります。deleteAll は1行ずつ読んでから1行ずつ消し、deleteAllInBatch は delete の SQL を1本実行するだけです。

TodoController.java は、最後の形がこれです。

TodoController.java
package com.triponte.todoapi;

import java.util.List;

import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
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.RestController;

@RestController
@RequestMapping("/api/todos")
public class TodoController {

    private final TodoRepository repository;

    public TodoController(TodoRepository repository) {
        this.repository = repository;
    }

    // 表の全行を、足した順(id の小さい順)に返す
    @GetMapping
    public List<Todo> list() {
        return repository.findAll(Sort.by("id"));
    }

    // 送られてきた一覧で、表をまるごと置き換える
    // @Transactional:消すのと入れるのを1つの単位にする。途中で失敗したら両方とも無かったことになる
    @PutMapping
    @Transactional
    public List<Todo> replace(@RequestBody List<Todo> todos) {
        repository.deleteAllInBatch();
        repository.saveAll(todos);
        return repository.findAll(Sort.by("id"));
    }
}

Java を起動すると、ログに表を作る SQL が出ました。表の形を書いたのは Todo.java だけです。

Hibernate: create table todo (id bigint generated by default as identity, done boolean not null, text varchar(255), primary key (id))

PUT を送ると、delete と insert が実行されました。

Hibernate: delete from todo t1_0
Hibernate: insert into todo (done,text,id) values (?,?,default)
Hibernate: select t1_0.id,t1_0.done,t1_0.text from todo t1_0 order by t1_0.id

5-5. 止めても残るかを見る

curl -X PUT で3件を送り、次に1件消して1件にチェックを入れた2件を送りました。Java を Ctrl+C で止めて、起動し直します。curl で聞くと、2件が残っていました。4章とは逆の結果です。

# 昔(JDBC) 今(Spring Data JPA)
1 表を読み書きする DAO クラスに SELECT・INSERT を1本ずつ書き、Connection の close() を自分で呼ぶ JpaRepository を継承したインターフェースを書くだけ
2 表を作る CREATE TABLE を自分で実行する @Entity のクラスから作られる(ddl-auto=update)
3 トランザクション setAutoCommit(false)・commit()・rollback() を自分で書く メソッドに @Transactional を付ける
4 データベース Oracle や MySQL のサーバーを別に用意する H2 は jar の1つ。Mac には何も入れない

6. jar 1本を EC2 で動かす

ここからお金がかかります(付録A)。EC2 は t3.small、ディスクは 8GB にしました。

先に、5章で起動した Nuxt を Ctrl+C で止めておきます。iMac の Java は動かしたままでかまいません。

画面と Nuxt は iMac に残し、Java と H2 だけを EC2 に移します。セキュリティグループで許可するのは 22番(ssh)だけです。8080 へは、ssh のトンネル(ssh -L のポートフォワード)14で iMac から届けました。

6-1. jar を作る

cd ~/Documents/todo-api
./mvnw -DskipTests package
ls -la target/*.jar

target/todo-api-0.0.1-SNAPSHOT.jar ができました。52MB あります。中の BOOT-INF/lib/ に、部品の jar が67本入っていました。

unzip -l target/todo-api-0.0.1-SNAPSHOT.jar | grep tomcat-embed

tomcat-embed-core-11.0.24.jar などが出ます。Tomcat は、この jar の中の部品の1つです。

6-2. Terraform で EC2 を作る

新しいフォルダ ~/Documents/todo-api-aws を作り、その中に main.tf を置きます。main.tf で作るものは2つです。Terraform の書き方は、前の記事「はじめてのTerraform」に書きました。

# 作るもの 中身
1 セキュリティグループ 22番(ssh)を自宅の IP からだけ許可する。8080 は許可しない
2 EC2 t3.small・ディスク 8GB・Amazon Linux 2023。初めて起動するときに Java 17(Amazon Corretto)だけを入れる
main.tf の全体(コメントは記事に合わせて直した)
main.tf
provider "aws" {
  region = "ap-northeast-1"
}

# 自宅の IP。apply のときに -var で渡す(ファイルには書かない)
variable "my_ip" {
  type = string
}

# 22番(ssh)を自宅の IP からだけ通す。8080 は許可しない
# Java の API へは ssh のトンネルを通して届ける(6-4)
resource "aws_security_group" "todo" {
  name        = "todo-api-sg"
  description = "ssh from my home only"

  ingress {
    description = "ssh"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.my_ip]
  }

  egress {
    description = "all outbound"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# Amazon Linux 2023 の最新イメージ番号を AWS に聞く。作るのではなく読むだけ
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

# EC2 本体。t3.small(2vCPU・2GB)
# 鍵は「はじめてのEC2」で作った aws-study-key をそのまま使う
resource "aws_instance" "todo" {
  ami                    = data.aws_ssm_parameter.al2023.value
  instance_type          = "t3.small"
  key_name               = "aws-study-key"
  vpc_security_group_ids = [aws_security_group.todo.id]

  root_block_device {
    volume_size = 8
    volume_type = "gp3"
  }

  # 初めて起動するときに1回だけ実行される。Java 17(Amazon Corretto)を入れる
  user_data = <<-EOT
    #!/bin/bash
    dnf install -y java-17-amazon-corretto-headless
  EOT

  tags = {
    Name = "todo-api"
  }
}

output "public_ip" {
  value = aws_instance.todo.public_ip
}

EC2 に入れるのは Java だけです。Tomcat も Maven も入れません。

cd ~/Documents/todo-api-aws
terraform init
terraform plan -var "my_ip=$(curl -s https://checkip.amazonaws.com)/32"
terraform apply -var "my_ip=$(curl -s https://checkip.amazonaws.com)/32"

plan で 2 to add(セキュリティグループと EC2)と出たのを見てから、apply を実行しました。ここから課金が始まります。最後に public_ip が出ます。この記事では 203.0.113.45 と書きます。

私は作業のフォルダを Google ドライブの中に置いていたので、terraform init が作る .terraform(833MB)を、TF_DATA_DIR=~/.local/tf/todo-api を付けてドライブの外に置きました(付録C)。ドライブの外で作業するなら要りません。

6-3. jar を送って起動する

EC2 が起動したら、jar を送ります。jar のある ~/Documents/todo-api に戻って実行します。

cd ~/Documents/todo-api
scp -i ~/.ssh/aws-study-key.pem target/todo-api-0.0.1-SNAPSHOT.jar ec2-user@203.0.113.45:/home/ec2-user/todo-api.jar
ssh -i ~/.ssh/aws-study-key.pem ec2-user@203.0.113.45

ここからは EC2 の中です。

java -version
nohup java -jar todo-api.jar > todo-api.log 2>&1 &
grep Started todo-api.log
curl http://127.0.0.1:8080/api/todos

java -version は Corretto 17.0.20.1 でした。user_data で入れた Java です。java が見つからないと出たら、user_data の dnf install がまだ終わっていないので、少し待ってから実行し直します。nohup を付けたのは、ssh を抜けても Java を動かし続けるためです。

grep が Started TodoApiApplication in 8.2… のような行を返せば起動済みです。何も返らなければ、数秒おいて実行し直します。起動してから curl を実行すると、[] が返りました。EC2 の上の H2 はまだ空です。

application.properties は iMac のときのままなので、EC2 でも 127.0.0.1:8080 だけで待ち受けています。セキュリティグループで 8080 を許可したとしても、外からは届きません。

6-4. ssh のトンネルで iMac から届ける

exit で EC2 から抜け、iMac のターミナルでトンネルを張ります。

ssh -i ~/.ssh/aws-study-key.pem -f -N -L 127.0.0.1:18080:127.0.0.1:8080 ec2-user@203.0.113.45
curl http://127.0.0.1:18080/api/todos

iMac の 18080 に来たものが、ssh の中を通って EC2 の 127.0.0.1:8080 に届きます。-f は裏で動かす、-N はコマンドを実行せずにつなぐだけ、の指定です。curl は EC2 の中で実行したときと同じ [] を返しました。

Nuxt を、NUXT_JAVA_API の値だけ替えて起動します。

cd ~/Documents/nuxt-study
NUXT_JAVA_API=http://127.0.0.1:18080 npm run dev

5章の Nuxt を止め忘れていると、ポート3000 が使われているので 3001 で起動します。そのとき 3000 には、iMac の Java につながった Nuxt が残っています(付録C)。

画面で「ミルク」を足してチェックを入れました。もう一度 ssh で EC2 に入って curl http://127.0.0.1:8080/api/todos を実行すると、[{"text":"ミルク","done":true}] が返りました。EC2 の H2 に入っています。iMac の H2 とは別のファイルです。

Nuxt のコードは4章から1行も変えていません。jar も iMac で作ったものを作り直していません。変えたのは NUXT_JAVA_API の値だけです。

6-5. 消す

Nuxt を Ctrl+C で止め、トンネルの ssh を止めてから、EC2 を消します。

pkill -f "18080:127.0.0.1:8080"
cd ~/Documents/todo-api-aws
terraform destroy -var "my_ip=$(curl -s https://checkip.amazonaws.com)/32"

EC2 のディスクと一緒に、H2 のファイルも消えます。

残っていないかを数えます。

aws ec2 describe-instances --filters "Name=instance-state-name,Values=running,stopped" --query 'Reservations[].Instances[].InstanceId' --output text
aws ec2 describe-volumes --query 'Volumes[].VolumeId' --output text

どちらも何も返らなければ、課金は止まっています。

最後に、iMac の Java を ./mvnw spring-boot:run のタブで Ctrl+C で止めます。nuxt-study を前の記事の形に戻すときは、git checkout main を実行します。

# 昔 今
1 本番のサーバーに入れるもの Java と Tomcat。Tomcat の設定ファイルも本番用に直す Java だけ
2 送るもの war を送り、Tomcat の webapps/ に置く jar を1本送り、java -jar で起動する
3 サーバーを用意する 申請して、届くのを待つ terraform apply 1本で立つ。要らなくなったら destroy

付録A:かかったお金

1〜5章は0円です。JDK・Maven・Spring Boot・H2 はどれも無料で、iMac の中だけで動かしました。

6章は、apply から destroy まで約13分でした。t3.small は1時間 0.0272 USD(約4円)、8GB のディスクは1日 約4円です。0.0272 USD × 13分で計算すると、合わせて約1円になります。AWS の請求の画面は、まだ見ていません。

付録B:かかった時間

# 見たもの 時間
1 Java の起動(Web の部品だけ・iMac) 2.2秒
2 Java の起動(JPA と H2 を足したあと・iMac) 5.3秒
3 Java の起動(同じ jar・EC2 の t3.small) 8.2秒
4 terraform destroy(EC2 が消えるまで) 19秒

付録C:つまずいたところ

# つまずき 分かったこと
1 start.spring.io で作った pom.xml が、親の POM が見つからずに止まった start.spring.io の一覧は 4.1.1.RELEASE と書くが、Maven の置き場にある版は 4.1.1。pom.xml の8行目を直したら通った
2 Web の部品の名前が、知っていたものと違った Spring Boot 4 では spring-boot-starter-web が spring-boot-starter-webmvc になっていた
3 ddl-auto を書かないと、止めたときに表が消える H2 のようにアプリの中で動くデータベースでは、初期値が create-drop になる。update を書いた
4 Java が起動する前に Nuxt を呼ぶと、500 が返った Nuxt のログに fetch failed が出る。Nuxt は Java の答えを待つだけなので、Java を先に起動する
5 6章の Nuxt が 3001 で起動した 3000 には、iMac の Java につながった Nuxt が動いたままだった。確かめの PUT を 3000 に送ってしまい、iMac の H2 の中身を空で上書きした。H2 は前の中身を残さない
6 terraform init が、作業のフォルダに 833MB の .terraform を作った 作業のフォルダは Google ドライブの中なので、Google ドライブが同期する。TF_DATA_DIR=~/.local/tf/todo-api を付けて、ドライブの外に置いた
  1. Spring Boot。Java の Web アプリを作るためのフレームワーク Spring に、Tomcat などの部品と初期設定を最初から付けたもの。java -jar 1本で動く形にまとめます。 ↩

  2. EC2。AWS で借りる仮想のサーバー。動かしている時間だけお金がかかります。 ↩

  3. H2。Java で書かれたデータベース。jar の1つとしてアプリの中で動き、データをファイル1つに保存します。 ↩

  4. Temurin。Eclipse 財団の Adoptium が配っている、無料の Java(OpenJDK)。OpenJDK のソースから作られています。 ↩

  5. Hibernate。Java のオブジェクトと表の行を対応させて、SQL を作って実行するもの。20年前からあります。Spring Data JPA の下で動いています。 ↩

  6. Nuxt(ナクスト)。Vue で画面を作り、Node.js の上でサーバーとしても動くフレームワーク。server/api/ に置いたファイルが API になり、$fetch で別のサーバーへリクエストを送れます。runtimeConfig は、起動するときの環境変数で上書きできる設定の欄です。 ↩

  7. Terraform。AWS に作るものをファイルに書き、terraform apply 1本で作るコマンド。terraform destroy で全部消せます。 ↩

  8. Maven。Java のビルドのコマンド。pom.xml に書いた部品をネットから取ってきて、jar を作ります。./mvnw は Maven を Mac に入れずに動かすスクリプトで、start.spring.io の zip に入っています。 ↩

  9. POM。Maven の設定ファイル pom.xml のこと。<parent> に書いた親の POM から、部品の版などの初期設定を受け継ぎます。 ↩

  10. アノテーション。クラスやメソッドの前に @ で付ける印。Spring はこの印を見て、URL との結び付けや、1つだけ作る部品を決めます。 ↩

  11. Jackson。Java のオブジェクトと JSON を相互に変える部品。Spring Boot の Web の部品に最初から入っています。 ↩

  12. DI(依存性の注入)。使う側が new せず、Spring が作ったものをコンストラクタの引数などで受け取る書き方。 ↩

  13. JPA。Java のオブジェクトをデータベースの表に保存するための決まりごと。Spring Data JPA は、その上でインターフェースを書くだけで読み書きできるようにしたものです。 ↩

  14. ssh のトンネル。ssh のつながりの中に、別の通信を通す使い方。-L 127.0.0.1:18080:127.0.0.1:8080 は「iMac の 18080 に来たものを、つないだ先の 127.0.0.1:8080 へ渡す」という指定です。 ↩

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?