0 太长不看版

  • HTTPServletService 方法将请求按类进行分解

    • 主要是根据HTTP方法的类型调用 doXXX 方法
    • GET 和 HEAD 方法需要对 if-modified-since 进行特殊处理,其他是直接调用
  • FrameworkServlet 重写 doXXX 方法,统一调用 doService 方法
    • doXXX 方法统一调用 processRequest 方法

      • doOptionsdoTrace 有额外的处理
      • 其他是直接调用
    • processRequest 主要是初始化 ThreadLocal ,调用 doService 方法,并进行日志等处理
      • ThreadLocalLocalContextAttributes
      • doService 方法执行核心逻辑,是抽象方法
      • 完成后会清空 ThreadLocal,打印日志,产生事件。
  • DispatcherServlet 进行具体的实现
    • 重写 doService 方法

      1. 添加 DispatcherServlet 特有的请求属性
      2. 对 HTML 的 include 请求进行处理
      3. 对重定向的请求进行处理
      4. 将请求转交给 doDispatch 方法进行实际的分发
    • doDispatch 方法的逻辑为:
      1. 查找是否有合适的 Handler,该过程在基于RESTful API 设计的 SpringMVC 中有性能问题
      2. 查找 Handler 是否有支持的 Adapter
      3. 执行拦截器
      4. 执行处理
      5. 解析结果并返回

1,DispatcherServlet 的父类做了什么

DistpathcerServlet 的类图如下,可见其父类为 FrameworkServlet ,同时是一个 HttpServlet .

1.1 HttpServlet 的分发逻辑:

service 方法作为入口, 其逻辑如下:

protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException { String method = req.getMethod(); if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
} catch (IllegalArgumentException iae) {
// Invalid date header - proceed as if none was set
ifModifiedSince = -1;
}
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
} } else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp); } else if (method.equals(METHOD_POST)) {
doPost(req, resp); } else if (method.equals(METHOD_PUT)) {
doPut(req, resp); } else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp); } else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp); } else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp); } else {
//
// Note that this means NO servlet supports whatever
// method was requested, anywhere on this server.
// String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs); resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}

可见对于 POST PUT DELETE OPTIONS TRACE 方法都是直接调用对应的具体方法 doXXX, 而 GET 方法会增加 if-modified-since 检查,符合条件后才进行 doGet, 这是为了支持 HTTP 协议的 if-modified-since 请求头。而 HEAD 方法的额外处理也是检查是否需要设置 last-modified 属性。

HTTP 协议的 if-modified-since 请求是条件请求,要求返回的资源在指定日期后发生过修改。如果发生修改,则返回 200 OK 以及对应资源,否则返回 304 Not Modified.

1.2 FrameworkServlet 做了什么

1.2.1 重写 doXXX 方法

FrameworkServlet 没有修改 HttpServlet 的分发逻辑,而是将所有的 doXXX 方法调用了同一方法 processRequest

@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException { processRequest(request, response);
}

doOptionsdoTrace 方法进行了一些额外处理:

1.2.1.1 doOptions

@Override
protected void doOptions(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException { // 如果允许转发,并且是 CORS 请求,那么检查它是否是 pre-flight 预检请求
if (this.dispatchOptionsRequest || CorsUtils.isPreFlightRequest(request)) {
processRequest(request, response);
if (response.containsHeader("Allow")) {
// Proper OPTIONS response coming from a handler - we're done.
// 如果返回值是 "Allow",即方法允许,那就直接返回
return;
}
} // 否则使用 HttpServlet 默认的方法检查是否允许
// Use response wrapper in order to always add PATCH to the allowed methods
super.doOptions(request, new HttpServletResponseWrapper(response) {
@Override
public void setHeader(String name, String value) {
// 如果结果是 "Allow", 那么以指定的格式返回
if ("Allow".equals(name)) {
value = (StringUtils.hasLength(value) ? value + ", " : "") + HttpMethod.PATCH.name();
}
super.setHeader(name, value);
}
});
}

doOptions 方法例外在 OPTIONS 方法由多个来源,而 Tomcat 只处理来自 CORS 的预检命令。对于不接受 CORS 的 Servlet 或其他来源的 OPTIONS 请求,就调用默认的方法实现。

1.2.1.2 doTrace

@Override
protected void doTrace(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException { if (this.dispatchTraceRequest) {
processRequest(request, response);
if ("message/http".equals(response.getContentType())) {
// Proper TRACE response coming from a handler - we're done.
return;
}
}
super.doTrace(request, response);
}

doTrace 方法就简单很多,它只需判断是否已经处理了命令,如果没有,则调用默认的方法。

为什么不直接用 processRequest 重写 service 方法?

除了满足里氏替换原则以外,根据 1.1 的分析,我们也可以看到,service 方法中处理了缓存机制,即 last-modified 属性和 if-modified-since 属性的相关处理,以及 OPTIONS 和 TRACE 方法所需的一些额外处理

1.2.2 processRequest

protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException { // 分配变量
long startTime = System.currentTimeMillis();
Throwable failureCause = null; LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request); RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes); WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor()); // 初始化上下文,实际上是把后两个参数用 ThreadLocal 缓存起来
initContextHolders(request, localeContext, requestAttributes); // 执行真正的业务逻辑 doService
try {
doService(request, response);
}
catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
} finally {
// 由于 ThreadLocal 是线程专用的,在线程池场景下需要清空,否则会影响下一次使用。
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
// 输出日志
logResult(request, response, failureCause, asyncManager);
// 输出事件
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}

processRequest 主要是进行封装和异常处理,并调用 doService 方法进行核心实现。而 doService 是一个抽象方法, DispatcherServlet 就实现了这一方法。

2 DispatcherServlet 的分发流程

2.1 doService 方法

doService 方法的文档注释为:暴露 DispatcherServlet 特有的请求属性,并将请求转交给 doDispatch 进行实际的分发

@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
logRequest(request); // Keep a snapshot of the request attributes in case of an include,
// to be able to restore the original attributes after the include.
// 如果是 include 请求,保存请求的一个快照,以在 include 中保存原始属性
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap<>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
} // Make framework objects available to handlers and view objects.
// 将框架对象暴露给 handler(controller)和 VO
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource()); // 通过 FlashMap 恢复重定向请求的属性
if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
} // 获取之前的 Path,并将当前 Path 添加到属性中
RequestPath previousRequestPath = null;
if (this.parseRequestPath) {
previousRequestPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);
ServletRequestPathUtils.parseAndCache(request);
} try {
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Restore the original attribute snapshot, in case of an include.
// 根据原始快照恢复属性
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
if (this.parseRequestPath) {
// 恢复 RequestPath
/* 该方法的逻辑为:如果 previousRequestPath 不为空,则将 request 的 PATH_ATTRIBUTE
属性设为 previousRequestPath, 否则删除 PATH_ATTRIBUTE 属性
*/
ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request);
}
}
}

该方法的功能是:

  • 添加 DispatcherServlet 特有的请求属性
  • 对 HTML 的 include 请求进行处理
  • 对重定向的请求进行处理
  • 将请求转交给 doDispatch 方法进行实际的分发

HTML 的 include 会引入另一个页面。这里采用了快照技术,将原始属性存放到快照中,并在处理完成后恢复原属性,以避免 include 后对原页面产生影响。

FlashMap 将一个 Request 的属性存储,并存放在 Session 中,这样如果发生了重定向,新产生的 Request 就可以从 flashMapManager 中获取前一个请求的属性(INPUT_FLASH_MAP_ATTRIBUTE)

2.2 doDispatch 方法

@SuppressWarnings("deprecation")
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 初始化变量
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try {
ModelAndView mv = null;
Exception dispatchException = null; try {
// 通过 multipartResolver 判断是否是文件的多部分请求
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request); // Determine handler for the current request.
// 获取 handler
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
} // Determine handler adapter for the current request.
// 获取 HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // Process last-modified header, if supported by the handler.
// 进行 last-modified 处理
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
} // 检查在 HandlerExecutionChain 中被注册的拦截器,调用 preHandle 方法,返回值为是否放行
// 内部使用了责任链模式
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
} // Actually invoke the handler.
// 最终执行 handler,并获取 ModelAndView
mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) {
return;
} // 如果返回的是 Model(即没有 View ),那么使用默认的 ViewName
applyDefaultViewName(processedRequest, mv);
// 执行拦截器的 PostHandle 方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
// 从 Spring 4.3 起,handler 发生的 Error 也会被处理
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 处理分发的结果,即解析后的 View 对象
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
// 出现异常,则需要继续执行完成拦截器的 afterCompletion 方法
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
// 如果是并发执行,则异步结束拦截器
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
// 如果是同步执行,则所有拦截器已经结束,这里把 MultiPart 请求过程中占用的文件全部释放
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}

doDispatch 方法的逻辑为:

  1. 查找是否有合适的 Handler
  2. 查找 Handler 是否有支持的 Adapter
  3. 执行拦截器
  4. 执行处理
  5. 解析结果并返回

MultipartResolver 解析请求是否是以允许的方法请求多部分文件,用于文件上传

3 相关问题

3.1 RESTful API 的性能问题

问题来源参见:SpringMVC RESTful 性能优化

doDispatch 方法查找是否有合适的 Handler 的调用链为:

DispatcherServlet::doDispatch -> DispatcherServlet::getHandler -> AbstractHandleMapping::getHandle -> AbstractHandlerMethodMapping::getHandlerInternal -> AbstractHandlerMethodMapping::lookupHandlerMethod

源码为:

@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();
// 查询是否有直接匹配
List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
// 如果有直接匹配,那么就将结果缓存
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
// 如果没有直接匹配,或者直接匹配的缓存结果为空,那么就将所有的记录加入
if (matches.isEmpty()) {
addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
}
if (!matches.isEmpty()) {
// 如果此时有了匹配,那就查找
Match bestMatch = matches.get(0);
// 如果匹配的 match 不止 1 个,那么就检查模糊性
if (matches.size() > 1) {
// 如果有超过一个匹配,则排序
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
bestMatch = matches.get(0);
// 日志 TRACE 级
if (logger.isTraceEnabled()) {
logger.trace(matches.size() + " matching mappings: " + matches);
}
// 检查模糊性
if (CorsUtils.isPreFlightRequest(request)) {
// 如果是 CORS 的预检命令,检查是否有 match 含有 CORS 配置
for (Match match : matches) {
if (match.hasCorsConfig()) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}
}
}
else {
// 如果不是 CORS 预检命令,检查次佳匹配是否和最佳匹配同等最佳
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.getHandlerMethod().getMethod();
Method m2 = secondBestMatch.getHandlerMethod().getMethod();
String uri = request.getRequestURI();
throw new IllegalStateException(
"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
}
}
}
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.getHandlerMethod());
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.getHandlerMethod();
}
else {
// 如果此时 matches 依然为空,那就直接返回
return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
}
}

AbstractHandlerMethodMapping::lookupHandlerMethod 中的查找逻辑为:

  1. 查找 mappingRegistry 中是否缓存有该 URL 的结果缓存。如果有,那就将缓存直接加入 matches
  2. 如果没有直接匹配,或者直接匹配的缓存结果为空,那么就将将所有注册的映射加入 matches 中,并遍历检查是否匹配
  3. 如果 matches 为空,直接返回 handleNoMatch
  4. 否则,检查最佳匹配是否唯一,如果唯一,则返回最佳匹配的 Handler, 否则返回模糊匹配错误

由于在 RESTful API 中,会有大量相似的 URL,第二步中的遍历将不得不使用正则表达式匹配,而正则表达式匹配的时间复杂度是很高的。因此当 RESTful API 不断增长的时候,性能也会不断变差。

解决方案就是SpringMVC RESTful 性能优化中提到的继承 AbstractHandlerMethodMapping 类并重写其匹配逻辑,然后替换掉 SpringMVC 的默认组件。

DispatcherServlet 分发流程的更多相关文章

  1. spring自动扫描、DispatcherServlet初始化流程、spring控制器Controller 过程剖析

    spring自动扫描1.自动扫描解析器ComponentScanBeanDefinitionParser,从doScan开始扫描解析指定包路径下的类注解信息并注册到工厂容器中. 2.进入后findCa ...

  2. interface21 - web - DispatcherServlet(DispatcherServlet初始化流程)

    前言 最近打算花点时间好好看看spring的源码,然而现在Spring的源码经过迭代的版本太多了,比较庞大,看起来比较累,所以准备从最初的版本(interface21)开始入手,仅用于学习,理解其设计 ...

  3. Spring MVC源码(二) ----- DispatcherServlet 请求处理流程 面试必问

    前端控制器 前端控制器,即所谓的Front Controller,体现的是设计模式中的前端控制器模式.前端控制器处理所有从用户过来的请求.所有用户的请求都要通过前端控制器.SpringMVC框架和其他 ...

  4. Android事件分发流程总结

    Action_Down 当按下一个控件,调用流程是Activity.dispatchTouchEvent -> ViewGroup.dispatchTouchEvent , 1.ViewGrou ...

  5. SpringMVC源码解析-DispatcherServlet启动流程和初始化

    在使用springmvc框架,会在web.xml文件配置一个DispatcherServlet,这正是web容器开始初始化,同时会在建立自己的上下文来持有SpringMVC的bean对象. 先从Dis ...

  6. android touch事件分发流程

    韩梦飞沙  韩亚飞  313134555@qq.com  yue31313  han_meng_fei_sha 三个方法:分发触摸事件dispatchTouchEvent.在触摸事件的时候onTouc ...

  7. Nginx 多进程连接请求/事件分发流程分析

    Nginx使用多进程的方法进行任务处理,每个worker进程只有一个线程,单线程循环处理全部监听的事件.本文重点分析一下多进程间的负载均衡问题以及Nginx多进程事件处理流程,方便大家自己写程序的时候 ...

  8. Android Tv 中的按键事件 KeyEvent 分发处理流程

    这次打算来梳理一下 Android Tv 中的按键点击事件 KeyEvent 的分发处理流程.一谈到点击事件机制,网上资料已经非常齐全了,像什么分发.拦截.处理三大流程啊:或者 dispatchTou ...

  9. Windows异常的分发处理流程

    根据异常来源,一般分硬件异常和软件异常,它们处理的流程大致一样,下面简单讲一下. 如果是硬件异常,CPU会根据中断类型号转而执行对应的中断处理程序.CPU会在IDT中查找对应的函数来处理,各个异常处理 ...

随机推荐

  1. BUUCTF-另一个世界

    另一个世界 010editor 打开最下方发现011开头字符串,应该是二进制 得到flag 看也有师傅写的是说八个一组转ascii码,现在也不是很理解啥意思.贴一下其他师傅的python脚本,算出的结 ...

  2. ms10_002 IE浏览器漏洞

    一.环境说明 kali linux 靶机:xp 二.ms10_002漏洞利用 msf5 exploit(windows/smb/ms08_067_netapi) > search ms10_00 ...

  3. 【RPA之家BluePrism手把手教程】2.3 多重计算

    2.3.1 添加除法运算计算框 2.3.2 设置除法运算计算属性 2.3.3 程序运行前初始值 2.3.4 程序运行后结果 使用多重计算框实现以上操作 2.3.5 添加多重选择框 2.3.6 设置多重 ...

  4. ssh空闲一段时间后自动断网

    ssh空闲一段时间后自动断网 用客户端工具,例如securecrt连接linux服务器,有的会出现过一段时间没有任何操作,客户端与服务器就断开了连接. 造成这个的原因,主要是因为客户端与服务器之间存在 ...

  5. 十进制转换为K进制 Java 代码

    最近在读<计算机科学导论--跨学科方法>(机械工业出版社),习题索引:1.3.21: 编写一个新程序Kary,输入两个命令行参数i和k,并将i转换为基数k的数值表示.假设i是java中的l ...

  6. 通过Go语言创建CA与签发证书

    本篇文章中,将描述如何使用go创建CA,并使用CA签署证书.在使用openssl创建证书时,遵循的步骤是 创建秘钥 > 创建CA > 生成要颁发证书的秘钥 > 使用CA签发证书.这种 ...

  7. Host–Parasite(主从关系): Graph LSTM-in-LSTM for Group Activity Recognition

    This article aims to tackle the problem of group activity recognition in the multiple-person scene. ...

  8. P1494 小Z的袜子 莫队

    题干 就是将$add$和$del$函数里的$ans$变化变成组合数嘛, 先预处理出$x$只相同袜子一共有$f[x] = 1+2+...+$$(x-1)$种组合, 要注意,由于$f[x]$是一直加到$x ...

  9. 如何用空气质量查询API接口进行快速开发

      空气质量的好坏反映了空气污染程度,它是依据空气中污染物浓度的高低来判断的.空气污染是一个复杂的现象,在特定时间和地点空气污染物浓度受到许多因素影响.来自固定和流动污染物的人为污染物排放大小是影响空 ...

  10. treap(大根堆)模板

    大根堆与小根堆性质相比简单很多,不用加特判 直接上代码: //treap(大根堆性质) #include<bits/stdc++.h> #define rint register int ...