前言
上回聊到了HandlerAdapter,今天继续聊后面的组件。今天的主角是HandlerMapping,这篇文章全为他服务了。
HandlerMapping
上回说的Handler,我们说是处理特定请求的。也就是说,不是所有的请求都能处理。那么问题来了,我们怎知道哪个请求是由哪个Handler处理的呢?
噔噔当,有请HandlerMapping闪亮登场。HandlerMapping就是处理uri到handler的映射的。从这个角度看他与Map有几分相似,都是key-value。然鹅,并不一样!Spring的命名,你不得不服,就这点也给你整的明明白白。他是Handler+Mapping。是映射Handler没错,但不是Map。我们的Handler可以处理特定的请求没错,但没说只能处理一个特定请求啊。也可能是一个类似的patern的一系列路径。先看看定义:
public interface HandlerMapping {
/**
* 获取Handler
*/
@Nullable
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}
public interface MatchableHandlerMapping extends HandlerMapping {
/**
* 匹配请求路径
*/
@Nullable
RequestMatchResult match(HttpServletRequest request, String pattern);
}
我们可以发现,他有两个接口,其中MatchableHandlerMapping是HandlerMapping的派生接口。多了请求匹配接口方法。
这里介绍以下他的三个重要实现:
实现 | 介绍 |
---|---|
SimpleUrlHandlerMapping | 用于简单映射URL到Handler。主要提供给客户定制使用的,因此不会自动配置 |
BeanNameUrlHandlerMapping | 如果Handler的beanName带“/”的,就是由他来进行映射的。他会自动扫描容器中的beanName来识别handler,自动建立映射关系。在没有配置HandlerMapping的情况下,会自动配置。 |
RequestMappingHandlerMapping | 看名字就知道,他是为了@RequestMapping进行映射的。在没有配置HandlerMapping的情况下,会自动配置 |
SimpleUrlHandlerMapping
PS:社区版只能这样看class diagrams了,勉强看吧。旗舰版的氪金大佬可以右键看氪金版的。
言归正传,SimpleUrlHandlerMapping特别简单,只要告诉他哪个请求由哪个handler处理就行。可以调用他的setMapping(Properties mapping)方法,或者是setUrlMap。前者会被转成Map给属性urlMap赋值,而后者则是直接对urlMap赋值。
可参考大佬的文章:SimpleUrlHandlerMapping用法
从继承关系看,他继承于AbstractUrlHandlerMapping。
AbstractUrlHandlerMapping实现了一系列基于url实现的HandlerMapping通用功能。主要体现在,他通过ApplicationContextAware接口获取到ApplicationContext,并且在该方法中获取handler的beanName并转换为实际handler对象。并注册到org.springframework.web.reactive.handler.AbstractUrlHandlerMapping#handlerMap
。得,到这里,应该知道,我们是在什么时候建立起URL跟Handler的关系了吧。
BeanNameUrlHandlerMapping
相较于SimpleUrlHandlerMapping,BeanNameUrlHandlerMapping则完全不需要用户自己定义url到handler的关系。,而是自动检测发现。前提是,你的handler在定义的时候beanName以“/”开头。而关于他的秘密也同样在他的继承关系图里:
与SimpleUrlHandlerMapping类似,他也是基于ApplicationContextAware接口进行扩展而来的能力。在获得上下文后,通过上下文遍历所有的beanName,就能知道这个bean是不是一个handler了。
小结
上面讲的都是基于AbstractUrlHandlerMapping扩展出来的HandlerMapping。而AbstractUrlHandlerMapping处理的都是基于URL来处理的,意味着不会有其他的可以匹配的条件。Handler注册中心则有两种。
Handler注册方式 | Handler注册中心 |
---|---|
基于URL 注册 | Map<String, Object> handlerMap = new LinkedHashMap<>(); |
基于pattern注册的 | Map<PathPattern, Object> pathPatternHandlerMap = new LinkedHashMap<>(); |
但都是简单Map就能搞定。
RequestMappingHandlerMapping
终于是到我们心心念念的@RequestMapping的Handler的Mapping了。为什么不叫MethodHandlerMapping呢?主要还是Handler是一个逻辑概念,MethodHandler了只是对目标方法进行了封装,并不是真正处理请求的。真正处理请求的是我们@RequestMapping的方法。还是那句话,取个名字都给你讲道理。
PS: 接下来会花很大篇幅来介绍他,因为他很重要绕不过去,他是我们常用的@RequestMapping的映射器。又因为功能强大而无法像上面的映射器那样三言两语讲清楚。想要让大家了解请求他的设计,只能花大功夫了。
前面我们知道HandlerMapping是用来寻找Handler的,并不与Handler的类型或者实现绑定,而是根据需要定义的。那么为什么要单独给@RequestMapping实现一个HandlerMapping?答案就在@RequestMapping本身。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
/**
* 处理器的名字,支持类级别和方法级别,多个路径用#分割
*/
String name() default "";
/**
* 匹配请求路径
*/
@AliasFor("path")
String[] value() default {};
/**
* 匹配请求路径
*/
@AliasFor("value")
String[] path() default {};
/**
* Http请求方法:可选GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE
*/
RequestMethod[] method() default {};
/**
* 匹配指定的地址栏参数
*/
String[] params() default {};
/**
* 匹配特定的header
*/
String[] headers() default {};
/**
* 匹配特定的Content-Type
*/
String[] consumes() default {};
/**
* 匹配特定的Accept
*/
String[] produces() default {};
}
发现了吗,各位?他除了能根据URI匹配,还能根据请求头、请求方法、甚至是请求参数来匹配!前面说的两种HandlerMapping都不能满足他,因此必须推出一个更加强大、可扩展性更强的HandlerMapping——RequestMappingHandlerMapping
那么问题来了:
- @RequestMapping是在什么如何被解析的呢?
我们很容易想到的就是,遍历容器中所有的对象,检查是否存在@Controller注解。存在,那就是控制器。然后接着遍历所有声明的public方法,检查是否存在@RequestMapping方法。这样,我们就找到了处理器方法。 - @RequestMapping是在什么时候被解析的呢?
本着“谁使用,谁解析”的原则,他自然是被RequestMappingHandlerMapping解析的。而又因为@RequestMapping的寻找可太费功夫,不可能在提供映射服务时再来解析,只能是初始化时进行解析。因此实现InitializingBean进行初始化,是个选择。
没错,实际上,SpringMVC跟你想的一样。在InitializingBean的afterPropertiesSet方法中,完成了以3下件事:
- 寻找@Controller的bean,并找到所有的@RequestMapping方法
- 解析@RequestMapping封装成RequestMappingInfo
- 将以上解析到的信息进行注册。信息包括:
信息 描述 @Controller/@RequestMapping对象 反射调用目标方法时,需要的target对象 RequestMappingInfo 由@RequestMapping解析而来 @RequestMapping的方法 注册时,注册器会将Method与handler对象一起封装成HandlerMethod进行注册。便于后面适配器调用。
@RequestMapping的注册
前面的解析@RequestMapping到RequestMappingInfo,可以省略,比较简单。但是@RequestMapping的注册没办法省略。因为如果搞不清楚他是怎么注册的,也就没办法理解他是怎么寻找目标处理器的。
为了支撑@RequestMapping多样化的匹配条件,不能再像前面两款HanderMapping一样,简单粗暴的使用Map了。在org.springframework.web.servlet.handler.AbstractHandlerMethodMapping
的内部定义了内部类,专门用来做注册中心,管理映射关系。
/**
* Mapping注册中心,可以理解为办事处
*/
class MappingRegistry {
/**
* T是匹配条件的对象。MappingRegistration是注册的信息,可以理解为你要做事情。
* 对于RequestMappingHandlerMapping,T就是RequestMappingInfo
* MappingRegistration包括信息:RequestMappingInfo、HandlerMethod、directPaths、mappingName、corsConfig
*/
private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
/**
* Map<path, RequestMappingInfo>
*/
private final MultiValueMap<String, T> pathLookup = new LinkedMultiValueMap<>();
/**
* Map<mappingName, List<HandlerMethod>>
*/
private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();
}
从他的属性,我们可以分析得到如下信息:
- 直接通过请求路径来查找处理器时,需要经过pathLookup中转registry,最后拿到MappingRegistration才到达HandlerMethod.
- 直接通过mappingName则可以直接找到HandlerMethod. 不过这个是Spring为了支持
<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>
而提供的。跟我们平时使用没多大关系。
但是各位观众老爷,pathLookup找到的是一个,而mappingName能找到多个,这是咋回事?误会啊,pathLookup的value可不是简单的一个元素,而是多个!他是MultiValueMap,不是我们经常看到的地摊货HashMap。他可以一个key对应多个value。
但是为什么会有多个呢?或者说为什么需要保存多个呢?
因为一个Handler可以处理多个请求,如果由多个Handler都能处理某一个请求的时候怎么办呢?况且SpringMVC还支持通配符匹配。umm…这一幕有点似曾相识,我们的nginx做路由转发的时候,不是也有类似的问题吗?这意味着在查找Handler的时候,我们还需要找到最佳的匹配。例如,/*相较于/hello,那肯定是/hello更精确,更合适啦。你看,多严谨!
得嘞,到这里我们就基本搞清楚RequestMappingHandlerMapping的初始化,以及他是怎么找到目标Handler了。
封装HandlerExecutionChain
此时,必须提醒一下大家,HandlerMapping接口的返回值是HandlerExecutionChain,并不是Handler。前面不管是说哪个HandlerMapping,都只是在说怎么寻找Handler、以及怎么注册。可没有说HandlerExecutionChain。其实,这得益于他们公共的父类方法org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler
。这也意味着,封装HandlerExecutionChain是在每个请求进来的时候才封装的。具体代码:
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
if (interceptor instanceof MappedInterceptor) {
MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
if (mappedInterceptor.matches(request)) {
chain.addInterceptor(mappedInterceptor.getInterceptor());
}
}
else {
chain.addInterceptor(interceptor);
}
}
return chain;
}
代码也比较简单。这里面还让我们多认识了MappedInterceptor,他可以针对特定的请求进行拦截。
好了,终于是把HandlerMapping讲完了。
后记
RequestMappingHandlerMapping使用了InitializingBean做初始化,但是当我们自己在做初始化的时候,尤其是使用多种初始化方式的时候,应当要注意Spring的调用顺序,否则有可能发生NPE,或者获取不到目标属性的情况。例如:同时在ApplicationContextAware、InitializingBean、@PostConstruct进行初始化。
为此,给大家找了官方的bean的生命周期
上一篇:
揭秘SpringMVC-DispatcherServlet之九大组件(一)
第一篇:
揭秘SpringMVC-DispatcherServlet启动-web上下文