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中查找对应的函数来处理,各个异常处理 ...
随机推荐
- pytorch初学
(pytorch_gpu) D:\pytorch-text>pythonPython 3.7.9 (default, Aug 31 2020, 17:10:11) [MSC v.1916 64 ...
- vue2升级vue3:vue2 vue-i18n 升级到vue3搭配VueI18n v9
项目从vue2 升级vue3,VueI18n需要做适当的调整.主要是Vue I18n v8.x 到Vue I18n v9 or later 的变化,其中初始化: 具体可以参看:https://vue- ...
- SAP 查看在线用户
SM04 可查看服务器全部客户端(Client)的用户的在线状态,并可以结束指定用户的会话状态,也就是强制踢出用户.
- Java获取当天或者明天等零点时间(00:00:00)0时0分0秒的方法
SimpleDateFormat sdfYMD = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Calendar calendar = ...
- 最优化:凸集、凸函数、KKT条件极其解释
1.凸集(大概定义) 2.凸函数 3.KK条件
- 集合-List接口常用实现类的对比
1.collection接口:单列集合,用来存储一个一个的对象 2. list接口:存储有序的.可重复的数据. --->"动态数组",替换原有的数组 (1) Arraylis ...
- gpg加解密异常
在本地windows电脑和开发环境(linux) ,都不报错,但是在测试环境(linux) 上报错. 报错信息 org.bouncycastle.openpgp.PGPException: Excep ...
- osi七层与TCP\IP协议
层次划分的方法 1.网络的每层应当具有相对独立的功能(便于排错)这个功能用不了必然是你这层处理问题 2.梳理功能之间的关系,使上一个功能可以实现为另一个功能提供必要的服务,从而形成系统的层次结构.为提 ...
- 小红书携手HMS Core,畅玩高清视界,种草美好生活
在相同流量消耗的情况下,540p可秒变1080p?这不是魔法,通过视频超分辨率技术(简称视频超分),就能让视频变得更清晰. 7月20日,在小红书最新版本7.48的App中,用户就能体验到这项技术带来的 ...
- OpenCV视频防抖技术解析
视频防抖有很多种技术,各有优劣,主流的目前分为三种:EIS电子防抖EIS电子防抖是通过软件算法实现防抖的.其技术运作原理是通过加速度传感器和陀螺仪模块侦测手机抖动的幅度,从而来动态调节整ISO.快门以 ...