JAVA && Spring && SpringBoot2.x — 学习目录
Spring源码篇(1)—RequestMappingHandlerMapping(Handler的注册)
Spring源码篇(2)—RequestMappingInfo与RequestCondition(Handler的映射)
SpringBoot2.x—定制HandlerMapping映射规则
1. SpringMVC中的HandlerMapping
组件 | 名称 | 作用 |
---|---|---|
DispatcherServlet | 前端控制器 | 接受请求,响应结果 |
HandlerMapping | 处理器映射器 | 根据请求URL查找handler,获取Handler链 |
HandlerAdapter | 处理器适配器 | 按照特定规则,去执行handler |
Handler | 处理器(Controller) | 接受用户请求,调用业务处理方法 |
ViewResolver | 视图解析器 | 进行视图解析,将逻辑视图解析为物理视图 |
View | 视图 | 将数据展示给用户的页面,例如JSP、freemarker |
根据上述描述,HandlerMapping实际上完成了两件工作:
- 根据请求获取到HandlerMethod,即定位到Controller层的那个方法进行处理;
- 将HandlerMethod与Interceptor封装为一个HandlerExecutionChain。
1.1 HandlerMapping接口
HandlerMapping是一个接口类,作用便是根据request获取HandlerExecutionChain
,其接口方法如下所示:
public interface HandlerMapping {
@Nullable
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}
1.2 本篇讨论的重点
该接口只有一个方法,便是返回HandlerExecutionChain,那么HandlerMapping中的对象是Handler是怎么被缓存,以便后续可以快速进行映射?
2. Handler注册原理
1. @RequestMapping标签的属性
请求与容器中的@RequestMapping注解参数进行匹配,获取到最符合条件的HandlerMethod。那么@RequestMapping的参数是什么?
属性 | 作用 | 示例值 |
---|---|---|
name | 名字 | 默认策略:类名#方法名
|
path/value | 映射路径 | value = "test" |
method | 方法类型 | method = RequestMethod.GET |
params | 请求参数 | params = "myParam=myValue" |
headers | 请求头 | headers = "Referer=http://www.baidu.com" |
consumes | 请求的content-type字段 | consumes = "text/plain" |
produces | 请求的accept字段 | produces = {"text/plain", "application/*"} |
在项目启动时,会将方法上的@RequestMapping
会被转换为RequestMappingInfo
对象,保存起来,以便后续request
请求与给定的RequestMappingInfo
进行匹配。
并且RequestMappingInfo对象预留了一个属性,用于用户统一的为所有@RequestMapping注解方法配置。
2. Handler注册的流程
项目启动之后,Spring可以获取到容器中的所有Bean对象。而RequestMappingHandlerMapping
是@RequestMapping
标签的处理器映射器。
- 根据@Controller和@RequestMapping标签筛选Bean;
- 根据@RequestMapping标签来筛选Bean中所有方法,将
@RequestMapping
标签解析为RequestMappingInfo
对象。 -
RequestMappingInfo
和method
对象进行注册
3. RequestMappingInfo对象
在映射HandlerMethod时,通过url获取到一组RequestMappingInfo
对象,而后每个mapping
对象都要和request
的属性进行匹配。再次获取到一组RequestMappingInfo
对象,使用比较器获取到一组最优的RequestMappingInfo
对象。
上文所述,@RequestMappingInfo
注解最终解析为RequestMappingInfo
对象:
public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
@Nullable
private final String name;
//请求路径条件(该对象完成了url合并,筛选url,择优url)
private final PatternsRequestCondition patternsCondition;
//请求方法条件
private final RequestMethodsRequestCondition methodsCondition;
//请求参数条件
private final ParamsRequestCondition paramsCondition;
//请求头条件
private final HeadersRequestCondition headersCondition;
//请求content-type条件
private final ConsumesRequestCondition consumesCondition;
//请求accept条件
private final ProducesRequestCondition producesCondition;
//自定义请求条件
private final RequestConditionHolder customConditionHolder;
}
处理@RequestMapping
含有的属性,SpringMVC
为我们提供了一个自定义条件,用户可以进行统一的配置,灵活的筛选Controller中方法执行业务。
4. 项目启动时注册了什么信息
源码:org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry
class MappingRegistry {
//Key:RequestMappingInfo(映射信息)。value:mapping、handlerMethod、directUrls、mappingName
private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
//key:Key:RequestMappingInfo(映射信息)。value:HandlerMethod
private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
//Key:请求路径。value:映射信息
private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
//Key:mappingName(默认策略:类名#方法名)。value:一组HandlerMethod
private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();
//key:HandlerMethod。value:cors(跨域资源共享策略)
private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
}
映射关系:url->mapping->handlerMethod->corsConfiguration。故这些信息都要被注册。
2. Handler的注册源码
源码:org.springframework.web.servlet.handler.AbstractHandlerMethodMapping
2.1 根据子类筛选Bean对象
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#isHandler
中,若是该类上存在@Controller
注解或者@RequestMapping
注解的类均可以通过。
2.2 根据子类获取MappingInfo对象
Spring拦截请求是方法级别的。processCandidateBean方法中筛选出容器所有的@Controller
或@RequestMapping
类。对方法的筛选(甚至可以理解为将Handler转化为Mapping)还是需要在子类中实现的。
-
AbstractHandlerMethodMapping
的(抽象)钩子方法。RequestMappingHandlerMapping
便是根据方法或类上是否存在@RequestMapping
注解来决定是否解析该方法。 - 注解解析完毕后,将类注解和方法注解进行合并。
@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
//判断方法上是否存在@RequestMapping注解,存在便解析为RequestMappingInfo对象
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
//判断类上是否存在@RequestMapping注解,存在便解析为RequestMappingInfo对象
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
//两个对象进行合并
info = typeInfo.combine(info);
}
String prefix = getPathPrefix(handlerType);
if (prefix != null) {
info = RequestMappingInfo.paths(prefix).build().combine(info);
}
}
return info;
}
@Nullable
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);
}
用户继承RequestMappingHandlerMapping
类重写getCustomMethodCondition
和getCustomTypeCondition
方法,自定义RequestCondition
对象。
优点:
request
不仅可以@RequestMapping
属性进行匹配(当然匹配的规则是固定的),还可以使用自定义规则进行匹配。一般在@RequestMapping
类/方法上使用自定义注解。这样在解析含有@RequestMapping
注解的类/方法时,用户便可以解析自定义注解的值,从而创建自定义的RequestCondition
对象。
本质上,由@RequestMapping
注解的属性来决定mapping
的属性条件,转换为由自定义注解来决定mapping
的属性条件。
例如:API版本控制,前提是不同的版本需要相同的url,但根据请求需要定位到不同的业务方法上。可以在@RequestMapping中定义
headers
属性。但是源码中根据headers
属性进行精确匹配(即完全相等)。
若是只有0.1和0.2个版本,用户上送0.3也要匹配到0.2版本,那么应该如何处理?
便要使用getCustomMethodCondition
创建自定义RequestCondition
对象,以便完成自定义业务的筛选。
2.3 注册mapping和handler
那么mapping对象的结构是怎样的呢,其实便是将@RequestMapping
注解中的属性封装为mapping对象。而在注册的时候,mapping可以作为key。根据用户request的url、header、param等属性来匹配mapping对象。
public void register(T mapping, Object handler, Method method) {
this.readWriteLock.writeLock().lock();
try {
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
//若是解析@RequestMapping方法得到的mapping对象是完全相同的,则会在启动的时候出现异常。
assertUniqueMethodMapping(handlerMethod, mapping);
this.mappingLookup.put(mapping, handlerMethod);
List<String> directUrls = getDirectUrls(mapping);
for (String url : directUrls) {
//将url和mapping进行绑定
this.urlLookup.add(url, mapping);
}
String name = null;
if (getNamingStrategy() != null) {
//根据命名策略,获取名字:类名#方法名
name = getNamingStrategy().getName(handlerMethod, mapping);
addMappingName(name, handlerMethod);
}
//判断方法/类上是否存在@CrossOrigin,若存在,将跨域配置与mapping绑定
CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
if (corsConfig != null) {
this.corsLookup.put(handlerMethod, corsConfig);
}
this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
}
finally {
this.readWriteLock.writeLock().unlock();
}
}
在项目中不应该存在完全相同的mapping对象,否则的话,会在启动的时候出现异常。
比如在项目中存在多个版本的接口。这些版本使用的url路径必须相同,我们可以在@RequestMapping
中定义header
属性来区分版本。这样即使存在相同的url参数,但由于每个@RequestMapping
注解的方法header参数不同。那么也属于不同的RequestMappingInfo
对象。即DispatcherServlet可以通过request确定唯一的handler对象。
官网文档
RequestMappingHandlerMapping API文档
推荐阅读
Spring MVC之RequestMappingHandlerMapping匹配
Spring Mvc之定制RequestMappingHandlerMapping
springboot 自定义 RequestMappingHandlerMapping / HandlerInterceptor 报错
RequestMappingHandlerMapping的用法