Spring WebFlux – Spring リアクティブプログラミング

Spring WebFluxは、Spring 5で導入された新しいモジュールです。Spring WebFluxは、Springフレームワークにおけるリアクティブプログラミングモデルへの第一歩です。

Springリアクティブプログラミング

リアクティブプログラミングモデルが初めての場合は、以下の記事を参照してリアクティブプログラミングについて学ぶことを強くお勧めします。

Spring 5が初めての場合は、Spring 5の特徴をご覧ください。

Spring WebFlux

Spring WebFluxは、Spring MVCモジュールの代替です。Spring WebFluxは、イベントループ実行モデル上に構築された完全非同期および非ブロッキングなアプリケーションを作成するために使用されます。以下の図は、Spring公式ドキュメンテーションからのSpring WebFluxとSpring Web MVCの比較についての洞察を提供しています。もし、非ブロッキングなリアクティブモデルでWebアプリケーションやREST Webサービスを開発したい場合は、Spring WebFluxを検討してみてください。Spring WebFluxは、Tomcat、Jetty、Servlet 3.1+コンテナだけでなく、NettyやUndertowなどの非Servletランタイムでもサポートされています。Spring WebFluxは、Project Reactor上に構築されています。Project Reactorは、Reactive Streams仕様の実装です。Reactorは、2つのタイプを提供します:

  1. Mono:Publisherを実装し、0または1つの要素を返す
  2. Flux:Publisherを実装し、N個の要素を返す。

Spring WebFlux Hello Worldの例

Let’s built a simple Spring WebFlux Hello World application. We will create a simple rest web service and use Spring Boot to run it on default Netty server. Our final project structure looks like below image. Let’s look into each component of the application one by one.

Spring WebFlux Maven Dependencies

<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.journaldev.spring</groupId>
  <artifactId>SpringWebflux</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>Spring WebFlux</name>
  <description>Spring WebFlux Example</description>
  
      <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <jdk.version>1.9</jdk.version>
    </properties>
    
  <parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.1.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webflux</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>io.projectreactor</groupId>
			<artifactId>reactor-test</artifactId>
			<scope>test</scope>
		</dependency>
    </dependencies>
	<repositories>
		<repository>
			<id>spring-snapshots</id>
			<name>Spring Snapshots</name>
			<url>https://repo.spring.io/snapshot</url>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</repository>
		<repository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</repository>
	</repositories>
	<pluginRepositories>
		<pluginRepository>
			<id>spring-snapshots</id>
			<name>Spring Snapshots</name>
			<url>https://repo.spring.io/snapshot</url>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</pluginRepository>
		<pluginRepository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</pluginRepository>
	</pluginRepositories>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.7.0</version>
                    <configuration>
                        <source>${jdk.version}</source>
                        <target>${jdk.version}</target>
                    </configuration>
                </plugin>
            </plugins>
    </pluginManagement>
    </build>
    
</project>

アプリケーションの最も重要な依存関係はspring-boot-starter-webfluxspring-boot-starter-parentです。その他の依存関係はJUnitテストケースを作成するためです。

Spring WebFlux Handler

Spring WebFluxハンドラーメソッドはリクエストを処理し、MonoまたはFluxをレスポンスとして返します。

package com.journaldev.spring.component;

import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;

import reactor.core.publisher.Mono;

@Component
public class HelloWorldHandler {

	public Mono<ServerResponse> helloWorld(ServerRequest request) {
		return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN)
			.body(BodyInserters.fromObject("Hello World!"));
	}
}

リアクティブコンポーネントMonoServerResponseボディを保持していることに注意してください。また、戻り値のコンテンツタイプ、レスポンスコード、およびボディを設定するための関数チェーンを見てください。

Spring WebFlux Router

ルーターメソッドは、アプリケーションのためのルートを定義するために使用されます。これらのメソッドはRouterFunctionオブジェクトを返し、ServerResponseのボディも保持します。

package com.journaldev.spring.component;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;

@Configuration
public class HelloWorldRouter {

	@Bean
	public RouterFunction<ServerResponse> routeHelloWorld(HelloWorldHandler helloWorldHandler) {

		return RouterFunctions.route(RequestPredicates.GET("/helloWorld")
                .and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), helloWorldHandler::helloWorld);
	}
}

したがって、/helloWorldに対してGETメソッドを公開し、クライアント呼び出しはプレーンテキストのレスポンスを受け入れる必要があります。

Spring Bootアプリケーション

さあ、Spring BootでシンプルなWebFluxアプリケーションを設定しましょう。

package com.journaldev.spring;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

上記のコードを見ると、Spring WebFluxに関連するものは何もありません。しかし、spring-boot-starter-webfluxモジュールの依存関係を追加したため、Spring BootがアプリケーションをSpring WebFluxとして設定します。

Java 9モジュールサポート

アプリケーションはJava 8で実行する準備ができていますが、Java 9を使用している場合は、module-info.javaクラスも追加する必要があります。

module com.journaldev.spring {
    requires reactor.core;
    requires spring.web;
    requires spring.beans;
    requires spring.context;
    requires spring.webflux;
    requires spring.boot;
    requires spring.boot.autoconfigure;
    exports com.journaldev.spring;
}

Spring WebFlux Spring Bootアプリの実行

EclipseでSpringのサポートがある場合、上記のクラスをSpring Bootアプリとして実行することができます。コマンドラインを使用する場合は、ターミナルを開き、プロジェクトのソースディレクトリからmvn spring-boot:runコマンドを実行します。アプリが実行されていることを確認するために、以下のログメッセージに注目してください。また、より多くのルートと機能を追加してこのシンプルなアプリを拡張する際にも役立ちます。

2018-05-07 15:01:47.893  INFO 25158 --- [           main] o.s.w.r.f.s.s.RouterFunctionMapping      : Mapped ((GET && /helloWorld) && Accept: [text/plain]) -> com.journaldev.spring.component.HelloWorldRouter$$Lambda$501/704766954@6eeb5d56
2018-05-07 15:01:48.495  INFO 25158 --- [ctor-http-nio-1] r.ipc.netty.tcp.BlockingNettyContext     : Started HttpServer on /0:0:0:0:0:0:0:0:8080
2018-05-07 15:01:48.495  INFO 25158 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080
2018-05-07 15:01:48.501  INFO 25158 --- [           main] com.journaldev.spring.Application        : Started Application in 1.86 seconds (JVM running for 5.542)

ログから明らかなように、アプリはポート8080でNettyサーバー上で実行されています。さあ、アプリケーションをテストしてみましょう。

Spring WebFlux App テスト

さまざまな方法でアプリをテストできます。

  1. CURLコマンドを使用する

    $ curl https://localhost:8080/helloWorld
    Hello World!
    $ 
    
  2. ブラウザでURLを開く

  3. Spring 5からのWebTestClientの使用方法 以下は、Spring 5のリアクティブウェブでWebTestClientを使用して当社のRESTウェブサービスをテストするためのJUnitテストプログラムです。

    package com.journaldev.spring;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.http.MediaType;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.test.web.reactive.server.WebTestClient;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    public class SpringWebFluxTest {
    
    	@Autowired
    	private WebTestClient webTestClient;
    
    	
    	@Test
    	public void testHelloWorld() {
    		webTestClient
    		.get().uri("/helloWorld") // GETメソッドとURI
    		.accept(MediaType.TEXT_PLAIN) // ACCEPT-Contentを設定
    		.exchange() // レスポンスにアクセスする
    		.expectStatus().isOk() // レスポンスがOKかどうかをチェック
    		.expectBody(String.class).isEqualTo("Hello World!"); // レスポンスの型とメッセージをチェック
    	}
    
    }
    

    JUnitテストケースとして実行し、問題なくパスするはずです。

  4. Spring Web ReactiveのWebClientを使用すると、REST Webサービスを呼び出すこともできます。

    package com.journaldev.spring.client;
    
    import org.springframework.http.MediaType;
    import org.springframework.web.reactive.function.client.ClientResponse;
    import org.springframework.web.reactive.function.client.WebClient;
    
    import reactor.core.publisher.Mono;
    
    public class HelloWorldWebClient {
    
    	public static void main(String args[]) {
    		WebClient client = WebClient.create("https://localhost:8080");
    
    		Mono result = client.get()
    				.uri("/helloWorld")
    				.accept(MediaType.TEXT_PLAIN)
    				.exchange();
    
    			System.out.println("Result = " + result.flatMap(res -> res.bodyToMono(String.class)).block());
    	}
    	
    }
    

    単純なJavaアプリケーションとして実行するだけで、適切な出力とたくさんのデバッグメッセージが表示されます。

概要

この記事では、Spring WebFluxの概要と、ハローワールドリアクティブなRestfulウェブサービスの構築方法について学びました。Springなどの人気フレームワークがリアクティブプログラミングモデルをサポートしていることは良いことです。しかし、すべての依存関係がリアクティブで非ブロッキングでない限り、アプリケーションも真にリアクティブではありません。例えば、リレーショナルデータベースのベンダーはリアクティブなドライバを持っていません。なぜなら、JDBCに依存しており、それは非リアクティブです。したがって、Hibernate APIも非リアクティブです。したがって、リレーショナルデータベースを使用している場合、まだ真にリアクティブなアプリケーションを構築することはできません。しかし、早くもそれが変わることを期待しています。

プロジェクトのコードは、私のGitHubリポジトリからダウンロードできます。

参考: 公式ドキュメント

Source:
https://www.digitalocean.com/community/tutorials/spring-webflux-reactive-programming