構建響應式 RESTful Web 服務

本指南將引導您完成使用 Spring WebFlux 建立一個“Hello, Spring!” RESTful Web 服務的全過程,然後使用 WebClient 消費該服務。

本指南展示了使用 Spring WebFlux 的函式式方法。您也可以透過註解使用 WebFlux

您將構建什麼

您將使用 Spring Webflux 構建一個 RESTful Web 服務,併為其構建一個 WebClient 消費者。您將能夠在 System.out 和

https://:8080/hello

您需要準備什麼

如何完成本指南

與大多數 Spring 入門指南一樣,您可以從頭開始並完成每個步驟,也可以跳過您已熟悉的基本設定步驟。無論哪種方式,您最終都會得到可工作的程式碼。

從頭開始,請轉到從 Spring Initializr 開始

跳過基礎知識,請執行以下操作

完成後,您可以將您的結果與 gs-reactive-rest-service/complete 中的程式碼進行核對。

從 Spring Initializr 開始

您可以使用這個預初始化專案,然後點選“生成”下載一個 ZIP 檔案。此專案已配置為符合本教程中的示例。

手動初始化專案

  1. 導航到 https://start.spring.io。此服務會為您拉取應用程式所需的所有依賴項,併為您完成大部分設定。

  2. 選擇 Gradle 或 Maven,以及您想使用的語言:Kotlin 或 Java。

  3. 點選依賴項,然後選擇Spring Reactive Web

  4. 單擊生成

  5. 下載生成的 ZIP 檔案,這是一個已根據您的選擇配置好的 Web 應用程式存檔。

如果您的 IDE 集成了 Spring Initializr,您可以從 IDE 中完成此過程。
您還可以從 GitHub fork 專案,並在您的 IDE 或其他編輯器中開啟它。

建立 WebFlux 處理器

我們將從一個 Greeting 型別開始,它將由我們的 RESTful 服務序列化為 JSON。

Java
package com.example.reactivewebservice;

public record Greeting(String message) {
}
Kotlin
package com.example.reactivewebservice

data class Greeting(val message: String)

在 Spring 響應式方法中,我們使用處理器來處理請求並建立響應,如下例所示

Java
package com.example.reactivewebservice;

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 GreetingHandler {

	public Mono<ServerResponse> hello(ServerRequest request) {
		return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
			.body(BodyInserters.fromValue(new Greeting("Hello, Spring!")));
	}
}
Kotlin
package com.example.reactivewebservice

import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import reactor.core.publisher.Mono

@Component
class GreetingHandler {

    fun hello(request: ServerRequest): Mono<ServerResponse> =
        ServerResponse.ok()
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(Greeting("Hello, Spring!"))
}

這個簡單的響應式類總是返回一個帶有“Hello, Spring!”問候語的 JSON 正文。它還可以返回許多其他東西,包括來自資料庫的項流、由計算生成的項流等等。請注意響應式程式碼:一個包含 ServerResponse 主體的 Mono 物件。

建立路由器

在此應用程式中,我們使用路由器來處理我們公開的唯一路由(/hello),如下例所示

Java
package com.example.reactivewebservice;

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

import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;

@Configuration(proxyBeanMethods = false)
public class GreetingRouter {

	@Bean
	public RouterFunction<ServerResponse> route(GreetingHandler greetingHandler) {

		return RouterFunctions.route(GET("/hello")
				.and(accept(MediaType.APPLICATION_JSON)), greetingHandler::hello);
	}
}
Kotlin
package com.example.reactivewebservice

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

@Configuration(proxyBeanMethods = false)
class GreetingRouter {

    @Bean
    fun route(greetingHandler: GreetingHandler): RouterFunction<ServerResponse> = router {
        (accept(MediaType.APPLICATION_JSON) and GET("/hello")).invoke(greetingHandler::hello)
    }
}

路由器監聽 /hello 路徑上的流量,並返回由我們的響應式處理程式類提供的值。

建立 WebClient

Spring 的 RestTemplate 類本質上是阻塞的。因此,我們不希望在響應式應用程式中使用它。對於響應式應用程式,Spring 提供了 WebClient 類,它是非阻塞的。我們使用基於 WebClient 的實現來消費我們的 RESTful 服務

Java
package com.example.reactivewebservice;

import reactor.core.publisher.Mono;

import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

@Component
public class GreetingClient {

	private final WebClient client;

	// Spring Boot auto-configures a WebClient.Builder instance with nice defaults and customizations.
	// We can use it to create a dedicated WebClient for our component.
	public GreetingClient(WebClient.Builder builder) {
		this.client = builder.baseUrl("https://:8080").build();
	}

	public Mono<String> getMessage() {
		return this.client.get().uri("/hello").accept(MediaType.APPLICATION_JSON)
				.retrieve()
				.bodyToMono(Greeting.class)
				.map(Greeting::message);
	}

}
Kotlin
package com.example.reactivewebservice

import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.bodyToMono
import reactor.core.publisher.Mono

@Component
class GreetingClient(builder: WebClient.Builder) {

    // Spring Boot auto-configures a WebClient.Builder instance with nice defaults and customizations.
    // We can use it to create a dedicated WebClient for our component.
    private val client: WebClient = builder.baseUrl("https://:8080").build()

    fun getMessage(): Mono<String> = client
        .get()
        .uri("/hello")
        .accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .bodyToMono<Greeting>()
        .map { it.message }
}

WebClient 類使用響應式特性,以 Mono 的形式儲存訊息內容(由 getMessage 方法返回)。這使用的是函式式 API,而不是命令式 API,來鏈式呼叫響應式運算子。

可能需要一些時間來適應響應式 API,但 WebClient 具有有趣的特性,也可以在傳統的 Spring MVC 應用程式中使用。

您也可以使用 WebClient 與非響應式、阻塞服務進行通訊。

使應用程式可執行

我們將使用 main() 方法來驅動我們的應用程式並從我們的端點獲取 Greeting 訊息。

Java
package com.example.reactivewebservice;

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

@SpringBootApplication
public class ReactiveWebServiceApplication {

	public static void main(String[] args) {
		ConfigurableApplicationContext context = SpringApplication.run(ReactiveWebServiceApplication.class, args);
		GreetingClient greetingClient = context.getBean(GreetingClient.class);
		// We need to block for the content here or the JVM might exit before the message is logged
		System.out.println(">> message = " + greetingClient.getMessage().block());
	}
}
Kotlin
package com.example.reactivewebservice

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class ReactiveWebServiceApplication

fun main(args: Array<String>) {
    val context = runApplication<ReactiveWebServiceApplication>(*args)
    val greetingClient = context.getBean(GreetingClient::class.java)
    // Block here to wait for the response, otherwise the JVM might exit before the message is logged
    println(">> message = ${greetingClient.getMessage().block()}")
}

@SpringBootApplication 是一個方便的註解,它添加了以下所有內容

  • @Configuration:將類標記為應用程式上下文的 bean 定義源。

  • @EnableAutoConfiguration:告訴 Spring Boot 根據類路徑設定、其他 bean 和各種屬性設定開始新增 bean。例如,如果 spring-webmvc 在類路徑中,此註解會將應用程式標記為 Web 應用程式並激活關鍵行為,例如設定 DispatcherServlet

  • @ComponentScan:告訴 Spring 在 hello 包中查詢其他元件、配置和服務,讓它找到控制器。

main() 方法使用 Spring Boot 的 SpringApplication.run() 方法啟動應用程式。您是否注意到沒有一行 XML?也沒有 web.xml 檔案。這個 Web 應用程式是 100% 純 Java,您不必處理任何管道或基礎設施的配置。

構建可執行 JAR

您可以使用 Gradle 或 Maven 從命令列執行應用程式。您還可以構建一個包含所有必要依賴項、類和資源並執行的單個可執行 JAR 檔案。構建可執行 JAR 使在整個開發生命週期中,跨不同環境等,輕鬆交付、版本化和部署服務作為應用程式。

如果您使用 Gradle,您可以透過使用 ./gradlew bootRun 執行應用程式。或者,您可以透過使用 ./gradlew build 構建 JAR 檔案,然後按如下方式執行 JAR 檔案

java -jar build/libs/gs-reactive-rest-service-0.1.0.jar

如果您使用 Maven,您可以透過使用 ./mvnw spring-boot:run 執行應用程式。或者,您可以使用 ./mvnw clean package 構建 JAR 檔案,然後按如下方式執行 JAR 檔案

java -jar target/gs-reactive-rest-service-0.1.0.jar
這裡描述的步驟建立了一個可執行的 JAR。您還可以構建一個經典的 WAR 檔案

日誌輸出已顯示。服務應在幾秒鐘內啟動並執行。

服務啟動後,您會看到一行

>> message = Hello, Spring!

該行來自 WebClient 消費的響應式內容。當然,您可以找到比將其放入 System.out 中更有趣的方法來處理您的輸出。

測試應用程式

現在應用程式正在執行,您可以對其進行測試。首先,您可以開啟瀏覽器並訪問 https://:8080/hello,然後會看到“Hello, Spring!”。對於本指南,我們還建立了一個測試類,以幫助您開始使用 WebTestClient 類進行測試。

Java
package com.example.reactivewebservice;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;

import static org.assertj.core.api.Assertions.assertThat;

//  We create a @SpringBootTest, starting an actual server on a RANDOM_PORT
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class GreetingRouterTest {

	// Spring Boot will create a WebTestClient for you,
	// already configure and ready to issue requests against "localhost:RANDOM_PORT"
	@Autowired
	private WebTestClient webTestClient;

	@Test
	public void testHello() {
		webTestClient
			// Create a GET request to test an endpoint
			.get().uri("/hello")
			.accept(MediaType.APPLICATION_JSON)
			.exchange()
			// and use the dedicated DSL to test assertions against the response
			.expectStatus().isOk()
			.expectBody(Greeting.class).value(greeting -> {
				assertThat(greeting.message()).isEqualTo("Hello, Spring!");
		});
	}
}
Kotlin
package com.example.reactivewebservice

import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody

// We create a @SpringBootTest, starting an actual server on a RANDOM_PORT
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class GreetingRouterTest(@Autowired private val webTestClient: WebTestClient) {

    // Spring Boot will create a WebTestClient for you,
    // already configured and ready to issue requests against "localhost:RANDOM_PORT"
    @Test
    fun testHello() {
        webTestClient
            // Create a GET request to test an endpoint
            .get().uri("/hello")
            .accept(MediaType.APPLICATION_JSON)
            .exchange()
            // and use the dedicated DSL to test assertions against the response
            .expectStatus().isOk
            .expectBody<Greeting>()
            .isEqualTo(Greeting("Hello, Spring!"))
    }
}

總結

恭喜!您已經開發了一個包含 WebClient 的響應式 Spring 應用程式來消費 RESTful 服務!

想寫新指南或為現有指南做貢獻嗎?請檢視我們的貢獻指南

所有指南的程式碼均採用 ASLv2 許可,文字內容採用署名-禁止演繹知識共享許可

獲取程式碼