承接上文,依照上文所说 DispatcherServlet
拿到请求后所作的第一件事情是定位到具体的 HandlerExecutionChain
,也就是该请求所需要执行的方法,包括拦截器方法与用户的业务方法,那么本篇来详细描述这个过程.
在分析之前先理解Spring MVC中的URL,对于Spring MVC来说URL分为两类,一种是静态的,一种是动态的.
/api/v1/login/
/api/v1/**
, /api/{version}/login/
这两种都属于动态的,是没法直接根绝URL定位到需要执行的方法. HandlerMapping
是Spring MVC中定位到具体执行链所使用的类,其所提供的是对外的功能,根据请求拿到具体的执行链.
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
在 DispatcherServlet
中根据请求得到对应的处理链(包括具体执行的方法与拦截器)调的是 getHandler
方法,该方法对 HandlerMappings
做了一个循环处理,直到找到第一个符合的 HandlerExecutionChain
为止, HandlerExecutionChain
是方法的执行链,其中包含着Spring MVC的 拦截器 以及用户定于的处理方法 HandlerMethod
.
这里个人觉得可以做个优化,让每一个 HandlerMapping
持有一个 handlerCount
字段,每次选中的 HandlerMapping
处理成功后该 handlerCount
自增,然后对 this.handlerMappings
根据 handlerCount
排序,这样随着服务的运行,会使得这个循环的尽可能的用最少的次数找到最合适的处理器.
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { // 对所有的Handler循环,直到找到能够处理的Handler for (HandlerMapping hm : this.handlerMappings) { HandlerExecutionChain handler = hm.getHandler(request); if (handler != null) { return handler; } } return null; }
HandlerMappings
继承结构如下:
从关系图可以得到哪些信息?
HandlerMapping
分为两种类型,一种是url到方法 AbstractHandlerMethodMapping
,这种形式比较常用,也是业务开发中主要使用到的形式,一种是url到其他处理器比如Controller,Resource的 AbstractUrlHandlerMapping
,该类属于Spring3之前常用的类. 下面按照模板方法设计模式的思路来分析
身为模板类的 AbstractHandlerMapping
,主要功能是实现 HandlerMapping
的方法,然后 拆解这个方法到更加细小的任务,传递到子类中 ,这个也是模板方法设计模式的本质.他的大概流程如下(省略了部分代码):
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { // getHandlerInternal是有模板类延迟到子类的一个方法 Object handler = getHandlerInternal(request); ...... // handler是Spring的话则去获取到具体的Bean if (handler instanceof String) { String handlerName = (String) handler; handler = getApplicationContext().getBean(handlerName); } // 构造执行链 HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request); .... // 返回 return executionChain; } // 延迟到子类的方法 protected abstract Object getHandlerInternal(HttpServletRequest request) throws Exception;
其中 HandlerExecutionChain
是一个包含了拦截器与对应的业务方法的包装类,比较简单,就不做分析了.可以看到 AbstractHandlerMapping
细分了 getHandlerInternal()
方法交由子类实现,自己则负责整个流程的构建.
方法映射处理AbstractHandlerMethodMapping
AbstractHandlerMethodMapping
自然也是模板类,其承担的责任是根据URL找到具体的方法.
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { // 取出具体的url信息 String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); // 获取读锁 this.mappingRegistry.acquireReadLock(); // 找到对应的方法 HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); // 返回对应的处理方法 return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); // 释放读锁 this.mappingRegistry.releaseReadLock(); }
从上述流程可以看出,主要的寻找逻辑在 lookupHandlerMethod()
方法中,等下在分析该方法.在这个查找中有 MappingRegistry
,它是什么?为什么有需要读锁?
按照该类的注释所说,该类是一个路由表,其包含着Spring MVC所管理的所有映射关系,并且运用读写锁提供并发访问能力,之所以需要并发因为 MappingRegistry
并不是一个线程安全的类,其提供了写入与获取方法,并且共享了一些线程不安全的类,比如 HashMap
,并且其属于读写比非常大的场景,因此使用读写锁实现高性能访问与并发安全在合适不过了.
有了所有的映射关系接下来是匹配流程,也就是 lookupHandlerMethod
的方法执行逻辑(代码比较长,参考注释观看):
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { List<Match> matches = new ArrayList<Match>(); // 从urlLookup中取出匹配结果,这里是直接匹配 List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath); if (directPathMatches != null) { addMatchingMappings(directPathMatches, matches, request); } // 上述直接匹配不到则全部匹配(这里是坑) if (matches.isEmpty()) { addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request); } // 匹配后进行筛选 if (!matches.isEmpty()) { Comparator<Match> comparator = new MatchComparator(getMappingComparator(request)); Collections.sort(matches, comparator); ... // 默认排序后第一个为最佳匹配 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 性能优化
该类属于Spring3之前所提供的类,由于笔者对Spring3之前的开发方式不是很清楚,因此不会过多的讨论该类,只从现在角度来分析该类的用处.该类的实现比较简单,其内部拥有一个Map保存了所有的映射关系,可以映射到对应的Controller,也可以映射到其他的Handler.
private final Map<String, Object> handlerMap = new LinkedHashMap<String, Object>();
LinkedHashMap
是一个线程不安全的类,因此对于这种类要求必须再启动时把所有的信息都注入,从而保证再运行时没有put写入的请求,以及扩容的需求,从而达到线程安全. spring.mvc.static-path-pattern=/static/**
由于使用不多,这里就不多研究了.
Spring MVC请求定位的实现原理就是先获取所有的映射关系,然后拿到请求后的url进行匹配,在匹配结果中筛选最合适的一个进行处理的过程.
Spring MVC--所存在的疑惑