SpringMVC系列-2 HTTP请求调用链

news/2024/12/26 6:59:05 标签: http, servlet, spring
http://www.w3.org/2000/svg" style="display: none;">

背景

本文作为 SpringMVC系列 第二篇,介绍HTTP请求的调用链:从请求进入Tomcat到数据流返回客户端的完整过程。为了尽可能把流程表达清楚,进行了很多减支处理,只关注主线逻辑。

本文也作为SpringMVC系列后续文章的基础,在调用链梳理清楚的基础上,后文对重要逻辑分别进行展开介绍,如拦截器、异常处理器、转换器、消息转换器、异步请求、文件上传等。在这些文章完成后,会出一个Spring框架应用专题,包括:结果集框架、错误码框架、鉴权逻辑、分页查询、事件框架等,基于此会对Spring系列和SpringMVC系列文章有更深层次的理解。

1.调用链

Tomcat从逻辑上可以分为连接器(coyote)和Servlet容器(catalina)两个部分:coyote负责接收客户端的请求,并按照协议对请求进行解析,封装成Java对象后发送给catalina以及将catalina返回的消息推送给客户端;catalina提供了Servlet容器实现,负责处理具体的请求并进行响应。
其中,coyote封装了底层的网络通讯(Socket),为catalina提供了统一的接口(Request/Response对象)而与Servlet容器解耦;catalina内部通过适配器将(Request/Response对象)转换为(HttpRequest/HttpResponse对象),然后将消息发送给Servlet对象,流程图如下所示:
https://img-blog.csdnimg.cn/e747503a399846acbb1db5d4ea5a3fcd.png" alt="在这里插入图片描述" />
总之,当Http请求到达Tomcat连接池后,会将请求消息封装成(HttpRequest/HttpResponse对象), 通过调用Servlet标准接口实现消息的传递。
SpringMVC框架对应的Servlet对象为DispatcherServlet,即调用栈会进入DispatcherServlet的void service(ServletRequest req, ServletResponse res)方法。
因此,有必要了解一下DispatcherServlet类的继承关系以及对Servlet方法实现情况,如下图所示:
https://img-blog.csdnimg.cn/9df482b0bda64ad8b8bee4eac255f589.png" alt="![在这里" />

2.HttpServlet

https://img-blog.csdnimg.cn/20f879b2100f457eacbb553dcb56f8cf.png" alt="在这里插入图片描述" />

DispatcherServlet关于Servlet:void service(ServletRequest req, ServletResponse res)接口的实现逻辑在HttpServlet类中,代码如下:

public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
      HttpServletRequest  request;
      HttpServletResponse response;
      try {
          request = (HttpServletRequest) req;
          response = (HttpServletResponse) res;
      } catch (ClassCastException e) {
          throw new ServletException(lStrings.getString("http.non_http"));
      }
      service(request, response);
  }

逻辑较为简单,直接将ServletRequest/ServletResponse对象转为HttpServletRequest/HttpServletResponse,并调用service(HttpServletRequest req, HttpServletResponse resp)接口。后者中根据HTTP方法类型派发给了doGet/doPost/doPut等接口;而doGet/doPost/doPut等接口的实现逻辑在FrameworkServlet中归一到void processRequest(HttpServletRequest request, HttpServletResponse response)接口中:

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

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

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

总之,所有来自Servlet的请求都会进入processRequest方法中进行处理。

3.FrameworkServlet

https://img-blog.csdnimg.cn/522bb2943570475f8bde69dc1e04a32b.png" alt="在这里插入图片描述" />

processRequest方法的主线逻辑如下:

protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
	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());

	initContextHolders(request, localeContext, requestAttributes);

	try {
		doService(request, response);
	} finally {
		resetContextHolders(request, previousLocaleContext, previousAttributes);
	}
}

上述代码在逻辑上可以分为三块:调用doService(request, response)接口处理并响应、调用前的准备工作、调用后的清理工作。
doService接口作为主体逻辑在下一节中进行介绍,本节对准备工作和清理工作进行介绍。

准备工作

LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);

RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

// WebAsyncManager提供了HTTP请求异步处理能力,不是本文的重点
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

根据request对象获取Locale对象并将其通过ThreadLocal对象中,将request和response对象封装在RequestAttributes对象中,保存在ThreadLocal对象中;借助ThreadLocal的能力,对外提供了静态方法,使得程序在任意时刻都取得Locale对象、request和response对象。

清理工作

清理工作对应从内存中将该线程相关的Locale对象和request和response对象从内存中清除,以防止内存泄露。

resetContextHolders(request, previousLocaleContext, previousAttributes);

private void resetContextHolders(HttpServletRequest request, LocaleContext prevLocaleContext, RequestAttributes previousAttributes) {
	LocaleContextHolder.setLocaleContext(prevLocaleContext, this.threadContextInheritable);
	RequestContextHolder.setRequestAttributes(previousAttributes, this.threadContextInheritable);
}

这是一个使用ThreadLocal基本的套路,后面介绍Spring应用专题时会涉及。

4.DispatchServlet

https://img-blog.csdnimg.cn/d076738dc1fa4bd386fdfd8f9ac123a2.png" alt="在这里插入图片描述" />

protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
	// springMVC容器
	request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
	
	// locale解析器
	request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
	
	// 主体相关
	request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
	request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

	// flashMapManager在后续介绍转发和重定向内容时进行介绍,这里暂时忽略
	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);
	}

	try {
		doDispatch(request, response);
	} finally {
		// 清理工作 
}

doService逻辑比较简单,包括:调用doDispatch方法前对request的属性设置,以及在调用之后清理工作。doDispatch方法是核心,其他逻辑是在为调用该方法做的准备或收尾工作。
本文希望将HTTP调用链的主线逻辑表达清楚,因此会省去异步请求(WebAsyncManager相关)、文件上传(MultipartResolver相关)、异常捕获等逻辑,如下所示:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
	HandlerExecutionChain mappedHandler = null;
	boolean multipartRequestParsed = false;

	try {
		ModelAndView mv = null;
		Exception dispatchException = null;

		try {
			mappedHandler = getHandler(request);
			if (mappedHandler == null) {
				noHandlerFound(request, response);
				return;
			}
			HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
			if (!mappedHandler.applyPreHandle(request, response)) {
				return;
			}
			mv = ha.handle(request, response, mappedHandler.getHandler());
			mappedHandler.applyPostHandle(request, response, mv);
		} catch (Exception ex) {
			dispatchException = ex;
		}
		processDispatchResult(request, response, mappedHandler, mv, dispatchException);
	} catch (Exception ex) {
		triggerAfterCompletion(request, response, mappedHandler, ex);
	} 
}

上述逻辑可以表示为:
https://img-blog.csdnimg.cn/d5a366ed42c9442b8bfee93b703606de.png" alt="【插图】" />

拦截器的preHandle方法按照拦截器在列表中的顺序正向执行,postHandle和afterCompletion反向执行。

上图可以分为以下几个步骤:
(1)根据request对象获取执行链HandlerExecutionChain;执行链由Interceptor(拦截器)和Handler(对controller的包装)组成;
(2)根据Handler获取HandlerAdapter;
(3)依次调用拦截器的preHandle方法;
(4)借助HandlerAdapter 通过反射调用目标方法(controller对应的方法);
(5)依次调用拦截器的postHandle方法;
(6)对结果或者异常进行后置处理;
(7)依次调用拦截器的afterCompletion方法;

上述步骤为正常执行流程,异常场景包括:

异常场景1:
当根据equest对象获取执行链HandlerExecutionChain失败时(没有匹配项),则直接想客户端返回404。

异常场景2:
调用某个拦截器的preHandle方法返回false时,则反向执行已执行过拦截器的afterCompletion方法,并跳出doDispatch方法。
https://img-blog.csdnimg.cn/1a9c3e03902840e6b85724741629311e.png" alt="在这里插入图片描述" />
涉及的代码逻辑如下:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
	//...
	if (!mappedHandler.applyPreHandle(processedRequest, response)) {
		return;
	}
	//...
}

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
	for (int i = 0; i < this.interceptorList.size(); i++) {
		HandlerInterceptor interceptor = this.interceptorList.get(i);
		if (!interceptor.preHandle(request, response, this.handler)) {
			triggerAfterCompletion(request, response, null);
			return false;
		}
		this.interceptorIndex = i;
	}
	return true;
}

当有拦截器的preHandle方法返回false时,调用triggerAfterCompletion(request, response, null)方法:

void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
	for (int i = this.interceptorIndex; i >= 0; i--) {
		HandlerInterceptor interceptor = this.interceptorList.get(i);
		try {
			interceptor.afterCompletion(request, response, this.handler, ex);
		} catch (Throwable ex2) {
			logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
		}
	}
}

倒序执行已执行过preHandle的拦截器。
异常场景3:
preHandle或者反射调用目标方法(Controller接口)过程中有异常抛出时,经过异常捕获将异常包装成Exception对象,并通过processDispatchResult方法处理结果:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
	try {
		//。。。
	} catch (Exception ex) {
		dispatchException = ex;
	} catch (Throwable err) {
		dispatchException = new NestedServletException("Handler dispatch failed", err);
	}
	processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}

processDispatchResult的主线逻辑如下:

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
	if (exception != null) {
		mv = processHandlerException(request, response, mappedHandler.getHandler(), exception);
	}
	mappedHandler.triggerAfterCompletion(request, response, null);
}

如果有异常抛出,则exception不为空,通过processHandlerException方法处理异常场景。
如论是否有异常抛出,最后都通过mappedHandler.triggerAfterCompletion会触发拦截的afterCompletion方法。
同样需要注意:拦截器在执行preHandle过程中抛出的异常,则会反向执行已执行过preHandle方法拦截器的afterCompletion方法。
如果在拦截器的postHandle或者反射调用目标方法过程中抛出的异常,则会执行所有拦截器的afterCompletion方法,流程如下所示:
https://img-blog.csdnimg.cn/97ff068dc9e64785bed95786022ac92a.png" alt="在这里插入图片描述" />


http://www.niftyadmin.cn/n/458992.html

相关文章

Go 语言数组

1、Go 语言数组 Go 语言提供了数组类型的数据结构。 数组是具有相同唯一类型的一组已编号且长度固定的数据项序列&#xff0c;这种类型可以是任意的原始类型例如整型、字符 串或者自定义类型。 相对于去声明 number0, number1, ..., number99 的变量&#xff0c;使用数组形…

【已解决】Vue3使用Element-plus按需加载时消息弹框ElMessage没有样式

Vue3使用Element-plus时消息弹框ElMessage没有样式 问题描述 Element-plus在使用ElMessage消息弹框的时候没有样式&#xff0c;按照官方的按需加载的方式引入的 1、Element-plus使用了自动按需导入&#xff0c;vite.config.js配置如下&#xff1a; plugins: [vue(),AutoImpo…

JavaScript 手写代码 第一期

文章目录 1.为什么要手写代码&#xff1f;2.手写代码2.1 手写Object.create()方法2.1.1 基本使用2.1.2 使用实例2.1.3 手写实现 2.2 手写实现instanceof方法2.2.1 基本使用2.2.2 使用实例2.2.3 手写实现 2.3 手写实现new操作符2.3.1 基本使用2.3.2 使用实例2.3.3 手写实现 1.为…

第五章 Linux的文件权限与目录配置

Linux最好的地方之一就在于他的多用户多任务环境。为了让各个使用者具有较保密的文件数据&#xff0c;因此文件的权限管理就变得很重要了。Linux一般将文件可存取的身份分为三个类别&#xff0c;分别是owner/group/others,且三种身份各有read/write/execute等权限。 5.1 使用者…

Object类的常用方法

Object类里有哪些常用的方法&#xff1f; 1.getClass():获取类的class对象。 2.hashCode&#xff1a;获取对象的hashCode值。 3.equals():比较对象是否相等&#xff0c;比较的是值和地址&#xff0c;子类可以重写。 4.clone():克隆方法。 5.toString():如果没有重写&#…

社区活动 | OpenVINO™ DevCon 中国系列工作坊第二期 | 使用 OpenVINO™ 加速生成式 AI...

生成式 AI 领域一直在快速发展&#xff0c;许多潜在应用随之而来&#xff0c;这些应用可以从根本上改变人机交互与协作的未来。这一最新进展的一个例子是 GPT 模型的发布&#xff0c;它具有解决复杂问题的能力&#xff0c;比如通过医学和法律考试这种类似于人类的能力。然而&am…

k8s学习总结

什么是K8s Kubernetes 是一个可移植的、可扩展的开源平台&#xff0c;用于管理容器化的工作负载和服务&#xff0c;可促进声明式配置和自动化。 Kubernetes 拥有一个庞大且快速增长的生态系统。Kubernetes 的服务、支持和工具广泛可用。 Kubernetes 这个名字源于希腊语&#x…

Java——《面试题——MySQL篇》

前文 java——《面试题——基础篇》 Java——《面试题——JVM篇》 Java——《面试题——多线程&并发篇》 Java——《面试题——Spring篇》 Java——《面试题——SpringBoot篇》 目录 前文 1、数据库的三范式是什么&#xff1f; 2、MySQL数据库引擎有哪些 3、说说…