本篇文章分析下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操作分析