本篇文章分析下Spring MVC如何映射一个URL地址到具体的 HandlerExecutionChain
,并转换request中的参数,最后执行所定位到的方法.
Spring MVC八大组件之一的 HandlerMapping
,其主要负责请求与对应处理器的映射.按照一般项目可以把请求地址分为以下三种,来分析Spring MVC到底是如何处理的.
在 DispatcherServlet
中根据请求得到对应的处理链(包括具体执行的方法与拦截器)调的是 getHandler
方法,该方法对 HandlerMappings
做了一个循环处理,直到找到第一个符合的 HandlerExecutionChain
为止.
这里个人觉得可以做个优化,让每一个 HandlerMapping
持有一个 handlerCount
字段,每次选中的 HandlerMapping
处理成功后该 handlerCount
自增,然后对 this.handlerMappings
根据 handlerCount
排序,这样随着服务的运行,会使得这个循环的尽可能的用最少的次数找到最合适的处理器.
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { // 对所有的Handler循环,直到找到能够处理的Handler for (HandlerMapping hm : this.handlerMappings) { if (logger.isTraceEnabled()) { logger.trace( "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'"); } HandlerExecutionChain handler = hm.getHandler(request); if (handler != null) { return handler; } } return null; }
根据上面的代码,寻找 HandlerExecutionChain
会委托给 HandlerMapping
的 getHandler
方法执行,那么接下来只需要分析 HandlerMapping
即可.对于 HandlerMapping
大概分为两种解析类型,如下图所示:
先看最顶层的抽象类 AbstractHandlerMapping
中定义的模板方法,其中 getHandlerInternal
是抽象方法委托给子类实现.
@Override public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { // getHandlerInternal委托给子类实现查找handler的方法 Object handler = getHandlerInternal(request); if (handler == null) { handler = getDefaultHandler(); } // 子类找不到则返回null,没有对应的处理器 if (handler == null) { return null; } // Bean name or resolved handler? if (handler instanceof String) { String handlerName = (String) handler; handler = getApplicationContext().getBean(handlerName); } // 构造handlerChain,主要是在hander中加入`HandlerInterceptor`拦截器. HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request); if (CorsUtils.isCorsRequest(request)) { CorsConfiguration globalConfig = this.corsConfigSource.getCorsConfiguration(request); CorsConfiguration handlerConfig = getCorsConfiguration(handler, request); CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig); executionChain = getCorsHandlerExecutionChain(request, executionChain, config); } return executionChain; }
如架构图所示 AbstractHandlerMapping
的子类有 AbstractHandlerMethodMapping
, AbstractUrlHandlerMapping
,很明显一个是映射对应的处理方法,一个是映射具体的URL.
该映射器主要是针对url-处理方法的映射关系,其内部持有 MappingRegistry
实例,该实例存放着所有的 @RequestMapping
所产生的映射关系,同时持有 ReentrantReadWriteLock
,也就是提供了并发访问的能力,其本身是读多写少的业务场景,因此读写锁是最合适的并发控制工具.
当拿到请求链接后,变会转到 org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod
方法中解析对应的处理方法 HandlerMethod
,如下注释:
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { List<Match> matches = new ArrayList<Match>(); // 存放找到的结果 // 因为URL是确定的,因此直接从映射关系中取,可以拿到一部分. List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath); if (directPathMatches != null) { 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()) { // 排序规则为org.springframework.web.servlet.mvc.method.RequestMappingInfo#compareTo方法,感兴趣可以研究下 Comparator<Match> comparator = new MatchComparator(getMappingComparator(request)); Collections.sort(matches, comparator); if (logger.isTraceEnabled()) { logger.trace("Found " + matches.size() + " matching mapping(s) for [" + lookupPath + "] : " + matches); } // 排序后第一个是最佳匹配 Match bestMatch = matches.get(0); if (matches.size() > 1) { 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(); throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" + request.getRequestURL() + "': {" + m1 + ", " + m2 + "}"); } } handleMatch(bestMatch.mapping, lookupPath, request); return bestMatch.handlerMethod; } else { return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request); } }
上述查找流程会有哪些问题?
当 directPathMatches
匹配不到时,会造成全量的遍历,笔者公司一个20w行代码的项目全量匹配是要循环300次,每一个URL方法都要试试匹配,然后再排序,再筛选,并且随着请求量的增加循环次数也在增加,系统负载能力是下降趋势的.那么哪些操作造成全量匹配?
分析 directPathMatches
的来源,其是根据URL查找出对应的处理链,然后再挨个判断,换句话说非具体的URL就找不到对应的处理器链,从而造成全量匹配,对于Spring MVC来说是 @PathVariable
或者是 login/**
通配符形式会导致全量匹配.因为这两种情况下链接本身不是固定的,因此无法精确匹配,只能全量搜索查找.
如果项目大量使用了类似的写法,解决办法就是定制解析流程,可以参考达达的定制过程 SpringMVC RESTful 性能优化
// k:url v:对应处理方法,可能多个 private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<String, T>(); public List<T> getMappingsByUrl(String urlPath) { return this.urlLookup.get(urlPath); }
与 AbstractHandlerMethodMapping
的处理方式差不多,主要集中在 org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#lookupHandler
方法,这里就不多做分析了,感兴趣的可以去阅读下.
对于Spring MVC来说,找到对应的方法后能拿到这个方法需要的参数名称以及参数类型,位置顺序,参数是在Url路径(@PathVariable),还是在请求参数(@RequestParam),还是在playload(@RequestBody)等相关信息,然后要解决的问题是如何找到,以及如何转换?
Spring MVC中提供了 HandlerMethodArgumentResolver
对参数进行解析,其主要提供如下两个方法.
public interface HandlerMethodArgumentResolver { // 是否可解析判断 boolean supportsParameter(MethodParameter parameter); // 具体解析方法 Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception; }
其下是 AbstractNamedValueMethodArgumentResolver
这个抽象类,该类定义了整个解析以及转换流程
@Override public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { .....省略一些代码 // 解析,委托给具体的实现类 Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest); .....省略一些代码 // 转换流程,委托给WebDataBinder if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name); try { arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter); } ... } handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest); return arg; }
以 RequestHeaderMethodArgumentResolver
为例,其主要对应 @RequestHeader
的解析操作,那么是否支持只需要判断参数是否用 @RequestHeader
修饰,取值则直接从Hedaer中获取.至于转换则是有其上的抽象模板父类完成.对于我们来说是一个示例,你可以仿照 RequestHeaderMethodArgumentResolver
的实现来自定义自己的取值规则.
@Override public boolean supportsParameter(MethodParameter parameter) { // 根据参数是否有该注解判断是否支持 return (parameter.hasParameterAnnotation(RequestHeader.class) && !Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())); } @Override protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { // 解析操作直接从header中取出对应的值 String[] headerValues = request.getHeaderValues(name); if (headerValues != null) { return (headerValues.length == 1 ? headerValues[0] : headerValues); } else { return null; } }
上述取值方法 resolveName
返回值为Object,调用 HadlerMethod
之前,需要转换为参数所需要的类型,在 AbstractNamedValueMethodArgumentResolver
可以看到如下的转换策略.
WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name); try { arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter); }
WebDataBinder最终会把转换委托给 org.springframework.core.convert.ConversionService
来实现,在 ConversionService
中包含有大量的 Converter
,也就是实际发生转换的地方.(转换这个委托太复杂了..这里直接略过跳到实际转换发生的地方)
以 IntegerToEnumConverterFactory
为例,其实现的是数字到枚举类的转换,使用的是枚举类的 ordinal
属性.该类也是一个很好的示例,告诉我们如果自定义转换规则则可以直接实现 ConverterFactory
接口,然后根据类型可以很轻松的实现自定义转换逻辑.
final class IntegerToEnumConverterFactory implements ConverterFactory<Integer, Enum> { @Override public <T extends Enum> Converter<Integer, T> getConverter(Class<T> targetType) { return new IntegerToEnum(ConversionUtils.getEnumType(targetType)); } private class IntegerToEnum<T extends Enum> implements Converter<Integer, T> { private final Class<T> enumType; public IntegerToEnum(Class<T> enumType) { this.enumType = enumType; } @Override public T convert(Integer source) { // 通过ordinal属性来定位到具体的枚举类. return this.enumType.getEnumConstants()[source]; } } }
整个流程看下来实际上是有点懵逼的,总体感觉下来Spring MVC并不是一款对于性能追求极致的框架,而是一款对扩展性追求极致的框架,其提供了太多的hack入口,让你可以定制自己的解析逻辑或者扩展现有的策略.
而整个设计流程给我最大的感触就是变与不变的分离,就像圆规画圆,第一步永远是固定圆心,然后另一支轴可以任意扩展,无论是 DispatcherServlet
还是各种 AbstractXXXXX
的设计都是如此,不变的定义在上层,变化的转换成另一个接口沉淀到其他层,尽量降低其他层的复杂度,从而在整个系统上提供了很高的扩展性,希望对你有启发.
最后如有错误还请指出,以免误人子弟.
Java学习记录--CAS操作分析