Spring 5 之 Spring Webflux 开发 Reactive 应用

5,924 阅读7分钟
原文链接: coyee.com

Spring 5 - Spring webflux 有一个全新的非堵塞的函数式 Reactive Web 框架,可以用来构建异步的、非堵塞的、事件驱动的服务,在伸缩性方面表现非常好。

将堵塞风格的代码(命令式编程)移植到函数式非堵塞的 Reactive 代码风格,可以很好当帮助你将业务逻辑定义为异步的函数调用,这个可以很方便的通过使用 Java 8 的方法引用或者是 Lambda 表达式来实现。非堵塞的线程可以极大的提升系统的处理能力。

在写这篇文章时,Spring 5 目前还在开发里程碑中(最新版本 5.0.0 M5)。


本文的代码可通过 Github 获取。

创建一个 Spring Boot 项目

创建 Spring Boot 项目最简单的方式是通过 Spring initializer 来做。在你的 Maven pom 中添加如下信息:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <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>
</dependencies>

Spring-boot-starter-webflux 将 spring-webflux、netty 以及其他必须的依赖包引入到类路径中。

创建一个简单的 UserRepository 和 User DTO 类用来从列表中获取用户数据。这只是一个假的 Bean,在实际过程中你可以从包括关系数据库、MongoDB 或者是 RestClient 获取数据。不过需要注意的是,今天我们所用的这些 JDBC 驱动并不是自然支持 Reactive 风格编程的。所有任何对数据库的调用都将导致线程的堵塞。而 MongoDB 提供一个 Reactive 的客户端驱动程序。关于让 Rest 调用非堵塞的内容将在下面 Reactive Web 服务部分的测试中提及。

Mono 和 Flux 是由 Project Reactor 提供的 Reactive 类型。Springs 同时支持其他 Reactive 流实现,如 RXJava。Mono 和 Flux 是由 Reactive 流的 Publisher 中实现的。Mono 是一个用来发送 0 或者单值数据的发布器,Flux 可以用来发送 0 到 N 个值。这非常类似 Flowable 和 RxJava 中的 Observable 。它们表示在订阅这些发布服务时发送数值流。

GetUserById() 返回一个 Mono<User> 表示其在数据可用的情况下发送 0 个或者单个用户, GetUsers() 返回一个用户列表的 Flux 实例,表示其发送 0 到多个用户数据。

与命令式的编程风格相比,我们并不会真正返回 User/List<User> ,因为这将导致线程的执行被堵塞,直到 User/List<User> 数据可用并从方法中返回。我们只是返回一个流的引用,这个流将最终返回 User/List<Users> 数据。

创建一个包含一些方法的 Handler 类用来处理 HTTP 请求

@Service
public class UserHandler {
	@Autowired
	private UserRepository userRepository;

	public Mono<ServerResponse> handleGetUsers(ServerRequest request) {
		return ServerResponse.ok().body(userRepository.getUsers(), User.class);
	}

	public Mono<ServerResponse> handleGetUserById(ServerRequest request) {
		return userRepository.getUserById(request.pathVariable("id"))
			.flatMap(user -> ServerResponse.ok().body(Mono.just(user), User.class))
			.switchIfEmpty(ServerResponse.notFound().build());
	}	
}

处理类相当于 Spring Web 中的服务 Bean,一般用来编写业务功能。ServerResponse 类似 Spring Web 的 ResponseEntity 用来封装响应数据,包括状态码、HTTP 头等信息。在 ServerResponse 对象中,ServerResponse 包含大量有用的默认方法,如 notFound()ok()accepted() created() 等等。这些方法可以用来创建不同类型的响应信息。

UserHandler 有着不同的方法并再次返回 Mono<ServerResponse>; UserRepository.getUsers() 返回 Flux<User>; 然然后 ServerResponse.ok().body(UserRepository.getUsers(), User.class) 将 Flux<User> 转成 Mono<ServerResponse>, 可以在 ServerResponse 可用时候进行数据发送。UserRepository.getUserById()  返回一个 Mono<User> ,而 ServerResponse.ok().body(Mono.just(user), User.class) 将这个 Mono<User> 转成 Mono<ServerResponse>, 这代表在 ServerResponse 可用时候发送响应的流。

ServerResponse.notFound().build()  返回一个 Mono<ServerResponse> 对象,当给定的 pathVariable 中没有找到对应用户信息时返回 404 的服务器响应信息。

在命令式的编程风格中,线程的执行会被堵塞,直到接收到数据。这使得数据在实际返回之前线程必须进行等待。而在 Reactive 编程中,我们定义一个流,用来发送数据并数据返回时所执行的操作。使用这种方法线程是不会被堵塞的。当数据返回时框架会选择一个可用的线程进行下一步处理。

创建一个 Route 类用来定义应用的路由信息

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

@Configuration
public class Routes {
	private UserHandler userHandler;

	public Routes(UserHandler userHandler) {
		this.userHandler = userHandler;
	}

	@Bean
	public RouterFunction<?> routerFunction() {
		return route(GET("/api/user").and(accept(MediaType.APPLICATION_JSON)), userHandler::handleGetUsers)
			.and(route(GET("/api/user/{id}")
			.and(accept(MediaType.APPLICATION_JSON)), userHandler::handleGetUserById));
	}
}

RouterFunction 类似 Spring Web 的 @RequestMapping 。RouterFunction 用来定义 Spring 5 应用的路由信息。RouterFunctions 助手类包含一些有用的方法,例如 route 定义路由并构建 RouterFunction 对象。RequestPredicates 包含大量有用的方法如 GET, POST, path, queryParam ,accept, headers, contentType 等等,可用来定义路由和构建 RouterFunction。每个 Route 映射到一个处理方法,当接收到 HttpRequest 请求的时候就会调用。

Spring5 同时支持 @RequestMapping 声明的控制器,用来定义应用的处理方法映射。我们可以编写一个控制器方法(如下所示),用来创建类似 @RequestMapping 风格的 API。

@GetMapping("/user") public Mono<ServerResponse> handleGetUsers() {}

Mono<ServerResponse> 是在控制器方法中返回的。

RouterFunction 为应用程序提供了 DSL 风格的路由功能。此时,Spring 并不支持两种风格混合使用。

创建 HttpServerConfig 类,然后使用它创建 HttpServer 类

import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
import reactor.ipc.netty.http.server.HttpServer;

@Configuration
public class HttpServerConfig {
	@Autowired
	private Environment environment;

	@Bean
	public HttpServer httpServer(RouterFunction<?> routerFunction) {
		HttpHandler httpHandler = RouterFunctions.toHttpHandler(routerFunction);
		ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
		HttpServer server = HttpServer.create("localhost", Integer.valueOf(environment.getProperty("server.port")));
		server.newHandler(adapter);
		return server;
	}
}

这个根据应用配置中指定的端口创建一个 Netty HttpServer 服务器。Spring 支持例如 Tomcat 或者 Undertow 等其他服务器。因为 Netty 本身是异步的和事件驱动的,因此它更适合用来运行 Reactive 应用。而 Tomcat 使用 Java NIO 来实现 Servlet 规范。Netty 的 NIO 实现专门针对异步、事件驱动和非堵塞应用进行了优化。

Tomcat 服务器使用的代码如下:

Tomcat tomcatServer = new Tomcat();
tomcatServer.setHostname("localhost ");
tomcatServer.setPort(Integer.valueOf(environment.getProperty("server.port ")));
Context rootContext = tomcatServer.addContext(" ", System.getProperty("java.io.tmpdir "));
ServletHttpHandlerAdapter servlet = new ServletHttpHandlerAdapter(httpHandler);
Tomcat.addServlet(rootContext, "httpHandlerServlet ", servlet);
rootContext.addServletMapping("/ ", "httpHandlerServlet ");
tomcatServer.start();

创建启动应用的 Spring Boot 主类

@SpringBootApplication
public class Spring5ReactiveApplication {
    public static void main(String[] args) throws IOException {
        SpringApplication.run(Spring5ReactiveApplication.class, args);
    }
}

测试应用

你可以使用任何一个 HTTP 测试工具来对应用进行测试,例如 Postman 或者 CURL。

Spring 测试同时也提供用来测试 Reactive 服务的集成测试编写。

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
public class UserTest {
@Autowired
private WebTestClient webTestClient;
	@Test
	public void test() throws IOException {
		FluxExchangeResult<User> result = webTestClient.get().uri("/api/user").accept(MediaType.APPLICATION_JSON)
			.exchange().returnResult(User.class);
		assert result.getStatus().value() == 200;
		List<User> users = result.getResponseBody().collectList().block();
		assert users.size() == 2;
		assert users.iterator().next().getUser().equals("User1");
	}

	@Test
	public void test1() throws IOException {
		User user = webTestClient.get().uri("/api/user/1")
			.accept(MediaType.APPLICATION_JSON).exchange().returnResult(User.class).getResponseBody().blockFirst();
		assert user.getId() == 1;
		assert user.getUser().equals("User1");
	}

	@Test
	public void test2() throws IOException {
		webTestClient.get().uri("/api/user/10").accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
			.isNotFound();
	}
}

WebTestClient 类似 TestRestTemplate 类,为 Spring Boot 应用提供了发起 Rest 调用以及诊断响应信息的方法。在测试配置中,Spring test 为 TestRestTemplate 创建了一个 Bean。还有一个 WebClient ,类似 Spring Web 中的 RestTemplate。这个可用来发起 Reactive 和非堵塞的 Rest 调用。

WebClient.create("http://localhost:9000 ").get().uri("/api/user/1 ")
    .accept(MediaType.APPLICATION_JSON).exchange().flatMap(resp -> resp.bodyToMono(User.class)).block();

exchange()返回一个 Mono<ClientResponse> 表示一个发送 clientResponse 的流。

block() 将堵塞线程的执行,直到 Mono 返回 User/List<User>,因为这是一个测试用例,因此我们需要一些数据来诊断回应信息。

Spring Web 是一个命令式的编程框架,可以很方便的进行开发和调试。你需要根据实际情况去决定采用 Spring 5 Reactive 或者是 Spring Web 命令式框架。在很多情况下,命令式的编程风格就可以满足,但当你的应用需要高可伸缩性,那么 Reactive 非堵塞方式是最适合的。