本系列文章皆在分析SpringMVC
的核心组件和工作原理,让你从SpringMVC
浩如烟海的代码中跳出来,以一种全局的视角来重新审视SpringMVC
的工作原理.
思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜
前言
在上一章SpringMVC流程分析(三):MultipartResolver组件------SpringMVC中处理上传请求的关键中,我们通过对doDispatch
方法中checkMultipart
方法分析由浅入深的分析了MultipartResolver
组件。接下来,我们将继续沿着doDispatch
的调用链进行深入分析,而getHandler
背后的逻辑便是本文所讨论的内容。
下图展示了本系列文章重点分析的组件信息,其中 HandlerMapping
是本文分析的重点。
getHandler的处理逻辑
getHandler
的内部逻辑如下所示。首先,其会遍历全部的HanlderMapping
信息,然后进行匹配,如果找到一个合适的处理器信息,则返回一个HandlerExecutionChain
对象;如果未找到,则返回一个null
对象。我们注意到此时会涉及到一个我们之前未曾接触过的HandlerMapping
组件。
那这个HandlerMapping
的结构关系是什么样的?其又有什么用?此外,我们在使用SpringMVC
时我们并没有配置过HandlerMapping
的相关组件信息,那SpringMVC
默认又会在容器中添加那些HandlerMaping
的实现类?
java
protected HandlerExecutionChain getHandler(HttpServletRequest request)
throws Exception {
if (this.handlerMappings != null) {
// 遍历所有的HandlerMapping信息,寻找适配的处理器
for (HandlerMapping mapping : this.handlerMappings) {
// 根据请求寻找适合的处理器
HandlerExecutionChain handler = mapping.getHandler(request);
// 将适配的Hanlder进行返回,类型为HandlerExecutionChain
if (handler != null) {
return handler;
}
}
}
return null;
}
接下来,我们便带着这些问题进入到HanlderMapping
的体系中,看看其在处理请求的过程中到底做了哪些工作。
(注:本文重点分析HandlerMapping
,HandlerExecutionChain
的相关内容会在后续进行讨论)
走进HandlerMapping
在开始讨论HandlerMapping
之前,我们还是通过一个餐厅点菜的例子来帮助大家理解HandlerMapping
的功能。
当顾客来到一家餐厅吃饭,落座后向服务员点了几个餐厅的特色菜,服务员收到菜单后,随即便将菜谱分发给不同厨师进行烹饪,等烹调完毕后服务员将菜端到客人桌前。
在这一例子中,顾客点餐的这一动作可以看做是一个Http
请求,而服务员则相当于一个DispatcherServlet
。顾客点菜后,由服务将菜谱交给对应的厨师进行烹饪。而协助完成个这一动作的背后逻辑在于服务员的脑海中有一个记事本
,其记录了厨师和菜系之间的对应关系,因此可以很快的将订单准确的交给相对应的厨师。
类似地,在SpringMVC
中,HandlerMapping
(处理器映射)便是用来充当记事本
这一角色的一个组件,其主要用途在于将传入的HTTP
请求映射到相应的处理器方法上 。 具体而言,在SpringMVC
中,DispatcherServlet
需要处理分发很多请求,而每个请求通常对应一个特定的Handler
来进行处理,而接收到一个请求后,具体使用哪个Handler
来处理则需要通过HandlerMaping
进行处理。
总的来看,HanlderMapping
的主要工作就是根据前端传来的请求,然后找到合适的处理器 。 这句话将贯穿全文,这是理解HandlerMapping
作用的的关键。
HandlerMapping内部的方法
要想明白一个类的功能,首先关注便是其内部所具有方法。而HandlerMapping
内部的方法信息如下所示。
java
public interface HandlerMapping {
// ....省略一些其中的常量属性
@Nullable
HandlerExecutionChain getHandler(HttpServletRequest request)
throws Exception;
}
对于HandlerMapping
而言,其内部的getHandler方法就是通过request
来获取一个HandlerExecutionChain
(注:HandlerExecutionChain
相当于对处理器的一种封装,其内部会包含处理器handler
和拦截器interceptors
)。
HandlerMapping
的体系结构
了解了类中的方法后,下一步便是观察其的体系结构。而HandlerMapping
接口的体系如下所示:
通过上图可以注意到,HanlderMapping
为一个接口,其中提供的方法 getHandler
方法用于获取对应的处理器。这个继承关系虽然看着有点绕,其实仔细观察就两大类:
AbstractHandlerMethodMapping
AbstractUrlHandlerMapping
(注:MathableHandlerMapping
作为一个接口,主要用于判定请求url和定义路径是否匹配)
具体来看,AbstractHandlerMethodMapping
体系下的都是根据方法名进行匹配的,而 AbstractUrlHandlerMapping
体系下的都是根据 URL 路径进行匹配的。虽然两者在匹配url
时会使用不同的方式,但是这两者有一个共同的父类 AbstractHandlerMapping
。
更进一步,在Spring MVC
中,常用的HandlerMapping
实现包括:
RequestMappingHandlerMapping
: 基于@RequestMapping
注解来匹配处理器方法。它通过扫描@Controller
注解和@RequestMapping
注解来建立请求与处理器方法之间的映射关系。SimpleUrlHandlerMapping
: 基于URL
路径的匹配。通过配置URL与处理器方法的映射关系,可以将指定URL
请求映射到相应的处理器方法上。BeanNameUrlHandlerMapping
: 基于Bean
名称的匹配。它将请求的URL
路径与容器中的Bean
名称进行匹配,将匹配成功的请求映射到相应的处理器方法上。
公共父类:AbstractHandlerMapping
通过上述的类图信息,我们注意到AbstractHandlerMethodMapping
和AbstractUrlHandlerMapping
全部继承自AbstractHandlerMapping
这一抽象类。虽然AbstractHandlerMethodMapping
和AbstractUrlHandlerMapping
会通过两种不同的方式来进行URL
信息的匹配,但其内部一定会存在某些公共特征。而AbstractHandlerMapping
的作用便是对这些共性特征进行处理。同时留有扩展方法,方便子类对处理逻辑进行增强和扩展。
接下来,我们便深入AbstractHandlerMapping
内部,看看其究竟定义了哪些公共逻辑,该类相关内容如下:
java
public abstract class AbstractHandlerMapping
extends WebApplicationObjectSupport
implements HandlerMapping, Ordered, BeanNameAware {
// ......省略其他方法和成员变量
private final List<HandlerInterceptor> adaptedInterceptors = new ArrayList<>();
/***
* 根据reqeust信息获取对应处理器
*/
@Override
public final HandlerExecutionChain getHandler(HttpServletRequest request)
throws Exception {
// <1> 获得处理器(HandlerMethod 或者 HandlerExecutionChain),该方法是抽象方法,由子类实现
Object handler = getHandlerInternal(request);
//... 省略非空判断
//<2> 如果找到的处理器是 String 类型,则从 Spring 容器中找到对应的 Bean 作为处理器
if (handler instanceof String) {
String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}
// 构建一个HandlerExecutionChain (包含处理器和拦截器)
HandlerExecutionChain executionChain =
getHandlerExecutionChain(handler, request);
return executionChain;
}
/***
* 创建 HandlerExecutionChain 对象(包含处理器和拦截器)
*/
protected HandlerExecutionChain getHandlerExecutionChain(Object handler,
HttpServletRequest request) {
// <1> 创建 HandlerExecutionChain 对象
HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
// <2> 获得请求路径
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, LOOKUP_PATH);
// <3> 遍历 adaptedInterceptors 数组,获得请求匹配的拦截器
for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
//<3.1> 若路径匹配,则添加到 chain 中
if (interceptor instanceof MappedInterceptor) {
MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
chain.addInterceptor(mappedInterceptor.getInterceptor());
}
}+
else {
chain.addInterceptor(interceptor);
}
}
return chain;
}
上述代码反映了如下信息:
-
在
AbstractHandlerMapping
内部 维护了HandlerInterceptor列表 。首先,AbstractHandlerMapping
内部维护了一个HandlerInterceptor
列表,用于存储应用于该HandlerMapping
的拦截器。这些拦截器将应用于匹配到的请求处理器方法。 -
AbstractHandlerMapping
定义了默认的请求匹配逻辑 。首先,AbstractHandlerMapping
实现了HandlerMapping
接口中的核心方法,包括getHandler()
和getHandlerExecutionChain()
等方法。它提供了一些默认的请求匹配的整体流程。而不同匹配模式下URL的匹配逻辑则交由子类实现,所以要分析不同方法获取处理的方式,分析子类的getHandlerInternal
即可。
正如之前所说,SpringMVC
内部提供了两种不同的路径适配规则,一种是基于@ReqeustMapping
注解来匹配的方式,一种是通过URL
路径规则匹配的方式。
至此,本文已经对HandlerMapping
已经有了一个大致的介绍。具体如下,首先,我们以doDispatch
方法中的getHandler
为切入点, 分析了getHandler
的主要逻辑在于遍历HandlerMapping
,然后返回一个 HandlerExecutionChain
(其内部会包含处理器handler
和拦截器interceptors
)。
因此我们又一次将注意力从getHandler
方法转移到HandlerMapping
之上,分析了HandlerMapping
中定义的方法、研究了类的体系结构。进一步,通过分析HandlerMapping
的类结构关系,注意到其相关子类有一个公共父类,即AbstractHandlerMapping
。更进一步,研究了AbstractHandlerMapping
的相关逻辑,发现了不同匹配模式下URL
的匹配逻辑则交由子类实现,所以要分析不同匹配方式的区别,重点在于分析子类的getHandlerInternal
。
接下来,我们便继续深入探究两种匹配方式,对比分析这两种匹配方式在路径匹配上的区别。
基于路径的匹配
在Spring MVC
中,AbstractUrlHandlerMapping
是HandlerMapping
接口的一个抽象实现,用于基于URL路径进行请求处理器映射。
在开始分析路径匹配方式之前,不妨先考虑一个问题,如果让我们来设计实现一个基于路径的匹配寻找处理器的对象我们该如何进行设计? 通过前面分析,我觉得应该大致从如下几个方面进行考虑:
- 该类应该可以解析请求路径信息。 具体而言,当收到请求时该类会解析请求中的
URL
路径信息。 - 该类应该可以将路径信息同处理器进行匹配的功能。 只有对象内部存储了
URL
与处理器的映射关系,当进行适配时,才能快速将请求的URL路径映射到对应的处理器方法上。 - 该类可以提供URL路径匹配的功能。 其可以通过读取
Http
中的请求路径信息,并将请求的URL
路径与处理器进行匹配。从而找到最适合处理当前URL
请求的处理器。 - 该类可以获取处理器方法。 根据请求获取处理器这是
HandlerMapping
接口所规定的功能。
其中AbstractUrlHandlerMapping
的代码如下:
java
public class AbstractUrlHanlderMapping {
public void initApplicationContext()
throws ApplicationContextException {
super.initApplicationContext();
// 初始化处理器和url路径间的匹配
detectHandlers();
}
//... 省略其他无关代码
/**
* 侦测容器的bean,获取url规则,进行url和处理器之间关系的映射处理
* 通常子类会对这部分的逻辑进行重写
*/
protected void detectHandlers() throws BeansException {
//... 省略部分无关代码
String[] beanNames =
applicationContext.getBeanNamesForType(Object.class));
for (String beanName : beanNames) {
String[] urls = determineUrlsForHandler(beanName);
if (!ObjectUtils.isEmpty(urls)) {
// 完成url和处理器之间的映射关系处理
registerHandler(urls, beanName);
}
}
//... 日志处理相关代码
}
//... 省略其他无关代码
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
// <1> 获得请求的路径
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
// <2> 根据请求信息,获取对应的Handler信息
Object handler = lookupHandler(lookupPath, request);
// <3> 如果找不到处理器,则使用 rootHandler 或 defaultHandler 处理器
if (handler == null) {
Object rawHandler = null;
// <3.1> 如果是根路径,则使用 rootHandler 处理器
if ("/".equals(lookupPath)) {
rawHandler = getRootHandler();
}
// <3.2> 使用默认处理器
if (rawHandler == null) {
rawHandler = getDefaultHandler();
}
if (rawHandler != null) {
// Bean name or resolved handler?
// <3.3> 如果找到的处理器是 String 类型,则从容器中找到该 beanName 对应的 Bean 作为处理器
if (rawHandler instanceof String) {
String handlerName = (String) rawHandler;
rawHandler = obtainApplicationContext().getBean(handlerName);
}
// <3.4> 空方法,校验处理器。目前暂无子类实现该方法
validateHandler(rawHandler, request);
// <3.5> 创建处理器(HandlerExecutionChain 对象)
handler = buildPathExposingHandler(rawHandler,
lookupPath, lookupPath, null);
}
}
return handler;
}
}
在上述逻辑中detectHandlers
主要用于侦测容器中的处理器信息,并完成url
和处理器之间的映射关系。而对于getHandlerInternal
方法而则主要定义了根据url
获取处理器的处理逻辑,其大致内容如下:
- 获得请求路径
- 调用
lookupHandler(String urlPath, HttpServletRequest request)
方法,获得处理器。 - 如果找不到处理器,则使用
rootHandler
或defaultHandler
处理器 - 根据路径规则进行不同适配
- 如果是
/
根路径,则使用rootHandler
处理器;反之,则使用默认处理器 - 如果找到的处理器是
String
类型,则从容器中找到该 beanName 对应的 Bean 作为处理器 - 调用
validateHandler(Object handler, HttpServletRequest request)
,对处理器进行校验,空方法,暂无子类实现该方法 - 调用
buildPathExposingHandler
方法,创建HandlerExecutionChain
处理器执行链,赋值给handler
处理器,详情见下文
- 如果是
- 返回请求对应的
handler
处理器
通过上述分析可以知道,AbstractUrlHandlerMapping
内部的getHandlerInternal
定义了获取处理器的逻辑;而在initApplicationContext
的detectHandlers
方法则完成了url
路径和处理器间的映射关系。这也符合当初我们对于AbstractUrlHanlderMapping
功能的设计。
此外,如果你接触 Spring MVC 较早,可能见过 SimpleUrlHandlerMapping
和 BeanNameUrlHandlerMapping
中的使用示例的配置方式。当然,目前这种方式已经基本不用了,被 @RequestMapping
等注解的方式所取代。
基于方法的匹配
在Springmvc
提供了多种配置方式,除了通过url
匹配对应处理器,还可以通过url
匹配对应的方法来完成相应的处理逻辑。使用的注解信息如下所示:
进一步,AbstractHandlerMethodMapping
是HandlerMapping
接口的抽象实现之一,用于基于处理器方法的映射。进而确定应该由哪个处理器方法来处理该请求。相关代码如下:
java
public abstract class AbstractHandlerMethodMapping<T> extends
AbstractHandlerMapping implements InitializingBean {
/**
* 是否只扫描可访问的 HandlerMethod 们
*/
private boolean detectHandlerMethodsInAncestorContexts = false;
/**
* Mapping 命名策略
*/
@Nullable
private HandlerMethodMappingNamingStrategy<T> namingStrategy;
/**
* Mapping 注册表,此为AbstractHandlerMethodMapping的一个内部类信息
*/
private final MappingRegistry mappingRegistry = new MappingRegistry();
/**
* 获取处理器
*/
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
// <1> 获得请求的路径
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
// <2> 获得 HandlerMethod 对象
// lookupHandlerMethod根据url请求适配合适的hanlder
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath,
request);
// <3> 进一步,获得一个新的 HandlerMethod 对象
// 相当于从ioc容器中获取相应的处理器信息
return (handlerMethod != null ?
handlerMethod.createWithResolvedBean() : null);
}
/**
* 完成url路径信息和处理的映射关系适配
* 相关实现逻辑交给子类的getMappingForMethod完成
*/
protected void detectHandlerMethods(Object handler) {
// <1> 获得 Bean 对应的 Class 对象
Class<?> handlerType = (handler instanceof String ? obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {
// <2> 获得真实的 Class 对象,因为 `handlerType` 可能是代理类
Class<?> userType = ClassUtils.getUserClass(handlerType);
// <3> 获得匹配的方法和对应的 Mapping 对象
// 此处为一个lambda表达式:主要逻辑委托于getMappingForMethod
// 然后创建该方法对应的 Mapping 对象,例如根据 @RequestMapping 注解创建 RequestMappingInfo 对象
// 交给子类去进行实现
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
return getMappingForMethod(method, userType);
// <4> 遍历方法,逐个注册 HandlerMethod
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
}
可以看到,在AbstractHandlerMethodMapping
的内部,其所完成的任务和之前分析AbstractUrlHanlderMapping
时的功能基本相似。大致逻辑也是先注册url
和处理器的映射信息,然后在getHandlerInternal
中定义获取处理的逻辑。所以通过url
匹配对应处理器和还可以通过url
匹配对应的方法在匹配逻辑上本身没太大区别。两者的最大区别在于url
地址信息和处理器进行映射时的处理机制有所不同。
具体而言,在SpringMVC
中,RequestMappingHandlerMapping
使用MappingRegistry
来维护处理方法和URL
映射之间的关系,并用RequestMappingInfo
则是用于表示RequestMapping
的信息的对象。
进一步,RequestMappingInfo
是一个用于封装@RequestMapping
注解及其派生注解(如:@GetMapping、@PostMapping
等)的类。它包含了所有与请求映射相关的信息,例如URL模式
、HTTP
请求方法、请求参数、请求头等。
更进一步,方法和URL
映射之间的关系通过MappingRegistry
来维护。当请求到达时,根据请求的URL
和HTTP
方法,从保存的RequestMappingInfo
对象中找到合适的处理方法。它维护了一个数据结构,将请求的URL
和HTTP
方法作为键,与相应的RequestMappingInfo
对象。
简而言之,RequestMappingHandlerMapping
中的MappingRegistry
使用RequestMappingInfo
对象来建立请求映射与处理方法之间的对应关系。当请求到达时,RequestMappingHandlerMapping
会查询MappingRegistry
,找到匹配的RequestMappingInfo
对象,并根据其中的处理方法信息将请求分发到相应的方法进行处理。
(注:更具体的内容可查看RequstMappingHandlerMapping
中的相关定义)
SpringMVC默认配置的处理器
SpringMVC
这类框架在使用过程中,通常会遵守一种约定优于配置的原则,即通过一系列约定和默认配置来减少开发人员需要手动进行配置的工作,从而提高开发效率和降低代码复杂性。
所以SpringMVC
在初始化过程中,程序内部会加载很多的默认配置信息,而HandlerMapping
的相关信息便会根据其内部的默认配置信息来进行加载。
(注:SpringMVC
中组件的初始化会在initStrategies
中完成,而HandlerMapping
的相关初始化则会在依托于initHandlerMappings
方法进行实现,其中调用链如下所示。)
其中,initHandlerMappings
方法的逻辑如下所示。
DispatcherServlet#initHandlerMappings
java
private void initHandlerMappings(ApplicationContext context) {
// .....省略大量无关代码
// 加载默认的HandlerMapping信息
if (this.handlerMappings == null) {
// 通过getDefaultStrategies方法进行加载相关内容
this.handlerMappings = getDefaultStrategies(context,
HandlerMapping.class);
}
// .....省略大量无关代码
}
可以注意到,在initHandlerMappings
内部,其会将加载HandlerMapping
的逻辑委托于getDefaultStrategies
进行实现,具体代码如下:
DispatcherServlet#getDefaultStrategies
java
public class DiapatcherServlet {
// 配置文件的地址信息
private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";
// 初始化加载配置文件
static {
// 加载资源加载器
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH,
DispatcherServlet.class);
// 加载默认配置文件DispatcherServlet.properties
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
// ... 省略大量无关代码
protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
String key = strategyInterface.getName();
// 从配置文件中根据相关key信息获取对应value
String value = defaultStrategies.getProperty(key);
// 获取类信息,利用反射机制中的forName进行实例化处理
if (value != null) {
String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
List<T> strategies = new ArrayList<>(classNames.length);
for (String className : classNames) {
try {
Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
Object strategy = createDefaultStrategy(context, clazz);
// 将HandlerMapping相关信息添加到集合strategies中
strategies.add((T) strategy);
}
// 返回HandlerMappings构建的集合
return strategies;
}
// ... 省略大量的try-catch逻辑
}
}
getDefaultStrategies
的逻辑无非就是从配置DispatcherServlet.properties
文件加载HandlerMapping
的相关默认配置,然后利用反射机制对这些HandlerMapping
进行实例化,其中DispatcherServlet.properties
有关HandlerMapping
的配置内容如下所示:
DispatcherServlet.properties
(路径:org.springmvc.web.servlet)
properties
org.springframework.web.servlet.HandlerMapping=
org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
org.springframework.web.servlet.function.support.RouterFunctionMapping
不难发现,在SpringMVC
中默认会为我们配置BeanNameUrlHandlerMapping,RequestMappingHandlerMapping
等两个关键的HandlerMapping
组件,以满足我们日常开发中对于HandlerMapping
的使用。
如上就是SpringMVC
中即使不配置HandlerMapping
也可使使用HandlerMapping
功能的原理,这一切主要都归功于SpringMVC
内部为我们已经进行了大量的默认配置,从而可以让我们将关注点集中业务逻辑的开发。
总结
本文以doDispahtch
方法中调用的getHandler
为切入点,由浅入深的分析了HandlerMapping
的组件的功能、类结构关系等内容。在此基础上,我们深入讨论了SpringMVC
中的不同匹配方式之间的区别。虽然在本文的讨论中会涉及到很多类的名称,但不要慌,这些类的么名称并不重要,你只需要记住: " HanlderMapping
的主要工作就是根据前端传来的请求,然后找到合适的处理器。无论类中的体系结构如何复杂,其核心一定是服务于这一目标。"