前の記事「やってみる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 の先頭はこうなります。
<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 を次のように書きます。
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 を新しく作ります。
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行を足します。足したあとはこうなります。
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 は、前の記事で作った一覧の置き場です。
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 } と同じ形です。
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 を作ります。
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 を次のように書き換えます。
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 を次のように書き換えます。
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つ足します。
<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 の下に足します。
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 です。
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 を作ります。
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 は、最後の形がこれです。
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 の全体(コメントは記事に合わせて直した)
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 を付けて、ドライブの外に置いた |
-
Spring Boot。Java の Web アプリを作るためのフレームワーク Spring に、Tomcat などの部品と初期設定を最初から付けたもの。
java -jar1本で動く形にまとめます。 ↩ -
EC2。AWS で借りる仮想のサーバー。動かしている時間だけお金がかかります。 ↩
-
H2。Java で書かれたデータベース。jar の1つとしてアプリの中で動き、データをファイル1つに保存します。 ↩
-
Temurin。Eclipse 財団の Adoptium が配っている、無料の Java(OpenJDK)。OpenJDK のソースから作られています。 ↩
-
Hibernate。Java のオブジェクトと表の行を対応させて、SQL を作って実行するもの。20年前からあります。Spring Data JPA の下で動いています。 ↩
-
Nuxt(ナクスト)。Vue で画面を作り、Node.js の上でサーバーとしても動くフレームワーク。
server/api/に置いたファイルが API になり、$fetchで別のサーバーへリクエストを送れます。runtimeConfigは、起動するときの環境変数で上書きできる設定の欄です。 ↩ -
Terraform。AWS に作るものをファイルに書き、
terraform apply1本で作るコマンド。terraform destroyで全部消せます。 ↩ -
Maven。Java のビルドのコマンド。
pom.xmlに書いた部品をネットから取ってきて、jar を作ります。./mvnwは Maven を Mac に入れずに動かすスクリプトで、start.spring.io の zip に入っています。 ↩ -
POM。Maven の設定ファイル
pom.xmlのこと。<parent>に書いた親の POM から、部品の版などの初期設定を受け継ぎます。 ↩ -
アノテーション。クラスやメソッドの前に
@で付ける印。Spring はこの印を見て、URL との結び付けや、1つだけ作る部品を決めます。 ↩ -
Jackson。Java のオブジェクトと JSON を相互に変える部品。Spring Boot の Web の部品に最初から入っています。 ↩
-
DI(依存性の注入)。使う側が
newせず、Spring が作ったものをコンストラクタの引数などで受け取る書き方。 ↩ -
JPA。Java のオブジェクトをデータベースの表に保存するための決まりごと。Spring Data JPA は、その上でインターフェースを書くだけで読み書きできるようにしたものです。 ↩
-
ssh のトンネル。ssh のつながりの中に、別の通信を通す使い方。
-L 127.0.0.1:18080:127.0.0.1:8080は「iMac の 18080 に来たものを、つないだ先の 127.0.0.1:8080 へ渡す」という指定です。 ↩