Spring Cloudでマイクロサービスを構成する（２）：REST API(WebFluxとR2DBC)編



### 概要

今回はSpring BootでREEST APIサービスのサンプルをさらっと作ります。

![services.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534936/259ee965-f80b-777b-2ebc-90eaa28c0517.png)


単純に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？？ノンブロッキング？？ という人には、以下の記事が参考になります。

* [Spring WebFluxのリアクティブプログラミング実装サンプル（基本編）](https://qiita.com/kilvis/items/fb18be963da6cac03ee9)
* [Spring Boot 爆速パフォーマンスアップ](https://qiita.com/cypher256/items/347f86ba10075debe6e6)

### 環境

以下が動く環境を前提とします。

* docker & docker-compose
* JDK11
* なんらかのエディタ、IDE

また、[前回の記事](https://qiita.com/takayoshi-shiraki/items/185dc0ec7c82dc6cb0f4) で作成したDB環境(PostgreSQLと管理WebのAdminer)を使いますので、
前回の記事で作成したものに、ファイルを追加していきます。

### 作業ステップ

本記事では以下のステップで進めていきます。

1. Spring Bootアプリの雛形作成
1. APIの実装（Entity, Repository, Controller）
1. APIのDockerイメージ化

### ステップ１：Spring Bootアプリの雛形作成

さて次は、[Spring Initializr](https://start.spring.io/) でSpring Bootプロジェクトのスケルトンを作ります。

今回は、左側のパネルでArtifactとNameだけ「account」に変更しました。

そして、右側のパネルで以下のDependencyを指定します。

![Spring Initializrの設定内容](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534936/27173738-7efa-bd3b-fc19-abd9cea5e69e.png "Spring Initializrの設定内容")

| 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）がダウンロードできます。


### ステップ２：APIの実装（Entity, Repository, Controller）

ステップ１でダウンロードしたZipを展開し、前回作成したdbディレクトリの隣にaccountディレクトリを配置します。

以下のディレクトリ構成になるようにファイル配置し、追加のファイルを作っていきます。

**`account/src/main/resources/application.properties`は不要になるので削除してください。**

```text
spring-msa
├── account
│   ├── Dockerfile   <-- ★ステップ３で作成する
│   ├── HELP.md
│   ├── mvnw
│   ├── mvnw.cmd
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java
│       │   │   └── com
│       │   │       └── example
│       │   │           └── account
│       │   │               ├── AccountApplication.java
│       │   │               ├── controller
│       │   │               │   ├── AccountController.java   <-- ★作成する
│       │   │               │   └── InfoController.java   <-- ★ステップ３で作成する
│       │   │               ├── 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   <-- ★前回作ったものに対し、ステップ３で追記する
```

まずはEntityです。accountテーブルのカラムに合わせます。
Lombokを使うことによってgetter/setterなどは省略しています。

```java:account/src/main/java/com/example/account/entity/Account.java
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するだけです。
（追加で必要になるメソッドはここに追加していきます）


```java:account/src/main/java/com/example/account/repository/AccountRepository.java
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流ですね。

```java:account/src/main/java/com/example/account/contoroller/AccountController.java
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 の設定ファイルです。
なるべく環境変数でアプリの外側から上書きできるようにしつつ、デフォルト値を設定しています。


```yml:account/src/main/resources/application.yml
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 アプリを起動しましょう。

```console
$ docker-compose up -d db adminer
$ cd account
$ ./mvnw clean springboot-run
```

そしておもむろにWebブラウザで `http://localhost:9001/account` にアクセスします。

DBに入っている3件がJSONで返却されましたか？
（Chromeを使っている人は、拡張機能「[JSON Viewer](https://chrome.google.com/webstore/detail/json-viewer/efknglbfhoddmmfabeihlemgekhhnabb)」を入れておくと、↓のような感じでJSONが整形されて幸せになれます。フィルタ機能もあってデータ確認が楽チン！）

![json.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534936/7d2f7b64-5c5e-2c61-dae4-942e33418923.png)


curlでのCRUDリクエストはこんな感じです。

```console
# 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
```

### ステップ３：APIのDockerイメージ化

さて、作ったSpring Boot アプリをDockerコンテナで動かせるようにしましょう。

**その前に、docker化には関係ありませんが、後々のためにContorollerを１つ足しておきます。**
`http://<host>:<port>` と `http://<host>:<port>/info` のパスに対してで以下の情報をJSONで返します。

* コンテナ名（コンテナ名はSpringは知らないので、環境変数経由でプロパティから取得しています。）
* ポート番号
* IPアドレス
* ホスト名

```java:account/src/main/java/com/example/account/contoroller/InfoController.java
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イメージを作れる機能が追加されましたが、この連載では扱いません。

```text:Dockerfile
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にも追記します。

```text: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も一緒に起動しましょう

```console
$ 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の整形ができて便利です。）

```console
$ 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 も叩いてみましょう。

```console
$ 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」を作っていきます。

ではでは。