天天看点

大佬用8000字带你搞懂spring5全新编程框架:Spring WebFlux

作者:程序员高级码农II

Spring WebFlux

在构建Web服务上,Spring 5引入了全新的编程框架,这就是SpringWebFlux。作为一款新型的Web服务开发框架,它与传统的WebMVC相比有哪些区别呢?我们又应该如何基于WebFlux组件来开发响应式Web服务呢?本篇我们就将对这些话题展开讨论。

对比WebMVC和WebFlux架构

和其他主流的Web开发框架一样,在Spring WebMVC中,对Web请求的处理机制也基于管道-过滤器(Pipe-Filter)架构模式。Spring WebMVC使用了Servlet中的过滤器链(FilterChain)来对请求进行拦截,FilterChain的定义如代码清单5-8所示。

代码清单5-8 FilterChain接口定义代码

public interface FilterChain {

public void doFilter (ServletRequest request, ServletResponse

response) throws IOException, ServletException;

}

我们知道WebMVC运行在Servlet容器上,常用的容器包括Tomcat、JBoss等。

当HTTP请求通过Servlet容器时就会被转换为一个ServletRequest对象,而处理的结果将以Servlet-Response对象的形式返回。当ServletRequest通过过滤器链中的一系列过滤器之后,最终就会到达作为前端控制器的DispatcherServlet。DispatcherServlet是WebMVC的核心组件,扩展了Servlet对象,并持有一组HandlerMapping和HandlerAdapter。

当ServletRequest请求到达时,DispatcherServlet负责搜索HandlerMapping实例并使用合适的HandlerAdapter对其进行适配。其中,HandlerMapping的作用是根据当前请求找到对应的处理器Handler,它只定义了一个方法,如代码清单5-9所示。

代码清单5-9 HandlerMapping接口定义代码

public interface HandlerMapping {

//找到与请求对应的Handler,封装为一个HandlerExecutionChain返回

HandlerExecutionChain getHandler(HttpServletRequest request)

throws Exception;

}

而HandlerAdapter根据给定的HttpServletRequest和HttpServletResponse对象真正调用给定的Handler,核心方法如代码清单5-10所示。

代码清单5-10 HandlerAdapter接口定义代码

public interface HandlerAdapter {

//针对给定的请求/响应对象调用目标Handler

ModelAndView handle(HttpServletRequest request,

HttpServletResponse response, Object handler) throws Exception;

}

在执行过程中,DispatcherServlet会在应用上下文中搜索所有HandlerMapping。日常开发过程中,最常用到的HandlerMapping包含BeanNameUrlHandlerMapping和Request-MappingHandlerMapping,前者负责检测所有Controller并根据请求URL的匹配规则映射到具体的Controller实例上,而后者基于@RequestMapping注解来找到目标Controller。

如果我们使用了RequestMappingHandlerMapping,那么对应的HandlerAdapter就是RequestMappingHandlerAdapter,它负责将传入的ServletRequest绑定到添加了@Request-Mapping注解的控制器方法上,从而实现对请求的正确响应。同时,HandlerAdapter还提供请求验证和响应转换等辅助性功能,使得Spring WebMVC框架在日常Web开发中非常实用。

基于上述讨论,我们可以梳理Spring WebMVC的整体架构如图5-4所示。

大佬用8000字带你搞懂spring5全新编程框架:Spring WebFlux

图5-4 Spring WebMVC的整体架构

就整体架构而言,Spring WebFlux和Spring WebMVC本质上并没有什么区别。事实上,前面介绍的HandlerMapping、HandlerAdapter等组件在WebFlux都有同名的响应式版本,这是WebFlux的一种设计理念,即在既有设计的基础上提供新的实现版本,只对部分需要增强和弱化的地方做调整。

WebFlux同样提供了一个过滤器链WebFilterChain,如代码清单5-11所示。

代码清单5-11 WebFilterChain接口定义代码

public interface WebFilterChain {

Mono<Void> filter(ServerWebExchange exchange);

}

这里的ServerWebExchange相当于是一个上下文容器,保存了全新的ServerHttp-Request、ServerHttpResponse对象以及一些框架运行时状态信息。

在WebFlux中,和DispatcherServlet相对应的组件是DispatcherHandler。与Dispatcher-Servlet类似,DispatcherHandler同样使用了一套响应式版本的HandlerMapping和Handler-Adapter完成对请求的处理。请注意,这两个接口是定义在org.springframework.web.reactive包中,而不是在原有的org.springframework.web包中。

响应式版本的HandlerMapping接口定义如代码清单5-12所示,可以看到这里返回的是一个Mono对象,从而启用了响应式行为模式。

代码清单5-12 响应式HandlerMapping接口定义代码

public interface HandlerMapping {

Mono<Object> getHandler(ServerWebExchange exchange);

}

同样,我们找到响应式版本的HandlerAdapter,如代码清单5-13所示。

代码清单5-13 响应式HandlerAdapter接口定义代码

public interface HandlerAdapter {

Mono<HandlerResult> handle(ServerWebExchange exchange, Object

handler);

}

相比WebMVC中ModelAndView这种比较模糊的返回结果,这里的HandlerResult代表处理结果,更加直接和明确。

WebFlux同样实现了响应式版本的RequestMappingHandlerMapping和RequestMapping-HandlerAdapter,因此我们仍然可以采用注解的方法来构建Controller。另外,WebFlux还提供了RouterFunctionMapping和HandlerFunctionAdapter组合,专门用来提供基于函数式编程的开发模式。

Spring WebFlux的整体架构如图5-5所示。

大佬用8000字带你搞懂spring5全新编程框架:Spring WebFlux

图5-5 Spring WebFlux整体架构

创建响应式Web API

介绍完整体架构,本节讨论如何基于Spring WebFlux创建响应式WebAPI。在Spring Boot中,创建响应式HTTP端点有两种实现方法,一种是传统的基于注解的编程模式,一种则是全新的函数式的编程模型。

基于注解的编程模式非常简单,其使用的注解都来自WebMVC框架,我们已经在第4章中做了详细介绍。以4.1.1节所展示的UserController为例,我们对其简单改造就能实现对应的响应式版本,如代码清单5-14所示。

代码清单5-14 响应式UserController实现代码

@RestController

@RequestMapping(value="users")

public class UserController {

@GetMapping(value = "/{id}")

public Mono<User> getUserById(@PathVariable Long id) {

User user = new User();

...

return Mono.just(user);

}

}

注意这里唯一的区别就是getUserById()方法的返回值从普通的User对象转变为了一个Mono<User>对象。因此,对于通过注解来实现响应式Web服务而言,开发人员并没有任何的学习成本。接下来,我们重点介绍函数式的编程模型。

在使用函数式编程模型创建响应式Web API时,我们需要引入一组全新的编程对象,即ServerRequest、ServerResponse、HandlerFunction和RouterFunction。

其中,ServerRequest和ServerRequest是一组,前者代表请求对象,可以访问各种HTTP请求元素,包括请求方法、URI和参数;而后者提供对HTTP响应的访问。将ServerRequest和ServerResponse组合在一起就可以创建HandlerFunction。最后,当通过HandlerFunction创建完成请求的处理逻辑后,需要把具体请求与这种处理逻辑关联起来,RouterFunction可以帮助我们实现这一目标。

RouterFunction与传统Spring WebMVC中的@RequestMapping注解功能类似。

同样,我们通过一些简单的代码示例来演示这些编程对象的使用方法,我们创建如代码清单5-15所示的一个HandlerFunction。

代码清单5-15 UserHandler实现代码

@Configuration

public class UserHandler {

@Autowired

private UserService userService;

public Mono<ServerResponse> getUserById(ServerRequest request) {

Long userId = request.pathVariable("userId");

return

ServerResponse.ok().body(this.orderService.getUserById(userId),

User.class);

}

}

在上述代码示例中,我们创建了一个UserHandler类,然后注入UserService并实现了一个getUserById()处理函数。

注意到,我们通过ServerRequest的pathVariable()方法从HTTP请求URL路径中获取了userId参数,这是ServerRequest最基础的用法。同时,ServerRequest还提供了一系列bodyToMono()和bodyToFlux()方法实现对请求消息体进行访问。

另外,我们在这里也看到了ServerResponse的基础用法。我们可以通过ServerResponse的ok()方法创建代表200状态码的响应,然后通过它的body()方法来构建一个Mono<ServerResponse>对象。

最后,我们根据已经创建的UserHandler来实现RouterFunction,示例代码如代码清单5-16所示。

代码清单5-16 RouterFunction实现代码

RouterFunction<ServerResponse> userRoute =

route(GET("/users/{id}").and(accept(APPLICATION_JSON)),

userHandler::getUserById);

上述代码的作用就是暴露了一个/users/{id}端点,然后通过UserHandler的getUserById()来响应这个端点。

当然,针对User对象,我们还可以创建各种HTTP端点,然后通过RouterFunction的andRoute()方法进行组合,实现效果如代码清单5-17所示。

代码清单5-17 组合RouterFunction实现代码

RouterFunction<ServerResponse> userRoute =

route(GET("/users/{id}") .and(accept(APPLICATION_JSON)), userHandler::getUserById)

.andRoute(GET("/users").and(accept(APPLICATION_JSON)),

userHandler::getUsers)

.andRoute(POST("/users").and(contentType(APPLICATION_JSON)),

userHandler::createUser);

消费响应式Web API

我们已经在4.1.2节中介绍了用于实现远程访问的RestTemplate模板工具类。Rest-Template的主要问题在于不支持响应式流规范,也就无法提供非阻塞式的流式操作。在Spring WebFlux中,也专门存在一个执行响应式远程访问的WebClient工具类,可以认为它就是RestTemplate的响应式升级版本。

和使用RestTemplate一样,创建WebClient的过程也比较简单,我们可以直接使用create()工厂方法来创建WebClient的实例,示例代码如代码清单5-18所示。

代码清单5-18 创建WebClient代码

WebClient webClient = WebClient.create();

同时,WebClient也提供了一组非常实用的工具方法来访问远程响应式Web服务,日常开发过程中比较常用的包括retrieve()方法和exchange()方法。

retrieve()方法是获取响应主体并对其进行解码的最简单方法,我们来看一个示例,如代码清单5-19所示。

代码清单5-19 使用retrieve()方法示例代码

WebClient webClient = WebClient.create("http://localhost:8080");

Mono<User> user = webClient.get()

.uri("/users/{id}", id)

.accept(MediaType.APPLICATION_JSON)

.retrieve()

.bodyToMono(User.class);

上述代码使用JSON作为序列化方式,从远程HTTP端点中获取了一个Mono<User>对象。

相比retrieve()方法,exchange()方法对响应结果拥有更多的控制权,该响应结果是一个ClientResponse对象,包含了响应的状态码、Cookie等信息,示例代码如代码清单5-20所示。

代码清单5-20 使用exchange()方法示例代码

Mono<User> result = webClient.get()

.uri("/users/{id}", id)

.accept(MediaType.APPLICATION_JSON)

.exchange()

.flatMap(response -> response.bodyToMono(User.class));

以上代码演示了如何对响应结果执行flatMap操作符,通过这一操作符调用ClientResponse的bodyToMono()方法以获取目标User对象。

Spring WebFlux案例分析

像Reactor这样的响应式库可以帮助我们构建一个异步的非阻塞流,并且为开发人员屏蔽掉底层的技术复杂度。

而基于Reactor框架的WebFlux进一步降低了开发响应式Web服务的难度。微服务架构的兴起为WebFlux提供了很好的应用场景。我们知道在一个微服务系统中,存在数十乃至数百个独立的微服务,它们相互通信以完成复杂的业务流程。这个过程势必涉及大量的I/O操作。I/O操作,尤其是阻塞式I/O操作会整体增加系统的延迟并降低吞吐量。如果能够在复杂的流程中集成非阻塞、异步通信机制,我们就可以高效处理跨服务之间的网络请求。对于这种场景,WebFlux是一种非常有效的解决方案。

在本节中,我们将基于分布式环境下的远程服务调用过程,来给出使用Spring WebFlux的具体案例。该案例模拟电商系统中订单(Order)下单过程。

我们知道想要完成下单操作,至少需要明确当前订单的用户(User)信息以及订单中所包含的商品(Good)信息。为了演示Web环境下的远程调用过程,也考虑到案例系统的简单性,我们将设计并实现两个独立的Web服务,一个是代表用户信息的user-service,一个是代表订单的order-service。

显然,order-service应该调用user-service中暴露的HTTP端点来完成下单操作,而订单所需的商品信息我们将采用模拟的方式生成数据。

1. 实现user-service

我们先来看user-service的实现过程。作为一个响应式Web服务,我们首先需要明确它所依赖的开发包。在Spring Boot应用程序中,我们需要引入代表WebFlux的spring-boot-starter-webflux依赖。同时,为了演示响应式数据访问过程,本案例将选择使用MongoDB这款提供了Spring Data ReactiveRepository的NoSQL数据库。

为此,我们需要同时引入传统的spring-bootstarter-data-mongodb依赖以及代表响应式MongoDB的spring-boot-starterdata-mongodb-reactive依赖,如代码清单5-21所示。

代码清单5-21 案例中的相关依赖包

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-webflux</artifactId>

</dependency> <dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-mongodb

reactive</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-mongodb</artifactId>

</dependency>

</dependencies>

在user-service中,代表领域实体的User类定义如代码清单5-22所示,这里使用的就是MongoDB所提供的各种注解,我们已经在前面使用过这些注解。

代码清单5-22 User对象定义代码

@Document("users")

public class User {

@Id

private String id;

@Field("userCode")

private String userCode;

@Field("userName")

private String userName;

}

有了实体类,接下来我们定义数据访问层组件UserRepository。请注意,这个User-Repository需要扩展响应式MongoDB所提供的ReactiveMongoRepository接口,如代码清单5-23所示。

代码清单5-23 UserRepository定义代码

public interface UserRepository extends ReactiveMongoRepository<User,

String> {

Mono<User> findUserByUserName(String userName);

}

请注意,现在通过UserRepository获取的用户对象的类型都是Mono<User>或Flux<User>,而不是普通的User。ReactiveMongoRepository通过底层的响应式数据驱动程序确保所有的响应式对象能够得到正确保存和查询。

在UserRepository的基础上,实现Service层组件就比较简单,只是对UserRepository所提供方法进行简单调用即可,如代码清单5-24所示。

代码清单5-24 UserService实现代码

@Service

public class UserService {

@Autowired

private UserRepository userRepository;

public Mono<User> getUserById(String userId) {

return userRepository.findById(userId);

}

public Mono<User> getUserByUserName(String userName) {

return userRepository.findUserByUserName(userName);

}

public void addUser(Mono<User> user){

user.flatMap(userRepository::save);

}

public void updateUser(Mono<User> user){

user.flatMap(userRepository::save);

} public void deleteUser(Mono<User> user){

user.flatMap(userRepository::delete);

}

}

同样需要注意的是,这里所有方法操作的业务对象类型也都是Mono<User>。而在addUser()、updateUser()和deleteUser()方法中,我们通过Reactor框架所提供的flatMap操作完成对UserRepository中对应方法的调用,这也是flatMap操作符非常典型的一种使用方式。

作为对外暴露HTTP端点的Controller层组件,UserController也并不复杂,这里给出该类的实现代码,如代码清单5-25所示。

代码清单5-25 UserController实现代码

@RestController

@RequestMapping(value="users")

public class UserController {

@Autowired

private UserService userService;

@GetMapping(value="/{userId}")

public Mono<User> getUserById(@PathVariable("userId") String

userId) {

return userService.getUserById(userId);

}

@GetMapping(value="/userName/{userName}")

public Mono<User> getUserByUserName(@PathVariable("userName")

String userName) {

return userService.getUserByUserName(userName);

}

@PostMapping(value = "/")

public void addUser(@RequestBody Mono<User> user) { userService.addUser(user);

}

@RequestMapping(value = "/", method = RequestMethod.PUT)

public void updateUser(@RequestBody Mono<User> user) {

userService.updateUser(user);

}

@RequestMapping(value = "/{userId}", method =

RequestMethod.DELETE)

@ResponseStatus(HttpStatus.NO_CONTENT)

public void deleteUser(@PathVariable("userId") String userId) {

User user = new User();

user.setId(userId);

userService.deleteUser(Mono.just(user));

}

}

当完成对user-service中所有组件的构建之后,我们也可以执行一些初始化操作,往MongoDB中插入部分User对象供后续进行使用。

2. 实现order-service

在整个案例中,order-service将调用user-service暴露的HTTP端点。所以我们将在order-service中新建一个UserWebClient类,专门用来访问远程服务,如代码清单5-26所示。

代码清单5-26 UserWebClient实现代码

@Service

public class UserWebClient {

public Mono<UserMapper> getUserById(String userId) {

Mono<UserMapper> userFromRemote = WebClient.create()

.get()

.uri("http://127.0.0.1:8082/users/{userId}", userId)

.retrieve() .bodyToMono(UserMapper.class);

return userFromRemote;

}

public Mono<UserMapper> getUserByUserName(String userName) {

Mono<UserMapper> userFromRemote = WebClient.create()

.get()

.uri("http://127.0.0.1:8082/users/userName/{userName}",userName)

.retrieve()

.bodyToMono(UserMapper.class);

return userFromRemote;

}

}

可以看到,这里通过WebClient工具类完成了对user-service中HTTP端点的远程访问。我们综合使用了retrieve()和bodyToMono()等方法来实现这一目标。

在order-service中使用这个UserWebClient的方式如代码清单5-27所示。

代码清单5-27 OrderService实现代码

@Service

public class OrderService {

@Autowired

UserWebClient userWebClient;

@Autowired

OrderRepository orderRepository;

public Mono<UserMapper> getUser(String userId) {

return userWebClient.getUserById(userId);

}

public Mono<Order> generateOrder(String userId, String goodName) { Order order = new Order();

//获取远程User信息

Mono<UserMapper> user = getUser(userId);

//验证目标用户是否存在

if(user == null ) {

return Mono.just(order);

}

//生成有效Order

Mono<Order> monoOrder = user.flatMap(u -> {

order.setId(UUID.randomUUID().toString());

order.setUserId(userId);

order.setOrderNumber("OrderNumber");

order.setDeliveryAddress("DemoDeliveryAddress");

order.setGoodsName(goodName);

order.setCreateTime(new Date());

return Mono.just(order);

});

//保存Order

return monoOrder.flatMap(orderRepository::save);

}

}

在上述generateOrder()方法中,我们对关键步骤代码添加了注释。

我们首先根据userId从user-service中获取远程User对象并进行校验,如果该对象为空则直接返回。反之,我们构建一个有效的Order对象并进行持久化。整个流程虽然很简单,但已经充分展示了两个服务之间进行响应式远程交互的实现过程。

注意这里再次使用了flatMap操作符完成对Order对象的处理。

本文给大家讲解的内容是springweb服务应用响应式Web开发组件:Spring WebFlux

  • 下文给大家讲解的是springweb服务应用响应式Web开发组件:Spring RSocket