构建响应式 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 许可,文字内容采用署名-禁止演绎知识共享许可

获取代码