Bootstrap

微服务专题05-Spring WebFlux 运用

前言

前面的章节我们讲了REST。本节,继续微服务专题的内容分享,共计16小节,分别是:

本节内容重点为:

  • WebFlux 核心组件:包括 HandlerMappingHandlerAdapter 以及 HandlerResultHandler
  • WebFlux 编程模型运用:介绍 Annotation 驱动以及函数声明是编程模型的差异以及最佳实践

回顾 Spring Web MVC

Spring Web MVC是基于Servlet API构建的原始Web框架,并从一开始就包含在Spring Framework中。正式名称“ Spring Web MVC”来自其源模块spring-webmvc的名称, 但它通常被称为“ Spring MVC”。

DispatcherServlet特殊bean 的委托来处理请求并提供适当的响应。“特殊bean”是指实现WebFlux框架协定的Spring管理的Object实例。这些通常带有内置合同,但是您可以自定义它们的属性,扩展或替换它们。

实际上SpringMVC的很多特性与webflux相同,比如下表是 Spring 官网上列出了通过检测到的特殊 bean DispatcherHandler:

Bean 的类型说明
HandlerMapping将请求与拦截器列表一起映射到处理程序,以 进行预处理和后期处理。映射基于某些标准,具体细节因HandlerMapping 实现而异。这两个主要的HandlerMapping实现是RequestMappingHandlerMapping支持带@RequestMapping注释的方法,SimpleUrlHandlerMapping并且维护URI路径模式向处理程序的显式注册。
HandlerAdapter帮助DispatcherServlet调用映射到请求的处理程序,而不管实际如何调用该处理程序。例如,调用带注释的控制器需要解析注释。主要目的HandlerAdapter是保护DispatcherServlet这些细节。
HandlerExceptionResolver解决异常的策略,可能将异常映射到处理程序,HTML错误视图或其他。
ViewResolver解析从处理程序返回到实际的基于逻辑字符串的视图名称,View 以呈现给响应。
LocaleResolver, LocaleContextResolver解决Locale一个客户正在使用的并且可能是其时区的问题,以便能够提供国际化的视图。
ThemeResolver解决您的Web应用程序可以使用的主题,例如,提供个性化的布局。
MultipartResolver借助多部分解析库来解析多部分请求的抽象(例如,浏览器表单文件上传)。
FlashMapManager存储和检索“输入”和“输出” FlashMap,它们通常用于通过重定向将属性从一个请求传递到另一个请求。

除了上述处理类之外,我们再看类 HandlerInterceptor 的源码,共计三个方法 :

在这里插入图片描述
从源码上不难看出,分为 前置、后置处理、完成阶段(异常处理):

  • preHandle 前置
  • postHandle 后置
  • afterCompletion 完成:类似于 finally 效果。这和 java.util.concurrent.CompletableFuture#whenComplete方法是不是有异曲同工之妙。

org.springframework.web.servlet.HandlerMapping

接下来说一下 HandlerMapping 相关问题:
在这里插入图片描述
(上面截图的的spring版本为spring-5.0.7-RELEASE) 首先我们看HandlerMapping 的实现类 AbstractHandlerMapping
通常来讲 HandlerMapping 的实现类是包含 HandlerInterceptor 集合的。那么在 AbstractHandlerMapping 是否也存在呢,答案是肯定的,请见源码:
在这里插入图片描述
包含 HandlerInterceptor 集合 的意义主要在于:

  • 拦截链条
  • 各司其职

既然是各司其职,就会引发顺序问题,谁先执行?谁后执行?

这里会引出一个概念------- 责任链模式

责任链通常有两种类型,即过滤类型和拦截类型。

Q:责任链和filter有什么区别呢?

A:实际上 HandlerInterceptor 就是采用返回值进行流程处理,而 Filter 采用 FilterChain 进行流程处理。 Filter 优先级天生比servlet高,但是 Filter 的最终节点 Servlet。Filter通过filter.chain进入链条的下一个环节,在服务器启动阶段动态组合链条,符合责任链设计模式(动态调用,组合依赖于配置)。

回归主线,继续分析 HandlerMapping的实现类 - AbstractHandlerMapping

在SpringMVC中,我们知道 DispatcherServlet 关联多个 HandlerMapping,类似于笛卡尔积,即:

  • DispatcherServletHandlerMapping = 1 : N
  • HandlerMappingHandlerInterceptor = 1 : N

Q: DispatcherServlet 作为 HandlerMapping 一种,那么问题来了,多个 HandlerMapping 谁能被 DispatcherServlet 选择?

重点内容

这里我先猜想,上面截图的源码 AbstractHandlerMapping 实现了 Ordered 接口参考顺序,这是一种可以排序的可能,另外呢,如果 HandlerMapping 被请求规则匹配了是否可以呢?

猜想验证过程:
首先看一下 org.springframework.web.servlet.DispatcherServlet#doDispatch 方法:
在这里插入图片描述
进去看 org.springframework.web.servlet.DispatcherServlet#getHandler:
这里很明显是通过循环将所有的handlerMappings 返回出去
在这里插入图片描述
所以为了看到handlerMappings 赋值的过程,我这里直接通过idea工具右键选中handlerMappings选择find usages菜单,查看读写的位置:
在这里插入图片描述
通过工具,发现handlerMappings的写入都集中在类 DispatcherServlet 上,所以直接跟进:
在这里插入图片描述
来到方法:org.springframework.web.servlet.DispatcherServlet#initHandlerMappings

initHandlerMappings 方法,主要是初始化HandlerMappings的结果,这里首先将所有匹配的handlerMapping放入了Map<String,HandlerMapping> 。

注意 initHandlerMappings 方法中这段代码:

	this.handlerMappings = new ArrayList<>(matchingBeans.values());

为什么不能将匹配的 handlerMappings直接赋值给当前this变量里呢,而是新创建一个集合再赋值?

重点内容

  • 首先,Map的values()方法返回的就是Collection

我们看说明,返回值是:a collection view of the values contained in this map。注意是View,也就是说,你只能看看values(这里实际上是HandlerMappings),数据是不可变的。
在这里插入图片描述

  • 其次,通过new了一个list数组以后,就可以动态的去写数据,而能写数据就可以对HandlerMappings进行排序了。所以紧接着代码如下:
	// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
			Map<String, HandlerMapping> matchingBeans =
					BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
			if (!matchingBeans.isEmpty()) {
				this.handlerMappings = new ArrayList<>(matchingBeans.values());
				// We keep HandlerMappings in sorted order.
				AnnotationAwareOrderComparator.sort(this.handlerMappings);
			}

接下来看看 sort 方法如何进行排序:
在这里插入图片描述

我们看 AnnotationAwareOrderComparator 的 API 发现如何排序的呢,有两种方式,分别是:

  • 通过某个Bean实现Ordered接口,我们这里明显是 AbstractHandlerMapping 去实现 Ordered接口,前面已经两次提到了!

在这里插入图片描述
当实现了Ordered以后,可以通过 getOrder方法返回顺序,注意源码所示:将HIGHEST_PRECEDENCE赋值给Integer的最小值;将LOWEST_PRECEDENCE赋值给Integer的最大值。所以总结:在类Order的规则里,顺序越大,优先级越小,顺序越小,优先级越大

  • 通过实现 @Order 注解

在Bean上注入 @Order 注解,这里没有应用,略讲。

Tips:看源码就是要考虑很多细节性的问题,一个优秀的架构师,往往能够做到通用性好,并且思考的更深刻

回过头来,继续看主线内容:
前面提到: DispatcherServlet 可以关联多个 HandlerMappingHandlerMapping 也可以关联多个 HandlerInterceptor,这种 1 对 N 的特性其实是有条件的!是需要经过筛选的,请看AbstractHandlerMapping源码:

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		
		Object handler = getHandlerInternal(request);
		if (handler == null) {
			handler = getDefaultHandler();
		}
		if (handler == null) {
			return null;
		}
		// Bean name or resolved handler?
		if (handler instanceof String) {
			String handlerName = (String) handler;
			handler = obtainApplicationContext().getBean(handlerName);
		}
		//核心代码
		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
		if (CorsUtils.isCorsRequest(request)) {
			CorsConfiguration globalConfig = this.globalCorsConfigSource.getCorsConfiguration(request);
			CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
			CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
		}
		return executionChain;
	}

此方法前面进行参数判断,尾部适配浏览器标准(跨域访问),重要看中间代码

直接看方法getHandlerExecutionChain:
在这里插入图片描述
注意这里:

String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);

此方法主要对HTTP请求的 URL 地址做处理,举个栗子:请求地址是 http://www.baidu.com/abc/def,那么 转化后剩下 /abc/def,其实就是相对路径。

接下来对于HandlerInterceptor 集合遍历,然后筛选,在本节开头就提到的HandlerInterceptor,它和MappedInterceptor有什么区别呢?

HandlerInterceptor可以匹配请求,而MappedInterceptor则不可以。

正因为HandlerInterceptor可以匹配请求,源码接着往下看,经过chain添加Interceptor才是被过滤的!

那么chain是怎么添加的Interceptor的呢?在回答这个问题之前,我们播放一个小插曲!

在前面的剧情里,AbstractHandlerMapping 重写了HandlerMapping接口的 getHandler, 返回值是 HandlerExecutionChain,现在我们看此返回值类:
在这里插入图片描述

这里面有两个集合:

  • HandlerInterceptor[]
  • List< HandlerInterceptor >

为什么要用两个属性相同的集合来表示?

插曲播放完毕,让我们回到刚才的问题上,刚才说到的1对N问题,我们说是要经过筛选的!是通过chain去添加Interceptor的,那么怎么添加的呢?

我们将

chain.addInterceptor(mappedInterceptor.getInterceptor());

这段代码点击实现里看看:

	private List<HandlerInterceptor> initInterceptorList() {
		if (this.interceptorList == null) {
			this.interceptorList = new ArrayList<>();
			if (this.interceptors != null) {
				// An interceptor array specified through the constructor
				CollectionUtils.mergeArrayIntoCollection(this.interceptors, this.interceptorList);
			}
		}
		this.interceptors = null;
		return this.interceptorList;
	}

这里的实现是通过interceptors数组作为临时变量存储后merge到interceptorList集合里,然后清空interceptors数组。interceptors数组起到临时变量的作用。

问题来了,为什么要临时存储interceptor 集合呢?我们可以看看此集合在哪里被用到了便一目了然

老办法,对准临时变量interceptors就是操作find usages:
在这里插入图片描述
我们找到这里:
在这里插入图片描述
list是可以变化的,而数组是没法变化的,写两种集合,好处就在于:

  • 为了防止串数据,改动一方不影响另一方。
  • 如果不改的话,考虑到框架前后版本不一致问题,某些版本访问是数组,也可能是集合,一旦写死就很麻烦~

最后我们再看类HandlerMapping 的 getHandler方法进行分析,getHandler的返回值是HandlerExecutionChain:

public class HandlerExecutionChain {

	private final Object handler;

	@Nullable
	private HandlerInterceptor[] interceptors;

	@Nullable
	private List<HandlerInterceptor> interceptorList;
    ...

前面对于两个集合分析了一遍,那么看看这个Object类型的handler指的又是什么呢?

请参考本节开始的截图的HandlerMapping类图:我们根据类图层级调用关系可以猜测到:

当一个request请求来临时,一定是通过handlerMapping抽象实现类AbstractHandlerMapping或者CompositeHandlerMapping去处理请求返回handler的,那么CompositeHandlerMapping实为透传,我们将重点放在AbstractHandlerMapping类的getHandler:

在这里插入图片描述
我们看着一段代码:

Object handler = getHandlerInternal(request);

在这里插入图片描述
接着看getHandlerInternal:
在这里插入图片描述
后两个方法实为透传,我们分别看前两个实现类:

  • AbstractHandlerMethodMapping

在这里插入图片描述
AbstractHandlerMethodMapping 通过其实现类, RequestMappingInfoHandlerMapping,再调用实现类 RequestMappingHandlerMapping 返回 HandlerMethod

  • AbstractUrlHandlerMapping

在这里插入图片描述
AbstractUrlHandlerMapping 获取 Handler(注意返回类型),在通过其实现类 SimpleUrlHandlerMapping 返回 Object

Tips: 通过这一小段分析,我们知道handler的类型是不固定的,通过SimpleUrlHandlerMapping 处理后返回的handler类型是Object,而通过RequestMappingHandlerMapping 返回的的handelr类型是 HandlerMethod

那么 HandlerMethod 与 执行方法有什么联系呢?

HandlerMethod 初始化过程

在这里插入图片描述

如果当 @Controller 方法上面标注了 @RequestMapping,而@RequestMapping 主要可以标识URI。比如@RequestMapping(“/cache”)。所以我们就可以大胆猜测是通过RequestMappingHandlerMapping 处理的。

Q:那么源码上对于 HandlerMethod 与 执行方法是怎么处理的呢?

A:我们从 HandlerMethod 初始化的地方入手看 org.springframework.web.reactive.result.method.AbstractHandlerMethodMapping#afterPropertiesSet:

在这里插入图片描述
afterPropertiesSet方法会在父类的Bean(RequestMappingHandlerMapping)初始化的时候调用。
我们看这里:

if (beanType != null && isHandler(beanType)) {
					detectHandlerMethods(beanName);
				}

如果beanType 不为空,并且符合isHandler条件的,才能够进入里面的方法,所以我们先看看,什么是所谓的Handler呢?
在这里插入图片描述
就是说当前bean被标注为@Controller或者@RequestMapping注解的才会被扫描到。

那么@RestController 以及@PostMapping等注解会被扫描到么?答案是肯定的,还记得我们前面的小节讲到的注解的派生性么?可以简单的理解为子类与父类的关系,只是功能上等同哦,所以这里扫描@Controller或者@RequestMapping的父类是可以将子类扫描进去的,框架本身也更好的在注解层面实现了扩展。

然后看方法detectHandlerMethods,根据字面意思是,探测所有的HandlerMethods,所以进一步查看:
在这里插入图片描述
此方法首先求Bean,然后把Bean里的Method都执行一遍,注册HandlerMethod,进去看注册的过程:
在这里插入图片描述
透传参数,继续调用查看register方法:
在这里插入图片描述

由上述过程,可以总结一下HandlerMethod 初始化过程:

  1. Spring 应用上下文获取所有的 Bean
  2. 筛选出标注 @Controller 或者 @RequestMapping 的 Bean
  3. 再次筛选标准 @RequestMapping 方法
  4. 将该 Bean 和对应 Method 合并成 HandlerMethod
  5. 存入HandlerMethod 集合

HandlerMethod 定位过程

我们知道当一个Request请求进来时,要先经过 DispatchServlet 的doDispatch方法:
在这里插入图片描述
如何获取到的Handler呢?接着看源码的调用:
在这里插入图片描述
这里基本上相当于透传,接着往里看:
在这里插入图片描述
这不又是回到了最初的起点么?
我们通过前面的分析已经知道,实际从Request里解析的是:AbstractHandlerMapping,而AbstractHandlerMapping又是通过另外两个子类实现不同的返回值类型的Handler,这里我们前面已经提到。

这里以AbstractHandlerMehtodMapping 为切入点:我们看获取HandlerMethod时这段代码:
在这里插入图片描述
基本上分为三个部分,前面判断如果没有MatchingMappings,则从所有的mapping集合里取出来:

private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
		for (T mapping : mappings) {
			T match = getMatchingMapping(mapping, request);
			if (match != null) {
				matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping)));
			}
		}
	}

接下来是匹配合适的Handler最后将其返回。

由上述过程,可以总结一下HandlerMethod 定位过程:

  1. HandlerMethod 集合查找 @RequestMapping 定义的 URI
  2. 返回 HandlerMethod

HandlerMappingHandlerAdapter 区别

从DispatchServlet类的属性上我们看到,实际上1对多的关系有很多:
在这里插入图片描述
这里讲一下 HandlerMappingHandlerAdapter 区别:
HandlerMapping 主要负责映射,通过一次 HTTP 请求找到对应(最佳匹配规则)的 HandlerMethod 以及多个 HandlerInterceptor ,而 HandlerInterceptor 也等同与 HandlerExecutionChain,看一下此类的基本属性:
在这里插入图片描述
HandlerExecutionChain这熟悉的三件套和我们之前分析AbstractHandlerMapping不也是异曲同工么!

HandlerAdapter 主要负责 Handler 执行后处理,详细逻辑见org.springframework.web.servlet.DispatcherServlet#getHandler:

	@Nullable
	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {
			for (HandlerMapping hm : this.handlerMappings) {
				if (logger.isTraceEnabled()) {
					logger.trace(
							"Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
				}
				HandlerExecutionChain handler = hm.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}

HandlerMapping 的getHandler处理后返回HandlerExecutionChain。

HandlerInterceptor : 没有匹配请求,而MappedInterceptor : 能够匹配请求。

Spring MVC 2.5 之前面向接口编程

在老版本的页面渲染处理中, HTML页面渲染采用 JstlView, JSON渲染则采用 MappingJackson2JsonView

并且我们看Controller 接口定义:

 public interface Controller {

 	ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
 
 }

其方法返回值 ModelAndView,方法参数 HttpServletRequestHttpServletResponse

  • HandlerMapping 老实现:

Spring MVC 2.5+ 面向注解驱动编程

通常我们说 View 是来做页面渲染,同样的JstlView也是对其做的实现,但是,实际场景中。@Controller 处理方法可能不返回 ModelAndView,这种情况下怎么办呢?

这就是SpringMVC新版本提出的特性:就是通过 HandlerMethod 对于 ModelAndView 进行适配。

到了Spring Web MVC 2.5+ 以后,则引入了适配器(Adapter)与HandlerMethod的概念。前面我们也分析了HandlerMethod流转过程,那么HandlerMethod作为形参有什么样的优势呢?

  • HandlerMethod返回值:
    • ModelAndView
    • String
    • ResponseEntity
    • void
  • HandlerMethod参数:
    • @RequestParam
    • @RequestHeader
    • @PathVariable
    • @RequestBody
    • Model
  • HandlerMapping 新实现:RequestMappingHandlerMapping

这些参数的扩展相对于老版本不是更加灵活了么!

理解 WebFlux 实现

org.springframework.web.reactive.HandlerMapping

对比参考 org.springframework.web.servlet.HandlerMapping

org.springframework.web.reactive.HandlerAdapter

对比参考 org.springframework.web.servlet.HandlerAdapter

org.springframework.web.reactive.DispatcherHandler

对比参考 org.springframework.web.servlet.DispatcherServlet

Java 微服务实现方案

Vert.x

Vert.x 入门以及与SpringBoot的一些联动

Spring Boot / Spring Cloud

https://spring.io/projects/spring-cloud

后记

更多架构知识,欢迎关注本套Java系列文章Java架构师成长之路

;