DispatcherServlet 分发流程
0 太长不看版
HTTPServlet
的Service
方法将请求按类进行分解- 主要是根据HTTP方法的类型调用
doXXX
方法 - GET 和 HEAD 方法需要对 if-modified-since 进行特殊处理,其他是直接调用
- 主要是根据HTTP方法的类型调用
FrameworkServlet
重写doXXX
方法,统一调用doService
方法doXXX
方法统一调用processRequest
方法doOptions
和doTrace
有额外的处理- 其他是直接调用
processRequest
主要是初始化ThreadLocal
,调用doService
方法,并进行日志等处理ThreadLocal
是LocalContext
和Attributes
doService
方法执行核心逻辑,是抽象方法- 完成后会清空
ThreadLocal
,打印日志,产生事件。
DispatcherServlet
进行具体的实现- 重写
doService
方法- 添加
DispatcherServlet
特有的请求属性 - 对 HTML 的 include 请求进行处理
- 对重定向的请求进行处理
- 将请求转交给
doDispatch
方法进行实际的分发
- 添加
doDispatch
方法的逻辑为:- 查找是否有合适的 Handler,该过程在基于RESTful API 设计的 SpringMVC 中有性能问题
- 查找 Handler 是否有支持的 Adapter
- 执行拦截器
- 执行处理
- 解析结果并返回
- 重写
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);
}
doOptions
和 doTrace
方法进行了一些额外处理:
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
方法的逻辑为:
- 查找是否有合适的 Handler
- 查找 Handler 是否有支持的 Adapter
- 执行拦截器
- 执行处理
- 解析结果并返回
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
中的查找逻辑为:
- 查找
mappingRegistry
中是否缓存有该 URL 的结果缓存。如果有,那就将缓存直接加入matches
中 - 如果没有直接匹配,或者直接匹配的缓存结果为空,那么就将将所有注册的映射加入
matches
中,并遍历检查是否匹配 - 如果
matches
为空,直接返回handleNoMatch
- 否则,检查最佳匹配是否唯一,如果唯一,则返回最佳匹配的 Handler, 否则返回模糊匹配错误
由于在 RESTful API 中,会有大量相似的 URL,第二步中的遍历将不得不使用正则表达式匹配,而正则表达式匹配的时间复杂度是很高的。因此当 RESTful API 不断增长的时候,性能也会不断变差。
解决方案就是SpringMVC RESTful 性能优化中提到的继承 AbstractHandlerMethodMapping
类并重写其匹配逻辑,然后替换掉 SpringMVC 的默认组件。
DispatcherServlet 分发流程的更多相关文章
- spring自动扫描、DispatcherServlet初始化流程、spring控制器Controller 过程剖析
spring自动扫描1.自动扫描解析器ComponentScanBeanDefinitionParser,从doScan开始扫描解析指定包路径下的类注解信息并注册到工厂容器中. 2.进入后findCa ...
- interface21 - web - DispatcherServlet(DispatcherServlet初始化流程)
前言 最近打算花点时间好好看看spring的源码,然而现在Spring的源码经过迭代的版本太多了,比较庞大,看起来比较累,所以准备从最初的版本(interface21)开始入手,仅用于学习,理解其设计 ...
- Spring MVC源码(二) ----- DispatcherServlet 请求处理流程 面试必问
前端控制器 前端控制器,即所谓的Front Controller,体现的是设计模式中的前端控制器模式.前端控制器处理所有从用户过来的请求.所有用户的请求都要通过前端控制器.SpringMVC框架和其他 ...
- Android事件分发流程总结
Action_Down 当按下一个控件,调用流程是Activity.dispatchTouchEvent -> ViewGroup.dispatchTouchEvent , 1.ViewGrou ...
- SpringMVC源码解析-DispatcherServlet启动流程和初始化
在使用springmvc框架,会在web.xml文件配置一个DispatcherServlet,这正是web容器开始初始化,同时会在建立自己的上下文来持有SpringMVC的bean对象. 先从Dis ...
- android touch事件分发流程
韩梦飞沙 韩亚飞 313134555@qq.com yue31313 han_meng_fei_sha 三个方法:分发触摸事件dispatchTouchEvent.在触摸事件的时候onTouc ...
- Nginx 多进程连接请求/事件分发流程分析
Nginx使用多进程的方法进行任务处理,每个worker进程只有一个线程,单线程循环处理全部监听的事件.本文重点分析一下多进程间的负载均衡问题以及Nginx多进程事件处理流程,方便大家自己写程序的时候 ...
- Android Tv 中的按键事件 KeyEvent 分发处理流程
这次打算来梳理一下 Android Tv 中的按键点击事件 KeyEvent 的分发处理流程.一谈到点击事件机制,网上资料已经非常齐全了,像什么分发.拦截.处理三大流程啊:或者 dispatchTou ...
- Windows异常的分发处理流程
根据异常来源,一般分硬件异常和软件异常,它们处理的流程大致一样,下面简单讲一下. 如果是硬件异常,CPU会根据中断类型号转而执行对应的中断处理程序.CPU会在IDT中查找对应的函数来处理,各个异常处理 ...
随机推荐
- BUUCTF-另一个世界
另一个世界 010editor 打开最下方发现011开头字符串,应该是二进制 得到flag 看也有师傅写的是说八个一组转ascii码,现在也不是很理解啥意思.贴一下其他师傅的python脚本,算出的结 ...
- ms10_002 IE浏览器漏洞
一.环境说明 kali linux 靶机:xp 二.ms10_002漏洞利用 msf5 exploit(windows/smb/ms08_067_netapi) > search ms10_00 ...
- 【RPA之家BluePrism手把手教程】2.3 多重计算
2.3.1 添加除法运算计算框 2.3.2 设置除法运算计算属性 2.3.3 程序运行前初始值 2.3.4 程序运行后结果 使用多重计算框实现以上操作 2.3.5 添加多重选择框 2.3.6 设置多重 ...
- ssh空闲一段时间后自动断网
ssh空闲一段时间后自动断网 用客户端工具,例如securecrt连接linux服务器,有的会出现过一段时间没有任何操作,客户端与服务器就断开了连接. 造成这个的原因,主要是因为客户端与服务器之间存在 ...
- 十进制转换为K进制 Java 代码
最近在读<计算机科学导论--跨学科方法>(机械工业出版社),习题索引:1.3.21: 编写一个新程序Kary,输入两个命令行参数i和k,并将i转换为基数k的数值表示.假设i是java中的l ...
- 通过Go语言创建CA与签发证书
本篇文章中,将描述如何使用go创建CA,并使用CA签署证书.在使用openssl创建证书时,遵循的步骤是 创建秘钥 > 创建CA > 生成要颁发证书的秘钥 > 使用CA签发证书.这种 ...
- 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. ...
- P1494 小Z的袜子 莫队
题干 就是将$add$和$del$函数里的$ans$变化变成组合数嘛, 先预处理出$x$只相同袜子一共有$f[x] = 1+2+...+$$(x-1)$种组合, 要注意,由于$f[x]$是一直加到$x ...
- 如何用空气质量查询API接口进行快速开发
空气质量的好坏反映了空气污染程度,它是依据空气中污染物浓度的高低来判断的.空气污染是一个复杂的现象,在特定时间和地点空气污染物浓度受到许多因素影响.来自固定和流动污染物的人为污染物排放大小是影响空 ...
- treap(大根堆)模板
大根堆与小根堆性质相比简单很多,不用加特判 直接上代码: //treap(大根堆性质) #include<bits/stdc++.h> #define rint register int ...