Spring 5 - Spring webflux 是一个新的非堵塞函数式 Reactive Web 框架,可以用来建立异步的,非阻塞,事件驱动的服务,并且扩展性非常好。
把阻塞(不可避免的)风格的代码迁移到函数式的非阻塞 Reactive 风格代码,需要把商业逻辑作为异步函数来调用。这可以参考 Java 8 的方法或者 lambda 表达式。由于线程是非阻塞的,处理能力能被最大化使用。
在发布这篇文章的时候,Spring 5 还处于一个里程碑版本中(5.0.0 M5)。
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-webflux
org.springframework.boot
spring-boot-starter-test
test
建立一个简单的用户数据表和从 list 中获取到 user 数据的 DTO 类。这仅仅是一个虚拟的数据 bean,但是这可以实时从其它的数据源像 Rdbms,MongoDb,或者 RestClient 加载数据。由于 JDBC 天生不是响应式的,所以任何对数据库的调用都会阻塞这个线程。MongoDB 有一个响应式的客户端驱动。在测试响应式 Web 服务时的进一步渲染时,REST 风格的调用不会导致任何的阻塞。
public class User {
public User(){}
public User(Long id, String user) {
this.id = id;
this.user = user;
}
private Long id;
private String user;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUser() { return user; }
public void setUser(String user) { this.user = user; }
}
@Repository
public class UserRepository {
private final List users = Arrays.asList(new User(1L, "User1"), new User(2L, "User2"));
public Mono getUserById(String id) {
return Mono.justOrEmpty(users.stream().filter(user -> {
return user.getId().equals(Long.valueOf(id));
}).findFirst().orElse(null));
}
public Flux getUsers() {
return Flux.fromIterable(users);
}
}
GetUserById() 返回一个 Mono
相比命令式编程风格,我们并不返回可用前阻塞线程的 User/List
@Service
public class UserHandler {
@Autowired
private UserRepository userRepository;
public Mono handleGetUsers(ServerRequest request) {
return ServerResponse.ok().body(userRepository.getUsers(), User.class);
}
public Mono handleGetUserById(ServerRequest request) {
return userRepository.getUserById(request.pathVariable("id"))
.flatMap(user -> ServerResponse.ok().body(Mono.just(user), User.class))
.switchIfEmpty(ServerResponse.notFound().build());
}
}
notFound()
,
ok()
,
accepted()
,
created()
等,可用于创建不同类型的反馈。
ServerResponse.ok().body(UserRepository.getUsers(), User.class)
可将此 Flux UserRepository.getUserById()
返回一个MonoServerResponse.ok().body(Mono.just(user), User.class)
将此 Mono
在给定的路径变量(pathVariable)中没有找到用户时,ServerResponse.notFound().build() 返回一个 Mono
在命令式编程风格中,数据接收前线程会一直阻塞,这样使得其线程在数据到来前无法运行。而响应式编程中,我们定义一个获取数据的流,然后定义一个在数据到来后的回调函数操作。这样不会使线程堵塞,在数据被返回时,可用线程就用于执行。
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));
}
}
@GetMapping("/user") public Mono handleGetUsers() {}
控制器方法返回Mono
RouterFunction为应用程序提供了DSL类型的路由功能。 到目前为止,Springs不支持混合这两种类型。
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 是异步的,而且天生基于事件驱动,因此更适合响应式的应用程序。Tomcat 使用 Java NIO 来实现 servlet 规范。Netty 是 NIO 的一个实现,它针对异步、事件驱动的非阻塞 IO 应用程序进行了优化。
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();
@SpringBootApplication
public class Spring5ReactiveApplication {
public static void main(String[] args) throws IOException {
SpringApplication.run(Spring5ReactiveApplication.class, args);
}
}
你可以使用任意诸如Postman、CURL等的HTTP测试工具测试该应用。
Spring测试也支持为响应式服务编写集成测试的功能。
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
public class UserTest {
@Autowired
private WebTestClient webTestClient;
@Test
public void test() throws IOException {
FluxExchangeResult result = webTestClient.get().uri("/api/user").accept(MediaType.APPLICATION_JSON)
.exchange().returnResult(User.class);
assert result.getStatus().value() == 200;
List 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();
}
}
WebClient.create("http://localhost:9000").get().uri("/api/user/1")
.accept(MediaType.APPLICATION_JSON).exchange().flatMap(resp -> resp.bodyToMono(User.class)).block();
exchange()返回Mono
block()阻塞线程执行,直到Mono返回User/List
Spring Web 因其易于开发/调试而是必要的。使用Spring5响应式或Spring Web命令式服务的决定必须根据用例明智地做出。在许多情况下,只有命令式的可能会很好,但是在高可扩展性是关键因素时,响应式非阻塞将更适合。