--- 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`も同じように修正してください。 修正が終われば全てのテストが成功することを確認してください。