概要
今回はSpring BootでREEST APIサービスのサンプルをさらっと作ります。
単純にSpring Boot でREST APIを作るだけだと面白くないので、以下を試すことにしましょう。
- Spring WebFlux を使ったCRUD APIを作る
- Spring Data R2DBC で PostgreSQLにつなぐ
Springでリアクティブプログラミングに対応したWebFluxがSpring5で追加されました。
Spring Bootでもバージョン2系からWebFluxを使用することができます。
SpringでノンブロッキングAPIを使っている以上、DBへのアクセスも、せっかくなのでノンブロッキングでやれると良さげです。
今回は通常のJDBCではなく、ノンブロッキングをサポートしているR2DBCを使ってPostgreSQLにアクセスしてみようと思います。
なぜWebFlux??ノンブロッキング?? という人には、以下の記事が参考になります。
環境
以下が動く環境を前提とします。
- docker & docker-compose
- JDK11
- なんらかのエディタ、IDE
また、前回の記事 で作成したDB環境(PostgreSQLと管理WebのAdminer)を使いますので、
前回の記事で作成したものに、ファイルを追加していきます。
作業ステップ
本記事では以下のステップで進めていきます。
- Spring Bootアプリの雛形作成
- APIの実装(Entity, Repository, Controller)
- APIのDockerイメージ化
ステップ1:Spring Bootアプリの雛形作成
さて次は、Spring Initializr でSpring Bootプロジェクトのスケルトンを作ります。
今回は、左側のパネルでArtifactとNameだけ「account」に変更しました。
そして、右側のパネルで以下のDependencyを指定します。
Dependencies | 説明 |
---|---|
Spring Reactive Web | Spring WebFlux & Netty |
Spring Data R2DBC | ノンブロッキングでDBアクセスするための機構 |
ProstgreSQL Driver | PostgreSQL用のR2DBC Driver & JDBC Driver |
Lombok | Getter/Setterなどを自動生成してくれる便利なやつ |
Spring Boot Actuator | Spring Boot の内部情報をチェックするために追加 |
この状態でGENERATE
ボタンを押すとスケルトン(account.zip)がダウンロードできます。
ステップ2:APIの実装(Entity, Repository, Controller)
ステップ1でダウンロードしたZipを展開し、前回作成したdbディレクトリの隣にaccountディレクトリを配置します。
以下のディレクトリ構成になるようにファイル配置し、追加のファイルを作っていきます。
account/src/main/resources/application.properties
は不要になるので削除してください。
spring-msa
├── account
│ ├── Dockerfile <-- ★ステップ3で作成する
│ ├── HELP.md
│ ├── mvnw
│ ├── mvnw.cmd
│ ├── pom.xml
│ └── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── account
│ │ │ ├── AccountApplication.java
│ │ │ ├── controller
│ │ │ │ ├── AccountController.java <-- ★作成する
│ │ │ │ └── InfoController.java <-- ★ステップ3で作成する
│ │ │ ├── entity
│ │ │ │ └── Account.java <-- ★作成する
│ │ │ └── repository
│ │ │ └── AccountRepository.java <-- ★作成する
│ │ └── resources
│ │ ├── application.properties <-- ★削除する
│ │ └── application.yml <-- ★作成する
│ └── test
│ └── java
│ └── com
│ └── example
│ └── account
│ └── AccountApplicationTests.java
├── db <-- 前回作成したもの
│ └── init
│ ├── 01_create_table.sql
│ └── 02_insert_data.sql
└── docker-compose.yml <-- ★前回作ったものに対し、ステップ3で追記する
まずはEntityです。accountテーブルのカラムに合わせます。
Lombokを使うことによってgetter/setterなどは省略しています。
package com.example.account.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table("account")
public class Account {
@Id
private Long id;
private String uname;
private String token;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS")
private LocalDateTime createdAt;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS")
private LocalDateTime updatedAt;
}
次に、Repositoryです。DBアクセスをするクラスです。
ここでは、Spring Data R2DBCに標準のCRUDを自動生成するインタフェースReactiveCrudRepository
が用意されているので、extendsするだけです。
(追加で必要になるメソッドはここに追加していきます)
package com.example.account.repository;
import com.example.account.entity.Account;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
public interface AccountRepository extends ReactiveCrudRepository<Account, Long> {
}
次はControllerです。リクエストを受け付けてレスポンスを返すクラスです。
標準的なCRUDサンプルとして以下のようにします。
Flex
やMono
が出てくるのがWebFlux流ですね。
package com.example.account.controller;
import com.example.account.entity.Account;
import com.example.account.repository.AccountRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController()
@RequestMapping(value = "/account")
@RequiredArgsConstructor
class AccountController {
@Autowired
private final AccountRepository accountRepository;
@GetMapping("")
public Flux<Account> all() {
return this.accountRepository.findAll();
}
@PostMapping("")
public Mono<Account> create(@RequestBody Account account) {
return this.accountRepository.save(account);
}
@GetMapping("/{id}")
public Mono<Account> get(@PathVariable("id") Long id) {
return this.accountRepository.findById(id);
}
@PutMapping("/{id}")
public Mono<Account> update(@PathVariable("id") Long id, @RequestBody Account account) {
return this.accountRepository.findById(id)
.map(p -> {
p.setUname(account.getUname());
p.setToken(account.getToken());
return p;
})
.flatMap(p -> this.accountRepository.save(p));
}
@DeleteMapping("/{id}")
public Mono<Void> delete(@PathVariable("id") Long id) {
return this.accountRepository.deleteById(id);
}
}
さて、次はSpring Boot の設定ファイルです。
なるべく環境変数でアプリの外側から上書きできるようにしつつ、デフォルト値を設定しています。
app:
container-name: ${CONTAINER_NAME:account}
server:
port: ${PORT:9001}
spring:
main:
banner-mode: "off"
application:
name: account-api
r2dbc:
url: ${DB_URL:r2dbc:postgresql://localhost:5432/db}
username: ${DB_USER:postgres}
password: ${DB_PASSWD:postgres}
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
さてここまで来たら、docker-composeでDBを起動してから、
ローカルのmaven wrapperでSpring Boot アプリを起動しましょう。
$ docker-compose up -d db adminer
$ cd account
$ ./mvnw clean springboot-run
そしておもむろにWebブラウザで http://localhost:9001/account
にアクセスします。
DBに入っている3件がJSONで返却されましたか?
(Chromeを使っている人は、拡張機能「JSON Viewer」を入れておくと、↓のような感じでJSONが整形されて幸せになれます。フィルタ機能もあってデータ確認が楽チン!)
curlでのCRUDリクエストはこんな感じです。
# show account list
curl -v -H "Content-Type: application/json" http://localhost:9001/account
# show id=1
curl -v -H "Content-Type: application/json" http://localhost:9001/account/1
# add new account
curl -v -X POST -H "Content-Type: application/json" -d '{"uname":"hoge", "token":"XXX"}' http://localhost:9001/account
# update id=1
curl -v -X PUT -H "Content-Type: application/json" -d '{"uname":"foo", "token":"YYYY"}' http://localhost:9001/account/1
# delete id=2
curl -v -X DELETE -H "Content-Type: application/json" http://localhost:9001/account/2
ステップ3:APIのDockerイメージ化
さて、作ったSpring Boot アプリをDockerコンテナで動かせるようにしましょう。
その前に、docker化には関係ありませんが、後々のためにContorollerを1つ足しておきます。
http://<host>:<port>
と http://<host>:<port>/info
のパスに対してで以下の情報をJSONで返します。
- コンテナ名(コンテナ名はSpringは知らないので、環境変数経由でプロパティから取得しています。)
- ポート番号
- IPアドレス
- ホスト名
package com.example.account.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.LinkedHashMap;
@RestController
@RequestMapping("")
public class InfoController {
@Autowired
Environment environment;
@GetMapping
public Mono<LinkedHashMap<String, String>> index() throws UnknownHostException {
return info();
}
@GetMapping("/info")
public Mono<LinkedHashMap<String, String>> info() throws UnknownHostException {
InetAddress localhost = InetAddress.getLocalHost();
LinkedHashMap<String, String> response = new LinkedHashMap<>();
response.put("containerName", environment.getProperty("app.container-name"));
response.put("port", environment.getProperty("server.port"));
response.put("hostAddress", localhost.getHostAddress());
response.put("hostName", localhost.getHostName());
return Mono.just(response);
}
}
次に、accountディレクトリ直下にDockerfileを作りましょう。
注)Spring Boot 2.3系からDockerfileなしにDockerイメージを作れる機能が追加されましたが、この連載では扱いません。
FROM openjdk:11-jdk-slim
ADD target/account-*.jar account.jar
ENV CONTAINER_NAME=account \
PORT=9001 \
DB_NAME=db \
DB_USER=postgres \
DB_PASSWD=postgres \
OPTS_ARGS=''
ENTRYPOINT ["java", "-jar", "/account.jar", "${OPTS_ARGS}"]
その上でdocker-compose.ymlにも追記します。
version: '3'
・・・
services:
db:
・・・
admine:
・・・
account:
image: spring-msa/account-api
container_name: account
build:
context: ./account
dockerfile: Dockerfile
ports:
- "9001:9001"
depends_on:
- db
environment:
- CONTAINER_NAME=account
- PORT=9001
- DB_USER=${DB_USER:-postgres}
- DB_PASSWD=${DB_PASSWORD:-postgres}
- DB_URL=${DB_URL:-r2dbc:postgresql://db:5432/db}
networks:
- default
これで以下のコマンドでaccountアプリを起動させることができます。
db(postgresql) と adminerも一緒に起動しましょう
$ cd account
$ ./mvnw clean package
$ cd ..
$ docker-compose build account
$ docker-compose up -d db adminer account
http://localhost:9001/account にアクセスしてDBの内容が返ってくればOKです。
(jq
を入れておくとJSONの整形ができて便利です。)
$ curl localhost:9001/account | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 350 0 350 0 0 15909 0 --:--:-- --:--:-- --:--:-- 15909
[
{
"id": 1,
"uname": "sato",
"token": "AAAA",
"createdAt": "2020-09-11T06:02:51.064",
"updatedAt": "2020-09-11T06:02:51.064"
},
{
"id": 2,
"uname": "yamada",
"token": "BBBB",
"createdAt": "2020-09-11T06:02:51.082",
"updatedAt": "2020-09-11T06:02:51.082"
},
{
"id": 3,
"uname": "suzuki",
"token": "CCCC",
"createdAt": "2020-09-11T06:02:51.091",
"updatedAt": "2020-09-11T06:02:51.091"
}
]
http://localhost:9001/info も叩いてみましょう。
$ curl localhost:9001/info | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 94 100 94 0 0 15666 0 --:--:-- --:--:-- --:--:-- 15666
{
"containerName": "account",
"port": "9001",
"hostAddress": "172.18.0.9",
"hostName": "c1f0f98a3859"
}
さいごに
とりあえず、JSONを返すREST APIができました。
今回の完成イメージのソースをここに置いておきます。
https://github.com/Piecemeal-Technology-Inc/spring-msa/tree/setup_db_api
次回からは「Service Discovery」や「API Gateway」を作っていきます。
ではでは。