1. 概述
Spring WebFlux 是Spring 5 引入的全新技术框架,采用响应式编程,最大的特点,就是从请求到响应整个过程都是异步非阻塞,底层支持netty,undertow,以及servlet 3.1 +等web容器。跟上一篇对mvc介绍一样,先抛开具体的技术,从外部视角,最简单的流程开始。
1.1 简单流程
它跟spring-mvc处理过程相似,同样包含以下步骤:
步骤1:解析http协议请求数据包,与mvc封装为Request类对象不同,它封装为ServerHttpRequest类对象。
步骤2:通过url匹配、映射到一个具体的Controller类method,或HandlerFunction实例。
步骤3:解析Request对象数据,转换为控制类method的参数数据。
步骤4:校验method的参数数据。
步骤5:invoke控制类method方法。
步骤6:按视图格式(json,xml,html等),序列化返回值。
步骤7:将视图数据,按http协议格式刷入TCP响应流。
1.2 spring-webflux流程
1.3 核心类
以下是spring-webflux处理过程中,最常见的几个类。
(1) DispatcherHandler类
所有spring-webflux处理请求,都由该类的handle(ServerWebExchange exchange)方法统一处理,从匹配handler,到invokeHandler, 以及最后view的处理逻辑,贯穿整个过程。
(2) RequestMappingHandlerMapping类
该类负责匹配controller类型的handler,通过url查找到唯一的controller.method,然后封装为Mono<HandlerMethod>实例,它不像mvc体系,还需要匹配spring拦截器。
为了提高匹配效率,在web项目启动时,它的内部类MappingRegistry,会扫描、加载所有RequestMapping方法,预先构建url查找字典。
(3) RouterFunctionMapping类
该类负责匹配HandlerFunction类型的handler,该handler通过RouterFunction方式路由配置。
(4) RequestMappingHandlerAdapter、HandlerFunctionAdapter类
该类负责controller类型的handler的invoke处理,以及调用前的参数解析、校验等准备逻辑。
(5) ResponseBodyResultHandler类
该类负责处理响应结果HandlerResult。
其中,响应式编程基础Mono,Flux类,计划单独整理,这里就不赘述了。
2. 实施
首先,需要添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
2.1 注解模式
这种编程模型与传统 Spring MVC 注解的使用上一致,样例如下:
@Controller
@RequestMapping(value="/demo")
public class DemoController {
private final Logger logger = LoggerFactory.getLogger(DemoController2.class);
private final AtomicInteger atomicInt = new AtomicInteger();
@Autowired
private MyService myService;
@RequestMapping(value="/info1")
@ResponseBody
public Mono<String> info1(ServerHttpRequest request, ServerHttpResponse response) {
response.getHeaders().add("myinfo", "xxx");
return Mono.just("hello spring flux");
}
@GetMapping(value="/info2")
@ResponseBody
public Mono<String> info2(
@RequestParam("id") Integer id) {
logger.info("enter function...." + Thread.currentThread().getName());
return Mono.create(x -> {
x.success("hello: " + atomicInt.getAndIncrement());
});
}
@GetMapping(value="/info3")
@ResponseBody
public Mono<Apple> info3(
@RequestParam("id") Integer id,
@RequestParam("name") String name) {
Mono<Apple> result = Mono.just(id)
.flatMap(x -> this.myService.getNameFromMvc(x))
.map(x -> {
Apple apple = new Apple();
apple.setId(id);
apple.setName(x);
return apple;
});
return result;
}
@GetMapping(value="/info4/{id}")
@ResponseBody
public Mono<String> info4(@PathVariable("id") String id) {
return Mono.just("hello: " + id);
}
@PostMapping(value="/info5")
@ResponseBody
public Mono<String> info5(
@RequestParam("id") String id,
@Validated
@RequestBody Apple bb) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
logger.info("request info :{}, {}", id, mapper.writeValueAsString(bb));
return Mono.just("hello info5");
}
}
2.2 HandlerFunction模式
通过HandlerFunction接口继承类,来处理handle逻辑,然后由RouterFunction配置该handler的匹配谓词RequestPredicate,样例如下:
HandlerFunction继承类
@Service
public class HelloHandler implements HandlerFunction<ServerResponse> {
@Override
public Mono<ServerResponse> handle(ServerRequest request) {
return ServerResponse.ok().body(BodyInserters.fromValue("Hello World"));
}
}
RouterFunction配置
@Configuration
public class RoutingConfig {
@Autowired
private HelloHandler helloHandler;
@Bean
public RouterFunction<ServerResponse> router1() {
RouterFunction<ServerResponse> router =
RouterFunctions.route(RequestPredicates.path("/hello-world"), helloHandler::handle);
return router;
}
}
3. 环节
3.1 解析http请求包
默认web容器netty,首先netty将请求数据包,解析为HttpRequest实例,然后再由spring-web框架,统一封装为ServerHttpRequest实例。
3.2 web过滤器
过滤器 WebFilter,区别于spring-mvc中过滤器,它完全是spring-web框架下概念,它同样也可对http请求进行统一拦截、重定向、修改等处理,样例如下:
@Component
@Order(value = 1)
public class MyWebFilter3 implements WebFilter {
private final Logger logger = LoggerFactory.getLogger(MyWebFilter1.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if (this.isPermit(exchange.getRequest().getPath().value())) {
logger.info("拦截请求:" + exchange.getRequest().getURI());
ServerHttpResponse response = exchange.getResponse();
byte[] msg = "no permit request.".getBytes(java.nio.charset.StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(msg);
return response.writeWith(Mono.just(buffer));
}
if (this.isRedirect(exchange.getRequest().getPath().value())) {
logger.info("重定向请求:" + exchange.getRequest().getURI());
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.FOUND);
response.getHeaders().setLocation(URI.create("https://www.baidu.com"));
return response.setComplete();
}
if (this.isInnerRedirect(exchange.getRequest().getPath().value())) {
logger.info("内部重定向:" + exchange.getRequest().getURI());
ServerHttpRequest authErrorReq = exchange.getRequest().mutate().path("/demo2/getInfo3/103").build();
ServerWebExchange authErrorExchange = exchange.mutate().request(authErrorReq).build();
return chain.filter(authErrorExchange);
}
return chain.filter(exchange);
}
private boolean isPermit(String url) {
return url.equals("/xxx/xxx");
}
private boolean isRedirect(String url) {
return url.equals("/xxx/xxx");
}
private boolean isInnerRedirect(String url) {
return url.equals("/xxx/xxx");
}
}
3.3 请求参数转换、解析
spring提供了多种处理方式,主要有Converter、HttpMessageConverter,以及注解等,这部分逻辑跟spring-mvc是一样的,就不在赘述。细节可参考上一篇介绍spring-mvc时,对该部分内容的介绍:web编程(1) spring-webmvc
4. 配置
一般通过实例化spring提供的WebFluxConfigurer接口,进行统一配置,样例如下:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
/*
* 静态资源
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
}
/*
* 跨域资源共享
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080", "https://www.example.com")
.allowedMethods("GET", "POST")
.allowCredentials(true);
}
/*
* http message编解码
*/
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder());
configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder());
}
/*
* Converter转换类
*/
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToDateConverter());
}
}
5. 异常
通过@ControllerAdvice注解,统一处理异常,这部分跟spring-webmvc没有区别。
@ControllerAdvice
public class GlobalExceptionHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public String methodArgumentNotValidException(MethodArgumentNotValidException e){
logger.error("GlobalExceptionHandler error", e);
FieldError f = e.getBindingResult().getFieldError();
logger.info("methodArgumentNotValidException: {}, {}, {}", f.getField(), f.getDefaultMessage(), f.getObjectName());
return "GlobalExceptionHandler: " + e.getMessage();
}
@ExceptionHandler(Exception.class)
@ResponseBody
public String handleException(Exception e){
logger.error("GlobalExceptionHandler error", e);
return "GlobalExceptionHandler: " + e.getMessage();
}
}
6. 总结
(1)由于http请求IO处理,采用异步非阻塞方式,它的线程很少,Netty容器默认IO线程数是内核数量的2倍,而webflux调整为1倍,非常适用于解决IO密集处理场景,例如api网关,它的CPU利用率很高,不存在多线程开发中,线程的上下文切换开销问题。
(2)整个处理过程,必须是非阻塞的,否则性能就会很差,包括依赖的第三方服务,都需要采用非阻塞方式处理,目前redis, mongodb 都已支持响应式数据访问,因JDBC本身同步阻塞,需要改由R2DBC方式处理。
(3)对于spring cloud 项目,web 客户端解决方案Feign,现在还不支持响应式处理,如果需要,官方建议使用 feign-reactive 来代替处理。
(4)对于HTTP请求客户端的处理,需要通过 WebClient,它本身完全非阻塞,跟controller层共享IO处理线程。
(5)以上都是该框架的优点,不足之处就是,传统的同步阻塞技术体系,积累的历史资源比较丰富,相对而言,响应式技术体系还在不断发展、丰富中,可利用的资源要少一些。