ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

DispatcherServlet 分发流程

2022-09-02 23:01:20  阅读:206  来源: 互联网

标签:分发 请求 matches 流程 request method DispatcherServlet 方法 response


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 .
UML类图

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 的默认组件。

标签:分发,请求,matches,流程,request,method,DispatcherServlet,方法,response
来源: https://www.cnblogs.com/CounterX/p/16648094.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有