Spring Flux中Request与HandlerMapping关系的形成过程
一、前言
Spring Flux中的核心DispatcherHandler的处理过程分为三步,其中首步就是通过HandlerMapping接口查找Request所对应的Handler。本文就是通过阅读源码的方式,分析一下HandlerMapping接口的实现者之一——RequestMappingHandlerMapping类,用于处理基于注解的路由策略,把所有用@Controller和@RequestMapping标记的类中的Handler识别出来,以便DispatcherHandler调用的。
HandlerMapping接口的另外两种实现类:1、RouterFunctionMapping用于函数式端点的路由;2、SimpleUrlHandlerMapping用于显式注册的URL模式与WebHandler配对。
文章系列
- 关于非阻塞IO:《从时间碎片角度理解阻塞IO模型及非阻塞模型》
- 关于SpringFlux新手入门:《快速上手Spring Flux框架》
- Spring Flux中Request与HandlerMapping关系的形成过程 本文
二、对基于注解的路由控制器的抽象
Spring中基于注解的控制器的使用方法大致如下:
@Controller public class MyHandler{ @RequestMapping("/") public String handlerMethod(){ } }
在Spring WebFlux中,对上述使用方式进行了三层抽象模型。
-
Mapping
- 用户定义的基于annotation的映射关系
- 该抽象对应的类是:org.springframework.web.reactive.result.method.RequestMappingInfo
- 比如上述例子中的 @RequestMapping("/")所代表的映射关系
-
Handler
- 代表控制器的类
- 该抽象对应的类是:java.lang.Class
- 比如上述例子中的MyHandler类
-
Method
- 具体处理映射的方法
- 该抽象对应的类是:java.lang.reflect.Method
- 比如上述例子中的String handlerMethod()方法
基于上述三层抽象模型,进而可以作一些组合。
-
HandlerMethod
- Handler与Method的结合体,Handler(类)与Method(方法)搭配后就成为一个可执行的单元了
-
Mapping vs HandlerMethod
- 把Mapping与HandlerMethod作为字典存起来,就可以根据请求中的关键信息(路径、头信息等)来匹配到Mapping,再根据Mapping找到HandlerMethod,然后执行HandlerMethod,并传递随请求而来的参数。
理解了这个抽象模型后,接下来分析源码来理解Spring WebFlux如何处理请求与Handler之间的Mapping关系时,就非常容易了。
HandlerMapping接口及其各实现类负责上述模型的构建与运作。
三、HandlerMapping接口实现的设计模式
HandlerMapping接口实现,采用了"模版方法"这种设计模式。
1层:AbstractHandlerMapping implements HandlerMapping, Ordered, BeanNameAware
^ |
2层:AbstractHandlerMethodMapping implements InitializingBean
^ |
3层:RequestMappingInfoHandlerMapping
^ |
4层:RequestMappingHandlerMapping implements EmbeddedValueResolverAware
下面对各层的职责作简要说明:
-
第1层主要实现了对外提供模型的接口
- 即重载了HandlerMapping接口的"Mono
- 即重载了HandlerMapping接口的"Mono
-
第2层有两个责任 —— 解析用户定义的HandlerMethod + 实现对外提供模型接口实现所需的抽象方法
- 通过实现了InitializingBean接口的"void afterPropertiesSet()"方法,解析用户定义的Handler和Method。
- 实现第1层对外提供模型接口实现所需的抽象方法:"Mono<?> getHandlerInternal(ServerWebExchange exchange)"
- 第3层提供根据请求匹配Mapping模型实例的方法
- 第4层实现一些高层次用到的抽象方法来创建具体的模型实例。
小结一下,就是HandlerMapping接口及其实现类,把用户定义的各Controller等,抽象为上述的Mapping、Handler及Method模型,并将Mapping与HandlerMethod作为字典关系存起来,还提供通过匹配请求来获得HandlerMethod的公共方法。
接下来的章节,将先分析解析用户定义的模型并缓存模型的过程,然后再分析一下匹配请求来获得HandlerMethod的公共方法的过程。
四、解析用户定义的模型并缓存模型的过程
4-1、实现InitializingBean接口
第2层AbstractHandlerMethodMapping抽象类中的一个重要方法——实现了InitializingBean接口的"void afterPropertiesSet()"方法,为Spring WebFlux带来了解析用户定义的模型并缓存模型的机会 —— Spring容器初初始化完成该类的具体类的Bean后,将会回调这个方法。
在该方法中,实现获取用户定义的Handler、Method、Mapping以及缓存Mapping与HandlerMethod映射关系的功能。
@Override public void afterPropertiesSet() { initHandlerMethods(); // Total includes detected mappings + explicit registrations via registerMapping.. ... }
4-2、找到用户定义的Handler
afterPropertiesSet方法中主要是调用了void initHandlerMethods()方法,具体如下:
protected void initHandlerMethods() { //获取Spring容器中所有Bean名字 String[] beanNames = obtainApplicationContext().getBeanNamesForType(Object.class); for (String beanName : beanNames) { if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { Class<?> beanType = null; try { //获取Bean的类型 beanType = obtainApplicationContext().getType(beanName); } catch (Throwable ex) { // An unresolvable bean type, probably from a lazy bean - let's ignore it. if (logger.isTraceEnabled()) { logger.trace("Could not resolve type for bean '" + beanName + "'", ex); } } //如果获取到类型,并且类型是Handler,则继续加载Handler方法。 if (beanType != null && isHandler(beanType)) { detectHandlerMethods(beanName); } } } //初始化后收尾工作 handlerMethodsInitialized(getHandlerMethods()); }
这儿首先获取Spring容器中所有Bean名字,然后循环处理每一个Bean。如果Bean名称不是以SCOPED_TARGET_NAME_PREFIX常量开头,则获取Bean的类型。如果获取到类型,并且类型是Handler,则继续加载Handler方法。
isHandler(beanType)调用,检查Bean的类型是否符合handler定义。
AbstractHandlerMethodMapping抽象类中定义的抽象方法"boolean isHandler(Class<?> beanType)",是由RequestMappingHandlerMapping类实现的。具体实现代码如下:
protected boolean isHandler(Class<?> beanType) { return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class)); }
不难看出,对于RequestMappingHandlerMapping这个实现类来说,只有拥有@Controller或者@RequestMapping注解的类,才是Handler。(言下之意对于其他实现类来说Handler的定义不同)。
具体handler的定义,在HandlerMapping各实现类来说是不同的,这也是isHandler抽象方法由具体实现类来实现的原因。
4-3、发现Handler的Method
接下来我们要重点看一下"detectHandlerMethods(beanName);"这个方法调用。
protected void detectHandlerMethods(final Object handler) { Class<?> handlerType = (handler instanceof String ? obtainApplicationContext().getType((String) handler) : handler.getClass()); if (handlerType != null) { //将handlerType转换为用户类型(通常等同于被转换的类型,不过诸如CGLIB生成的子类会被转换为原始类型) final Class<?> userType = ClassUtils.getUserClass(handlerType); //寻找目标类型userType中的Methods,selectMethods方法的第二个参数是lambda表达式,即感兴趣的方法的过滤规则 Map<Method, T> methods = MethodIntrospector.selectMethods(userType, //回调函数metadatalookup将通过controller定义的mapping与手动定义的mapping合并起来 (MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType)); if (logger.isTraceEnabled()) { logger.trace("Mapped " + methods.size() + " handler method(s) for " + userType + ": " + methods); } methods.forEach((key, mapping) -> { //再次核查方法与类型是否匹配 Method invocableMethod = AopUtils.selectInvocableMethod(key, userType); //如果是满足要求的方法,则注册到全局的MappingRegistry实例里 registerHandlerMethod(handler, invocableMethod, mapping); }); } }
首先将参数handler(即外部传入的BeanName或者BeanType)转换为Class<?>类型变量handlerType。如果转换成功,再将handlerType转换为用户类型(通常等同于被转换的类型,不过诸如CGLIB生成的子类会被转换为原始类型)。接下来获取该用户类型里所有的方法(Method)。循环处理每个方法,如果是满足要求的方法,则注册到全局的MappingRegistry实例里。
4-4、解析Mapping信息
其中,以下代码片段有必要深入探究一下
Map<Method, T> methods = MethodIntrospector.selectMethods(userType, (MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType));
MethodIntrospector.selectMethods方法的调用,将会把用@RequestMapping标记的方法筛选出来,并交给第二个参数所定义的MetadataLookup回调函数将通过controller定义的mapping与手动定义的mapping合并起来。
第二个参数是用lambda表达式传入的,表达式中将method、userType传给getMappingForMethod(method, userType)方法。
getMappingForMethod方法在高层次中是抽象方法,具体的是现在第4层RequestMappingHandlerMapping类中实现。在具体实现getMappingForMethod时,会调用到RequestMappingHandlerMapping类的下面这个方法。从该方法中,我们可以看到,首先会获得参数element(即用户在Controller中定义的方法)的RequestMapping类型的类实例,然后构造代表Mapping抽象模型的RequestmappingInfo类型实例并返回。
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); RequestCondition<?> condition = (element instanceof Class ? getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element)); return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null); }
构造代表Mapping抽象模型的RequestmappingInfo类型实例,用的是createRequestMappingInfo方法,如下。可以看到RequestMappingInfo所需要的信息,包括paths、methods、params、headers、consumers、produces、mappingName,即用户定义@RequestMapping注解时所设定的可能的参数,都被存在这儿了。拥有了这些信息,当请求来到时,RequestMappingInfo就可以测试自身是否是处理该请求的人选之一了。
protected RequestMappingInfo createRequestMappingInfo( RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) { RequestMappingInfo.Builder builder = RequestMappingInfo .paths(resolveEmbeddedValuesInPatterns(requestMapping.path())) .methods(requestMapping.method()) .params(requestMapping.params()) .headers(requestMapping.headers()) .consumes(requestMapping.consumes()) .produces(requestMapping.produces()) .mappingName(requestMapping.name()); if (customCondition != null) { builder.customCondition(customCondition); } return builder.options(this.config).build(); }
4-5、缓存Mapping与HandlerMethod关系
最后,registerHandlerMethod(handler, invocableMethod, mapping)调用将缓存HandlerMethod,其中mapping参数是RequestMappingInfo类型的。。
内部调用的是MappingRegistry实例的void register(T mapping, Object handler, Method method)方法,其中T是RequestMappingInfo类型。
MappingRegistry类维护所有指向Handler Methods的映射,并暴露方法用于查找映射,同时提供并发控制。
public void register(T mapping, Object handler, Method method) { this.readWriteLock.writeLock().lock(); try { HandlerMethod handlerMethod = createHandlerMethod(handler, method); ...... this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name)); } finally { this.readWriteLock.writeLock().unlock(); } }
五、匹配请求来获得HandlerMethod
AbstractHandlerMethodMapping类的“Mono getHandlerInternal(ServerWebExchange exchange)”方法,具体实现了根据请求查找HandlerMethod的逻辑。
@Override public Mono<HandlerMethod> getHandlerInternal(ServerWebExchange exchange) { //获取读锁 this.mappingRegistry.acquireReadLock(); try { HandlerMethod handlerMethod; try { //调用其它方法继续查找HandlerMethod handlerMethod = lookupHandlerMethod(exchange); } catch (Exception ex) { return Mono.error(ex); } if (handlerMethod != null) { handlerMethod = handlerMethod.createWithResolvedBean(); } return Mono.justOrEmpty(handlerMethod); } //释放读锁 finally { this.mappingRegistry.releaseReadLock(); } }
handlerMethod = lookupHandlerMethod(exchange)调用,继续查找HandlerMethod。我们继续看一下HandlerMethod lookupHandlerMethod(ServerWebExchange exchange)方法的定义。为方便阅读,我把注释也写在了代码里。
protected HandlerMethod lookupHandlerMethod(ServerWebExchange exchange) throws Exception { List<Match> matches = new ArrayList<>(); //查找所有满足请求的Mapping,并放入列表mathes addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, exchange); if (!matches.isEmpty()) { //获取比较器comparator Comparator<Match> comparator = new MatchComparator(getMappingComparator(exchange)); //使用比较器将列表matches排序 matches.sort(comparator); //将排在第1位的作为最佳匹配项 Match bestMatch = matches.get(0); if (matches.size() > 1) { //将排在第2位的作为次佳匹配项 Match secondBestMatch = matches.get(1); } handleMatch(bestMatch.mapping, bestMatch.handlerMethod, exchange); return bestMatch.handlerMethod; } else { return handleNoMatch(this.mappingRegistry.getMappings().keySet(), exchange); } }
六、总结
理解了Spring WebFlux在获取映射关系方面的抽象设计模型后,就很容易读懂代码,进而更加理解框架的具体处理方式,在使用框架时做到“知己知彼”。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
技术| Python的从零开始系列连载(三十二)
综述最近山大软件园校区QLSC_STU无线网掉线掉的厉害,连上之后平均十分钟左右掉线一次,很是让人心烦,还能不能愉快地上自习了?能忍吗?反正我是不能忍了,嗯,自己动手,丰衣足食!写个程序解决掉它! 假若你不能连这个无线,那就照照思路啦~ 决战前夕首先我们看一下那个验证页面是咋样滴,上个图先嘿,这界面还算可以把,需要我们输入的东西就是俩,一个就是学号,另一个是身份证号后六位,然后就可以登录,享受免费的无线网啦。 不过不知道谁设置了个登录时长,一段时间后就会掉线了,于是,自动模拟登陆系统就要应运而生啦。 来,我们先点击一下连接,看一下浏览器怎么工作的。 按下F12,监听网络,我们点击第一个响应,也就是login.jsp,看一下。 我们具体看一下headers,里面form提交了什么东西,真的是茫茫多的数据啊。 嗯,一目了然POST的数据和提交的地址。 让我们来分析几个数据吧:ClientIP:当前客户端的IP地址,在山大软件园校区这个地址是211.87开头的 timeoutvalue:连接等待时间,也就是俗话说的timeout StartTime:登录时间,也就是在你登录的那一刻的时间戳,...
- 下一篇
Java零基础入门学习1:初探Java世界
在我们的编程世界中,Java可以说是不可或缺的一部分,它可以实现C++中很多不能实现的地方,使用也与C++有一定差别。学过C或C++的,Java上手很快。如果你之前没学过C或C++,也没关系。我呢将用通俗的语言,生活中的栗子,来帮助大家理解、学习好Java编程。 有一句经典的话:计算机编程极其困难,它要求你获得计算机科学专业的学位,需要投入几千美元来购买计算机硬件和软件,需要极强的分析能力,需要有耐心,而且对含咖啡因的饮料有强烈的爱好。 而这句话除了最后一个,全是错的。我喜欢喝可乐,并发现百事比可口好喝,这是我当程序员来最大的收获之一,尽管不是很健康。 其实,编程不难,只要有兴趣,熟练掌握没问题。当你面对计算机时,你想告诉他,I love programming! 那么,你就可以用程序来坐到它。 首先,我们需要一个编程环境,也就是IDE,集成开发环境。一个友好的IDE能使人心情舒畅,愉悦,对做题有很大的帮助。 我们在这里一律采用Netbeans,可在这里下载。 首先,新建一个项目。 然后,选择类型Java。 之后给它命名为你喜欢的名字。这里以Java_class为例。 建好之后,我们按...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Mario游戏-低调大师作品
- CentOS6,CentOS7官方镜像安装Oracle11G
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Windows10,CentOS7,CentOS8安装Nodejs环境
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS8编译安装MySQL8.0.19
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7