SpringMVC系列-2 HTTP请求调用链

news2024/9/21 4:20:00

背景

本文作为 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对象,流程图如下所示:
在这里插入图片描述
总之,当Http请求到达Tomcat连接池后,会将请求消息封装成(HttpRequest/HttpResponse对象), 通过调用Servlet标准接口实现消息的传递。
SpringMVC框架对应的Servlet对象为DispatcherServlet,即调用栈会进入DispatcherServlet的void service(ServletRequest req, ServletResponse res)方法。
因此,有必要了解一下DispatcherServlet类的继承关系以及对Servlet方法实现情况,如下图所示:
![在这里

2.HttpServlet

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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);
	} 
}

上述逻辑可以表示为:
【插图】

拦截器的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方法。
在这里插入图片描述
涉及的代码逻辑如下:

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方法,流程如下所示:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/674214.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

软考A计划-系统集成项目管理工程师--一般常识-上

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff…

SpringBoot 异常处理的主要组件有哪些

SpringBoot 异常处理的主要组件有哪些 在 SpringBoot 应用程序中&#xff0c;异常处理是一个非常重要的话题。当应用程序出现异常时&#xff0c;我们需要对异常进行处理&#xff0c;以保证应用程序的稳定性和可靠性。SpringBoot 提供了一系列的组件&#xff0c;用于处理各种类…

简析IAST—Agent篇 | 信息安全

一、IAST简单介绍 IAST(Interactive Application Security Testing)交互式应用程序安全测试&#xff0c;通过服务端部署Agent探针&#xff0c;流量代理/VPN或主机系统软件等方式&#xff0c;监控Web应用程序运行时函数执行并与扫描器实时交互&#xff0c;高效、精准的安全漏洞&…

LeetCode 面试题 16.19. 水域大小

LeetCode 面试题 16.19. 水域大小 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;https://leetcode.cn/problems/group-anagrams/description/ 博主Github&#xff1a;https://github.com/GDUT-Rp/LeetCode 题目&#xff1a; 你有一个用于表示一片…

C++初阶之初识C++

初识C 1.什么是C2.C的发展史2.1 历史渊源2.2 名称由来2.3 C标准 3.C的重要性3.1 语言的使用广泛度3.2 工作领域 4.如何学习C5.结语 1.什么是C C语言是结构化和模块化的语言&#xff0c;适合处理较小规模的程序。对于复杂的问题&#xff0c;规模较大的 程序&#xff0c;需要高度…

【k8s系列】一分钟搭建MicroK8s Dashboard

本文基于上一篇文章的内容进行Dashboard搭建&#xff0c;如果没有看过上一篇的同学请先查阅上一篇文章 k8s系列】使用MicroK8s 5分钟搭建k8s集群含踩坑经验 使用MicroK8s搭建Dashboard很简单&#xff0c;只需要在Master节点按照以下几步操作 1.启用Dashboard插件 microk8s en…

“Vue3+Vite打包后,白屏沉默,重启重试无果,我该如何解决?”

每次最后打包总是会或多或少出现一些问题&#xff0c;昨天打包项目完之后 直接点击dist中的index.html去看看有没有什么发题&#xff0c;一打开奇怪的事发生了&#xff0c;居然是空白&#xff1b;但是在vscode中右键Open with Live Server一看项目没啥问题&#xff0c;但是部署…

下载安装Python解释器和环境变量配置

一、 python解释器下载 1.百度python官网并打开 官网网址&#xff1a;https://www.python.org/ 在这里插入图片描述 2.选择DownLoads&#xff0c;Windows并打开 3.在下图页面选择你需要的版本下载即可&#xff08;本文以python 3.10.4版本演示&#xff09; 在这里插入图片描述 …

C++笔记之extern关键字

C笔记之extern关键字 code review! 文章目录 C笔记之extern关键字0.前言1.extern是C语言的关键字还是C中的关键字&#xff1f;2.extern关键字和全局变量3.ChatGpt讲述extern的用法4.extern一般用法4.1.在本模块中使用4.2.跨模块中使用 5.标准定义使用extern关键字的步骤7.ext…

Nginx 访问日志中有 Get 别的网站的请求是什么原因?

&#x1f482; 个人网站:【海拥】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 目录 前言理解 Nginx 访问日…

父亲的打,最让人想念。

11年前的端午节&#xff0c;父亲就走了。 父亲十五岁就外出打工&#xff0c;从小出去打工&#xff0c;吃了没有文化的苦&#xff0c;父亲从小教育我要好好读书&#xff0c;可我从来听不进。 所以我经常挨打。 老家在湖南的一个农村&#xff0c;我父亲十几岁就南下打工&#xff…

[进阶]网络通信:TCP通信,一发一收,多发多收

TCP通信 特点&#xff1a;面向连接、可靠通信。通信双方事先会采用 “三次握手〞 方式建立可靠连接&#xff0c;实现端到端的通信&#xff1b;底层能保证数据成功传给服务端。Java提供了一个java.net.socket类来实现TCP通信。 TCP通信——客户端开发 客户端程序就是通过java.…

基于OpenCV-车辆检测项目(简易版)

车辆检测 1.项目介绍2. 读取一段视频3.通过形态学处理识别车辆4.描画轮廓5. 车辆计数并显示 本项目使用的视频地址链接 1.项目介绍 对一个视频进行车辆数量的检测&#xff0c;用到的知识有视频的读取&#xff0c;滤波器&#xff0c;形态学&#xff0c;添加直线、文本&#xff…

黑马头条2

文章目录 前言一、接口工具1.1 postman1.2 swagger1.3 knife4j 二、 网关一、基本搭建二、全局过滤器jwt 三、 前端集成导入前端工程项目安装nginx测试 &#x1f315;博客x主页&#xff1a;己不由心王道长&#x1f315;! &#x1f30e;文章说明&#xff1a;黑马头条开发&#x…

一站式数据可观测性平台 Datavines 正式开源啦

Datavines是一站式开源数据可观测性平台&#xff0c;提供元数据管理、数据概览报告、数据质量管理&#xff0c;数据分布查询、数据趋势洞察等核心能力&#xff0c;致力于帮助用户全面地了解和掌管数据&#xff0c;让您做到心中有数&#xff0c;目前作为 Datavane 开源组织的重点…

Java ForkJoin 简介和应用

Java 并行框架 Fork Join 一.Fork Join 简介1.框架说明2.任务说明 二.应用示例1.RecursiveTask分组示例分组求和 2.RecursiveAction3.CountedCompleter 三.ForkJoin 实践代码测试1.测试用 Excel 文件2.读取结果 一.Fork Join 简介 1.框架说明 ForkJoinPool 继承自 AbstractEx…

【Java-SpringBoot+Vue+MySql】Day4-VUE框架使用

一、VUE入门 1、环境准备 2、预备知识 3、实战演练 vue官网 Vue.js - 渐进式 JavaScript 框架 | Vue.js 基础语法&#xff0c;vue2和vue3区别不大&#xff0c;但是后面路由会有很大区别。 前期基础语法&#xff0c;我们通过链接的方式使用vue&#xff0c;后面会用npm进行安装…

Transformer-XL模型简单介绍

目录 一、前言 二、整体概要 三、细节描述 3.1 状态复用的块级别循环 3.2 相对位置编码 四、论文链接 一、前言 以自注意力机制为核心的 Transformer 模型是各种预训练语言模型中的主要组成部分。自注意力机制能够构建序列中各个元素之间的上下文关联程度&#xff0c;挖掘…

java 2023秒杀项目 day(1) 面经

java 2023杀项目 day(1) 面经 一、秒杀项目1.1 如何设计秒杀系统1.2 数据库 二、业务2.1 登录2.2.1 密码加密 2.2.2 密码参数校验2.2.3 分布式session2.2.3.1 解决方案 2.2.4 参数解析器 2.3 异常处理2.3.1 ControllerAdvicerExceptionHandler 2.4 秒杀2.4.1 逻辑2.4.1 秒杀前判…

图像处理——以支票识别为例

用到环境 1、pycharm community edition 2022.3.2 2、Python 3.10 后续应该会在资源上传项目&#xff0c;需要的话可以私信我。 流程 图1 扩展实验“金额识别”流程图 正文 导入 cv2、pytesseract、re 和 locale 模块。 使用 cv2.imread() 函数加载名为 cheque.jpg 的支票图像…