---
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アプリに変換します。
**目次**
### pom.xmlの更新
`pom.xml`に`spring-boot-starter-parent`を設定します。
```xml
...
org.springframework.boot
spring-boot-starter-parent
2.2.0.BUILD-SNAPSHOT
```
代わりに
``の次の箇所を削除します。
```xml
org.springframework.boot
spring-boot-dependencies
${spring-boot.version}
pom
import
```
また、`` 内の``も削除してください。
次に、``内の、
``` xml
org.springframework
spring-context
org.springframework
spring-webflux
ch.qos.logback
logback-classic
io.projectreactor.netty
reactor-netty
io.netty
netty-transport-native-epoll
io.netty
netty-transport-native-unix-common
com.fasterxml.jackson.core
jackson-databind
com.fasterxml.jackson.datatype
jackson-datatype-jsr310
```
を
```xml
org.springframework.boot
spring-boot-starter-webflux
```
に置換します。
### 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 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 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 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 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`も同じように修正してください。
修正が終われば全てのテストが成功することを確認してください。