---
title: Spring WebFlux.fnハンズオン - 8. Spring Bootアプリに変換
tags: ["Spring WebFlux.fn Handson", "Reactor", "Reactor Netty", "Netty", "Spring 5", "Spring WebFlux", "Spring Boot", "Java", "Cloud Foundry", "Pivotal Web Services", "Pivotal Cloud Foundry"]
categories: ["Programming", "Java", "org", "springframework", "web", "reactive"]
date: 2019-08-12T12:25:54Z
updated: 1970-01-01T00:00:00Z
---

本ハンズオンで、次の図のような簡易家計簿のAPIサーバーをSpring WebFlux.fnを使って実装します。
あえてSpring BootもDependency Injectionも使わないシンプルなWebアプリとして実装します。

**ハンズオンコンテンツ**

1. [はじめに](/entries/500)
1. [簡易家計簿Moneygerプロジェクトの作成](/entries/501)
1. [YAVIによるValidationの実装](/entries/502)
1. [R2DBCによるデータベースアクセス](/entries/503)
1. [Web UIの追加](/entries/504)
1. [例外ハンドリングの改善](/entries/505)
1. [収入APIの実装](/entries/506)
1. [Spring Bootアプリに変換](/entries/507) 👈
1. [GraalVMのSubstrateVMでNative Imageにコンパイル](/entries/510)

ここまで、Spring BootやDependency Injectionを敢えて使わず実装してきました。Spring Bootを使わなくても(Small Footprintな)アプリは作れるということを示す目的だったのですが、
Spring Boot Actuatorやこのハンズオンで扱わなかった機能をSpring Bootを使わずに実装していくのは効率的ではありません。
今後、Moneygerを開発し続けていくのであればSpring Boot Wayに乗った方が無難でしょう。

この章ではこれまで作ったSpring Bootアプリに変換します。

**目次**
<!-- toc -->


### pom.xmlの更新

`pom.xml`に`spring-boot-starter-parent`を設定します。

```xml
    <description>...</description>

    <!-- ここから -->
     <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.0.BUILD-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <!-- ここまで -->

    <properties>
    <!-- ... -->
    </properties>
```

代わりに

`<dependenciesManagement>`の次の箇所を削除します。

```xml
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
```

また、`<properties>` 内の`<spring-boot.version>`も削除してください。


次に、`<dependencies>`内の、

``` xml
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
        </dependency>
        <dependency>
            <groupId>io.projectreactor.netty</groupId>
            <artifactId>reactor-netty</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.netty</groupId>
                    <artifactId>netty-transport-native-epoll</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>io.netty</groupId>
                    <artifactId>netty-transport-native-unix-common</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
        </dependency>
```

を

```xml
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
```

に置換します。


### HandlerとRepositoryへアノテーション付与

`ExpenditureHandler`および`IncomeHandler`に`@Component`アノテーションを付与します。


```java
import org.springframework.stereotype.Component;

// ...

@Component
public class ExpenditureHandler {
  // ...
}
```

`R2dbcExpenditureRepository`および`R2dbcIncomeRepository`に`@Repository`アノテーションを付与します。

```java
import org.springframework.stereotype.Repository;
// ...

@Repository
public class R2dbcExpenditureRepository implements ExpenditureRepository {
  // ...
}
```

### App.javaのSpring Boot Application化およびConfigクラスの作成

`App.ava`に全てのConfigurationを定義しましたが、Spring Boot対応に伴い、`App.java`は次のコードだけにします。

```java
// ...
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {

    public static void main(String[] args) throws Exception {
        SpringApplication.run(App.class, args);
    }
}
```

Configurationは必要なものだけ`com.example.config`パッケージ配下に定義します。


* `src/main/java/com/example/config/RouteConfig.java`
* `src/main/java/com/example/config/R2dbcConfig.java`

の2つのファイルを作成してください。


#### RouteConfigの作成

`RouteConfig`に次の内容を記述してください。

```java
package com.example.config;

import com.example.expenditure.ExpenditureHandler;
import com.example.income.IncomeHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;

@Configuration
public class RouteConfig {

    @Bean
    public RouterFunction<ServerResponse> routes(ExpenditureHandler expenditureHandler, IncomeHandler incomeHandler) {
        return expenditureHandler.routes()
            .and(incomeHandler.routes());
    }
}
```

収入APIを作成していない場合は

```java
package com.example.config;

import com.example.expenditure.ExpenditureHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;

@Configuration
public class RouteConfig {

    @Bean
    public RouterFunction<ServerResponse> routes(ExpenditureHandler expenditureHandler) {
        return expenditureHandler.routes();
    }
}
```

で良いです。

`App.staticRoutes()`と同等の機能はSpring Bootより提供されるため削除しました。
また`App.handlerStrategies()`も同じく削除します。

`com.example.error.ErrorResponseExceptionHandler`も使わないので削除してください。

#### R2dbcConfigの作成

`R2dbcConfig`に次の内容を記述してください。

```java
package com.example.config;

import io.r2dbc.pool.ConnectionPool;
import io.r2dbc.pool.ConnectionPoolConfiguration;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.r2dbc.connectionfactory.R2dbcTransactionManager;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.transaction.reactive.TransactionalOperator;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.Optional;

@Configuration
public class R2dbcConfig {

    @Bean
    public TransactionalOperator transactionalOperator(ConnectionFactory connectionFactory) {
        return TransactionalOperator.create(new R2dbcTransactionManager(connectionFactory));
    }

    @Bean
    public DatabaseClient databaseClient(ConnectionFactory connectionFactory) {
        final DatabaseClient databaseClient = DatabaseClient.builder()
            .connectionFactory(connectionFactory)
            .build();
        initializeDatabase(connectionFactory.getMetadata().getName(), databaseClient).subscribe();
        return databaseClient;
    }

    @Bean
    public ConnectionFactory connectionFactory() {
        // postgresql://username:password@hostname:5432/dbname
        String databaseUrl = Optional.ofNullable(System.getenv("DATABASE_URL")).orElse("h2:file:///./target/demo?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
        return ConnectionFactories.get("r2dbc:" + databaseUrl);
    }

    @Bean
    public ConnectionPool connectionPool(ConnectionFactory connectionFactory) {
        return new ConnectionPool(ConnectionPoolConfiguration.builder(connectionFactory)
            .initialSize(4)
            .maxSize(4)
            .maxIdleTime(Duration.ofSeconds(3))
            .validationQuery("SELECT 1")
            .build());
    }

    public static Mono<Void> initializeDatabase(String name, DatabaseClient databaseClient) {
        if ("H2".equals(name)) {
            return databaseClient.execute("CREATE TABLE IF NOT EXISTS expenditure (expenditure_id INT PRIMARY KEY AUTO_INCREMENT, expenditure_name VARCHAR(255), unit_price INT NOT NULL, quantity INT NOT NULL, expenditure_date DATE NOT NULL)")
                .then()
                .then(databaseClient.execute("CREATE TABLE IF NOT EXISTS income (income_id INT PRIMARY KEY AUTO_INCREMENT, income_name VARCHAR(255), amount INT NOT NULL, income_date DATE NOT NULL)")
                    .then());
        } else if ("PostgreSQL".equals(name)) {
            return databaseClient.execute("CREATE TABLE IF NOT EXISTS expenditure (expenditure_id SERIAL PRIMARY KEY, expenditure_name VARCHAR(255), unit_price INT NOT NULL, quantity INT NOT NULL, expenditure_date DATE NOT NULL)")
                .then()
                .then(databaseClient.execute("CREATE TABLE IF NOT EXISTS income (income_id SERIAL PRIMARY KEY, income_name VARCHAR(255), amount INT NOT NULL, income_date DATE NOT NULL)")
                    .then());
        }
        return Mono.error(new IllegalStateException(name + " is not supported."));
    }
}
```

収入APIを作成していない場合は

```java
package com.example.config;

import io.r2dbc.pool.ConnectionPool;
import io.r2dbc.pool.ConnectionPoolConfiguration;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.r2dbc.connectionfactory.R2dbcTransactionManager;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.transaction.reactive.TransactionalOperator;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.Optional;

@Configuration
public class R2dbcConfig {

    @Bean
    public TransactionalOperator transactionalOperator(ConnectionFactory connectionFactory) {
        return TransactionalOperator.create(new R2dbcTransactionManager(connectionFactory));
    }

    @Bean
    public DatabaseClient databaseClient(ConnectionFactory connectionFactory) {
        final DatabaseClient databaseClient = DatabaseClient.builder()
            .connectionFactory(connectionFactory)
            .build();
        initializeDatabase(connectionFactory.getMetadata().getName(), databaseClient).subscribe();
        return databaseClient;
    }

    @Bean
    public ConnectionFactory connectionFactory() {
        // postgresql://username:password@hostname:5432/dbname
        String databaseUrl = Optional.ofNullable(System.getenv("DATABASE_URL")).orElse("h2:file:///./target/demo?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
        return ConnectionFactories.get("r2dbc:" + databaseUrl);
    }

    @Bean
    public ConnectionPool connectionPool(ConnectionFactory connectionFactory) {
        return new ConnectionPool(ConnectionPoolConfiguration.builder(connectionFactory)
            .initialSize(4)
            .maxSize(4)
            .maxIdleTime(Duration.ofSeconds(3))
            .validationQuery("SELECT 1")
            .build());
    }

    public static Mono<Void> initializeDatabase(String name, DatabaseClient databaseClient) {
        if ("H2".equals(name)) {
            return databaseClient.execute("CREATE TABLE IF NOT EXISTS expenditure (expenditure_id INT PRIMARY KEY AUTO_INCREMENT, expenditure_name VARCHAR(255), unit_price INT NOT NULL, quantity INT NOT NULL, expenditure_date DATE NOT NULL)")
                .then();
        } else if ("PostgreSQL".equals(name)) {
            return databaseClient.execute("CREATE TABLE IF NOT EXISTS expenditure (expenditure_id SERIAL PRIMARY KEY, expenditure_name VARCHAR(255), unit_price INT NOT NULL, quantity INT NOT NULL, expenditure_date DATE NOT NULL)")
                .then();
        }
        return Mono.error(new IllegalStateException(name + " is not supported."));
    }
}
```

で良いです。


この変更に伴い、

`R2dbcExpenditureRepositoryTest.java`および`R2dbcIncomeRepositoryTest.java`の

```java
App.initializeDatabase("H2", this.databaseClient).block();
```

を

```java
R2dbcConfig.initializeDatabase("H2", this.databaseClient).block();
```

に変更してください。

なお、[Spring Boot R2DBC Starter](https://github.com/spring-projects-experimental/spring-boot-r2dbc)は現在開発中で、今後はSpring Bootに取り込まれるので、`R2dbcConfig`のBean定義は不要になるでしょう。

### テストコードの修正

これまでテストコード内で`App.handlerStrategies()`を設定することで、アプリケーション側のWebFlux基盤の設定とテスト側のWebFlux基盤の設定を同一にしていました。
Spring Boot対応に伴い、アプリケーション側のWebFlux基盤はSpring Bootより提供されるため、テスト側もこれに合わせます。
`org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest`アノテーションと`WebTestClient.bindToApplicationContext`メソッドを使うことでこれを実現します。


`ExpenditureHandlerTest`を次のように修正してください。


```java
// ...

// ここから追加
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.web.reactive.function.server.RouterFunction;
// ここまで追加

// ...
// ここから追加
@WebFluxTest
@Import(ExpenditureHandler.class)
// ここまで追加
class ExpenditureHandlerTest {
    // ここから追加
    @Configuration
    static class Config {

        @Bean
        public RouterFunction<?> routes(ExpenditureHandler expenditureHandler) {
            return expenditureHandler.routes();
        }

        @Bean
        @Primary
        public ExpenditureRepository expenditureRepository() {
            return new InMemoryExpenditureRepository();
        }
    }

    @Autowired
    private ApplicationContext applicationContext;
    // ここまで追加
}
```


```java
    private InMemoryExpenditureRepository expenditureRepository = new InMemoryExpenditureRepository();
```

はBean定義したものを使うので

```java
    @Autowired
    private InMemoryExpenditureRepository expenditureRepository;
```

次のコードは不要なので削除してください。


```java
    private ExpenditureHandler expenditureHandler = new Expenditure(this.expenditureRepository);
```

最後に`WebTestClient`の作成方法を変更します。次のコードを

```java
        this.testClient = WebTestClient.bindToRouterFunction(this.expenditureHandler.routes())
            .handlerStrategies(App.handlerStrategies())
            // ...
```

次の内容に変更してください。

```java
        this.testClient = WebTestClient.bindToApplicationContext(this.applicationContext)
        // ...
```

以上で修正は終了です。`IncomeHandlerTest`も同じように修正してください。

修正が終われば全てのテストが成功することを確認してください。
