4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-10-01

概要

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

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

環境

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

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

また、前回の記事 で作成したDB環境(PostgreSQLと管理WebのAdminer)を使いますので、
前回の記事で作成したものに、ファイルを追加していきます。

作業ステップ

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

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

ステップ1:Spring Bootアプリの雛形作成

さて次は、Spring Initializr でSpring Bootプロジェクトのスケルトンを作ります。

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

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

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

ステップ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などは省略しています。

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するだけです。
(追加で必要になるメソッドはここに追加していきます)

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サンプルとして以下のようにします。
FlexMonoが出てくるのがWebFlux流ですね。

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

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 アプリを起動しましょう。

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

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

DBに入っている3件がJSONで返却されましたか?
(Chromeを使っている人は、拡張機能「JSON Viewer」を入れておくと、↓のような感じでJSONが整形されて幸せになれます。フィルタ機能もあってデータ確認が楽チン!)

json.png

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アドレス
  • ホスト名
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イメージを作れる機能が追加されましたが、この連載では扱いません。

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

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

ではでは。

4
2
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
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?