tomcat + spring mvc原理(十):spring mvc如何将请求投送到Controller中的方法2
前言
原理九介绍了HandlerMapping组件的接口标准和AbstractHandlerMapping抽象类的主要功能——加载相关的Interceptor。本期主要讲HandlerMethodMapping子类的实现细节,具体是如何获取Controller中的HandlerMethod的过程,从而完成对HandlerMapping组件@RequestMapping注解对应的请求投送。
AbstractHandlerMethodMapping和其子类
所谓的HandlerMethodMapping指的就是将请求映射到处理该请求的Method,即使用@RequestMapping、@GetMapping或者@PostMapping注解的方法,在这里有专门的类型HandlerMethod与之对应,现在项目常用的都是这种方式。首先在看代码之前,自行想一下如果简单实现这样一种映射要如何做。一般是使用map,将匹配条件(比如url)和method组成键值对。有时候可能一个匹配条件可以匹配多个HandlerMethod(然后选出一个最优的),所以一对一的映射可能解决不了问题。实际上,AbstractHandlerMethodMapping使用的也是map的这种思路,接下来看它是如何处理这个问题的。
匹配条件
在理解如何匹配到我们自定义的方法(由@RequestMapping、@GetMapping或者@PostMapping注解的)之前,先要了解一下匹配条件有哪些以及这些匹配条件是如何实现的。匹配条件的理解需要追溯到@RequestMapping这个注解。
public @interface RequestMapping {
String name() default "";
@AliasFor("path")
String[] value() default {};
@AliasFor("value")
String[] path() default {};
RequestMethod[] method() default {};
String[] params() default {};
String[] headers() default {};
String[] consumes() default {};
String[] produces() default {};
}
其中name、path、value都是指url,method是指请求的类型,GET、POST、PUT、DELETE等;params是指定request中必须包含某些参数值;headers是指request中必须包含某些指定的header值;consumes代表限定请求的提交内容类型(Content-Type),类型如application/json, text/html;produces指定返回内容的类型,需要在请求头中(Accept属性)包含指定类型。这些都是属于匹配条件。
spring mvc为这些匹配条件定义了一个通用的接口RequestCondition。
public interface RequestCondition<T> {
//将当前匹配条件和另一个匹配条件结合,比如url类型的和method类型
T combine(T other);
//获取匹配request的匹配条件,比如一个匹配条件是多个值的url匹配,
/该方法会返回匹配传入request的匹配url条件。
@Nullable
T getMatchingCondition(HttpServletRequest request);
//对比在当前传入request条件下两个匹配条件。该方法假设两个匹配
//条件是通过getMatchingCondition产生的,和这个request有相
//关关系。
int compareTo(T other, HttpServletRequest request);
}
这个接口的抽象类实现AbstractRequestCondition主要重载了equals()、hashCode()和toString()方法,其子类代表了具体的6种类型的匹配条件。
你应该发现了,实际上子类有8个。前六个:
类名 | 功能 |
---|---|
PatternsRequestCondition | url匹配 |
RequestMethodsRequestCondition | 请求类型匹配 |
HeadersRequestCondition | header属性匹配 |
ParamsRequestCondition | 参数值匹配 |
ProducesRequestCondition | 返回内容类型匹配 |
ConsumesRequestCondition | 请求提交类型匹配 |
最后CompositeRequestCondition代表是一个复合类型,可以将其他匹配类型封装到自己的一个类实例中。RequestConditionHolder是一个好东西,我记得在原理八有
这些属性、配置都存储在当前环境中,可以通过Holder获取。也就是说,在处理请求的过程中RequestContextHolder能够获取所有和请求相关的内容,大家在业务代码编写中也可以使用LocaleContextHolder和RequestContextHolder这两个Holder。
RequestConditionHolder是一样的道理,也可以在编写业务代码时获取请求条件匹配的信息,算是框架提供的一个环境参数获取的工具。这些子类实现了接口的方法逻辑,可以保证在HandlerMethodMapping中被正常调用,代码不是非常复杂,大家有兴趣可以自己去看。
AbstractHandlerMethodMapping及其子类初始化
AbstractHandlerMethodMapping是一个泛型类,定义了一个MappingRegistry内部类,并且内部初始化并持有了一个
MappingRegistry的类对象。顾名思义,MappingRegistry这个类的主要功能就是管理注册匹配条件的map。
class MappingRegistry {
private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();
private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
}
首先理解T代表什么。T是AbstractHandlerMethodMapping中定义的泛型,统一用来代表匹配条件。urlLookupMap用来存储url和匹配条件的映射关系,url是pattern样式的,因此可以使用通配符。这里的map是MultiValue类型,一个键对应多个匹配条件。mappingLookup是用来存储匹配条件和HanlerMethod(Controller下被@RequestMapping注解方法的专用类型)的映射关系。nameLookup里的name是使用HandlerMethodMappingNamingStrategy策略的实现类从HandlerMethod中解析出来的,是用在mvc URI解析过程的变量。对于corsLookup,在这篇文章的上半部中(原理九)有提到跨站请求,这里就是HandlerMethod和跨站请求配置的映射,当然是在该请求为跨站请求的前提下才会用到该映射。registry会把除了corsLookup映射的其他所有映射统一到一起,这样registry的每一条记录中会包括匹配条件、对应HandlerMethod、完整url和映射名,其中匹配条件匹配条件和HandlerMethod不能为空。
urlLookup和mappingLookup相互配合,先通过url获取匹配条件,再通过匹配条件获取HandlerMethod。有时候获取的匹配条件可能不止一个,这就需要选出最优的一个。
AbstractHandlerMethodMapping初始化的方式是实现了InitializingBean接口,这个接口只有一个方法。
public interface InitializingBean {
void afterPropertiesSet() throws Exception;
}
这个接口的神奇之处在于,当BeanFactory把一个bean内的所有属性都装备好之后,这个接口唯一的方法就会自动被调用。AbstractHandlerMethodMapping在自己实现的afterPropertiesSet方法中调用了initHandlerMethods。
protected void initHandlerMethods() {
for (String beanName : getCandidateBeanNames()) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
processCandidateBean(beanName);
}
}
handlerMethodsInitialized(getHandlerMethods());
}
initHandlerMethods先筛选了beanName前缀为“scopedTarget”的bean,这个前缀一般是用来作为HandlerMethod的bean探测的筛选条件。接下来就对筛选下来的bean一个一个用processCandidateBean方法处理。先说后面的handlerMethodsInitialized方法,这个其实是一个模板方法,让子类有能力在所有HandlerMethod加载完后,可以对HandlerMethod做子类想要进行的操作。不过,子类并没有重载这个方法。
processCandidateBean中使用isHandler方法对获取的bean的类型进行判断,从而筛选出Handler。逻辑也比较简单,就是通过是否注解有@Controller或者是@RequestMapping来判断。
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
因为@GetMapping和@PostMapping注解本质上还是通过@RequestMapping来实现的,所以这两个注解也在筛选的范围内。
接着需要从被筛选的bean中提取出被注解的Method,并注册到各个map中,detectHandlerMethods做的就是这个工作。processCandidateBean方法在筛选出Handler之后,就调用了detectHandlerMethods方法。
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
//提取和构建
if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
});
if (logger.isTraceEnabled()) {
logger.trace(formatMappings(userType, methods));
}
//注册
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
根据需求,detectHandlerMethods方法可以分为两步,第一步提取Method并构建匹配条件,第二部将Method注册到三大map中。提取Method用的是spring的类方法遍历MethodIntrospector,获取HandlerMethod的匹配条件用的是getMappingForMethod()方法,这个方法是由子类RequestMappingHandlerMapping实现的。主要思路是获取@RequestMapping注解中的各个参数,构造为综合的匹配条件类RequestMappingInfo。RequestMappingInfo是RequestCondition的接口实现,内部包含了@RequestMapping的6种匹配条件。
public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
······
private final PatternsRequestCondition patternsCondition;
private final RequestMethodsRequestCondition methodsCondition;
private final ParamsRequestCondition paramsCondition;
private final HeadersRequestCondition headersCondition;
private final ConsumesRequestCondition consumesCondition;
private final ProducesRequestCondition producesCondition;
······
}
detectHandlerMethod方法的第二步为注册。对单个HandlerMethod注册采用的是registerHandlerMethod方法:
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
this.mappingRegistry.register(mapping, handler, method);
}
public void register(T mapping, Object handler, Method method) {
this.readWriteLock.writeLock().lock();
try {
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
assertUniqueMethodMapping(handlerMethod, mapping);
this.mappingLookup.put(mapping, handlerMethod);
List<String> directUrls = getDirectUrls(mapping);
for (String url : directUrls) {
this.urlLookup.add(url, mapping);
}
String name = null;
if (getNamingStrategy() != null) {
name = getNamingStrategy().getName(handlerMethod, mapping);
addMappingName(name, handlerMethod);
}
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();
}
}
registerHandlerMethod方法中实际调用的是mappingRegistry(上文已经介绍过)内部对象的register方法。register先是根据Method构造了HandlerMethod,在判断mappingLookup不存在这种映射关系后,将这对映射添加到了mappingLookup中,然后根据匹配条件生成了url,将url和匹配条件的映射加入到了urlLookup中。后面还根据条件是否满足,填充了nameLookup和corsLookup。最后在registry的map中添加了完整的记录。这样,整个映射初始化构建就完成了。
AbstractHandlerMethodMapping匹配请求
其实,构建初始化的映射条件是比较重要的,匹配请求就比较简单了。匹配请求的入口是
mapping.getHandler(request);
在AbstractHandlerMapping的实现中引导子类使用模板方法getHandlerInternal,这在原理九中已经介绍过了。这里来看下
AbstractHandlerMethodMapping对getHandlerInternal方法的代码实现。
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
this.mappingRegistry.acquireReadLock();
try {
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
this.mappingRegistry.releaseReadLock();
}
}
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();
//先通过url获取所有所有匹配条件
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
//通过匹配条件获取所有的HandlerMethod放入matches
//(Match包含匹配条件和Handlermethod)
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
//如果没有任何匹配条件,将所有都放进去
// No choice but to go through all mappings...
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
if (!matches.isEmpty()) {
//通过Match来获取最佳的匹配条件
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
//最佳匹配的排序
matches.sort(comparator);
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
if (logger.isTraceEnabled()) {
logger.trace(matches.size() + " matching mappings: " + matches);
}
if (CorsUtils.isPreFlightRequest(request)) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}
//将最佳匹配条件和第二对比
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
//如果没有差别,则报错
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
String uri = request.getRequestURI();
throw new IllegalStateException(
"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
}
}
//设置最佳匹配的HandlerMethod
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
//在请求体中设置了一些其他相关属性
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
else {
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}
正如我在源码中注释所展示的那样,整个匹配的过程匹配条件RequestCondition发挥了很大作用,包括其中重载compareTo,equals等方法,帮助进行排序。关于Match排序部分相关的代码还有挺有意思的,有兴趣可以读下。
这样获取了HandlerMethod之后,可以和在父类获取的Interceptor一起,构成一个HandlerExecutionChain返回给DispatcherServlet,这样整个HandlerMapping组件就实现了自己的生命价值。
tomcat + spring mvc原理(一):tomcat原理综述和静态架构
tomcat + spring mvc原理(二):tomcat容器初始化加载和启动
tomcat + spring mvc原理(三):tomcat网络请求的监控与处理1
tomcat + spring mvc原理(四):tomcat网络请求的监控与处理2
tomcat + spring mvc原理(五):tomcat Filter组件实现原理
tomcat + spring mvc原理(六):tomcat WAR包的部署与加载
tomcat + spring mvc原理(七):spring mvc的Servlet和九大标准组件的静态结构与初始化
tomcat + spring mvc原理(八):spring mvc对请求的处理流程
tomcat + spring mvc原理(九):spring mvc如何将请求投送到Controller中的方法 1