带着问题读源码——Spring MVC是怎么找到接口实现类的?

引言

我们的产品主打金融服务领域,以B端客户为我们的核心合作伙伴,然而,我们的服务最终将惠及C端消费者。在技术实现上,我们采用了公司自主研发的微服务框架,该框架基于SpringBoot,旨在提供高效、可靠的服务支持。

本文继《生产问题排查系列——未知404状态接口请求》之后,深入探讨并扩展了对我们公司自主研发框架的理解。在上一篇文章中,我们通过应用性能管理工具定位并解决了持续出现的404请求问题。

代码实例

下面给出一个简单的示例,方面读者在后续源码阅读中能着我们的排查思路一起来看上述问题。

pom.xml中引入Spring Boot以及Spring MVC

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.14</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>org.example</groupId><artifactId>springmvc</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

TestController:定义一个请求接口

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;@Controller
public class TestController {@RequestMapping("/test")@ResponseBodypublic String test(){return "ok";}
}

源码阅读

下面我们将从一个Spring提供的Health监控请求,一步一步分析Spring MVC是如何转发请求的。

Spring MVC所有的接口请求都将由doDispatch方法负责转发。源码如下:

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 {processedRequest = checkMultipart(request);multipartRequestParsed = (processedRequest != request);// 确定当前请求处理的实际处理类mappedHandler = getHandler(processedRequest);if (mappedHandler == null) {noHandlerFound(processedRequest, response);return;}// 确定当前请求的处理程序适配器。HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());// 如果处理程序支持,则处理上次修改的标头。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;}}if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}// 调用处理程序mv = ha.handle(processedRequest, response, mappedHandler.getHandler());if (asyncManager.isConcurrentHandlingStarted()) {return;}applyDefaultViewName(processedRequest, mv);mappedHandler.applyPostHandle(processedRequest, response, mv);}catch (Exception ex) {dispatchException = ex;}catch (Throwable err) {// 从4.3开始,我们也在处理处理程序方法抛出的错误,//使它们可用于@ExceptionHandler方法和其他场景。dispatchException = new NestedServletException("Handler dispatch failed", err);}processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);}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()) {// 而不是postHandle和afterCompletionif (mappedHandler != null) {mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);}}else {// 清理由多部分请求使用的所有资源。if (multipartRequestParsed) {cleanupMultipart(processedRequest);}}}}

监控接口请求会转发给AbstractHandlerMethodAdapter.handle方法处理,源码如下:

public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {return handleInternal(request, response, (HandlerMethod) handler);}

然后请求会转发到AbstractHandlerMethodAdapter.handleInternal方法中,源码如下:

protected ModelAndView handleInternal(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {ModelAndView mav;checkRequest(request);// 如果需要,在同步块中执行invokeHandlerMethod。if (this.synchronizeOnSession) {HttpSession session = request.getSession(false);if (session != null) {Object mutex = WebUtils.getSessionMutex(session);synchronized (mutex) {mav = invokeHandlerMethod(request, response, handlerMethod);}}else {// 没有可用的HttpSession->不需要互斥mav = invokeHandlerMethod(request, response, handlerMethod);}}else {// 根本不需要对会话进行同步。。。mav = invokeHandlerMethod(request, response, handlerMethod);}if (!response.containsHeader(HEADER_CACHE_CONTROL)) {if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);}else {prepareResponse(response);}}return mav;}

然后接口请求会转发到RequestMappingHandlerAdapter.invokeHandlerMethod,进行一系列参数填充后调用invokeAndHandle方法,源码如下:

protected ModelAndView invokeHandlerMethod(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {ServletWebRequest webRequest = new ServletWebRequest(request, response);try {WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);if (this.argumentResolvers != null) {invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);}if (this.returnValueHandlers != null) {invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);}invocableMethod.setDataBinderFactory(binderFactory);invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);ModelAndViewContainer mavContainer = new ModelAndViewContainer();mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));modelFactory.initModel(webRequest, mavContainer, invocableMethod);mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);asyncWebRequest.setTimeout(this.asyncRequestTimeout);WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);asyncManager.setTaskExecutor(this.taskExecutor);asyncManager.setAsyncWebRequest(asyncWebRequest);asyncManager.registerCallableInterceptors(this.callableInterceptors);asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);if (asyncManager.hasConcurrentResult()) {Object result = asyncManager.getConcurrentResult();mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];asyncManager.clearConcurrentResult();LogFormatUtils.traceDebug(logger, traceOn -> {String formatted = LogFormatUtils.formatValue(result, !traceOn);return "Resume with async result [" + formatted + "]";});invocableMethod = invocableMethod.wrapConcurrentResult(result);}invocableMethod.invokeAndHandle(webRequest, mavContainer);if (asyncManager.isConcurrentHandlingStarted()) {return null;}return getModelAndView(mavContainer, modelFactory, webRequest);}finally {webRequest.requestCompleted();}}

此时请求会转发到ServletInvocableHandlerMethod.invokeAndHandle,之后调用InvocableHandlerMethod.invokeForRequest执行方法,源码如下:

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);setResponseStatus(webRequest);if (returnValue == null) {if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {disableContentCachingIfNecessary(webRequest);mavContainer.setRequestHandled(true);return;}}else if (StringUtils.hasText(getResponseStatusReason())) {mavContainer.setRequestHandled(true);return;}mavContainer.setRequestHandled(false);Assert.state(this.returnValueHandlers != null, "No return value handlers");try {this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);}catch (Exception ex) {if (logger.isTraceEnabled()) {logger.trace(formatErrorForReturnValue(returnValue), ex);}throw ex;}}

InvocableHandlerMethod.invokeForRequest会调用InvocableHandlerMethod.doInvoke执行实际方法

public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);if (logger.isTraceEnabled()) {logger.trace("Arguments: " + Arrays.toString(args));}return doInvoke(args);}

InvocableHandlerMethod.doInvoke会调用实际的bean执行方法。从这一步的getBean()方法我们就能知道执行spring bean是什么类。

protected Object doInvoke(Object... args) throws Exception {Method method = getBridgedMethod();try {if (KotlinDetector.isSuspendingFunction(method)) {return CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args);}return method.invoke(getBean(), args);}catch (IllegalArgumentException ex) {assertTargetBean(method, getBean(), args);String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");throw new IllegalStateException(formatInvokeError(text, args), ex);}catch (InvocationTargetException ex) {// Unwrap for HandlerExceptionResolvers ...Throwable targetException = ex.getTargetException();if (targetException instanceof RuntimeException) {throw (RuntimeException) targetException;}else if (targetException instanceof Error) {throw (Error) targetException;}else if (targetException instanceof Exception) {throw (Exception) targetException;}else {throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);}}}

总结

上述源码调用流程如下:
在这里插入图片描述

在深入研究Spring MVC的源码调用链路后,有了以下体会:

代码之间的调用关系错综复杂,构成了一个深广交织的网络。通过阅读源码,我们可以发现,从请求的接收到处理再到响应的返回,涉及了众多组件和层次的交互。这种深度的耦合使得没有全景图的情况下,仅凭对各个类功能的碎片化认识,难以形成对框架整体工作机制的实际理解。

为了真正掌握Spring MVC的内在逻辑,后续工作需要着手细致地梳理这些类之间的组织结构,明确它们如何协同工作以及各自的职责边界。这对于后续的框架使用、问题排查乃至自定义扩展都至关重要。

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

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

相关文章

MyBatis笔记梳理

文章目录 什么是 MyBatis&#xff1f;前期准备依赖配置文件mapper利用注解 增、删、改、查查增改删#{} 和 ${} 的区别类型别名 动态sqlwhere ifforeachsql引用不常用标签 多表查询多对一&#xff08;一对一&#xff09;一对多多对多多表查询 个人理解 延迟加载概念使用场景延迟…

Qt/C++音视频开发66-音频变速不变调/重采样/提高音量/变速变调/倍速播放/sonic库使用

一、前言 之前在做倍速这个功能的时候&#xff0c;发现快速播放会有滴滴滴的破音出现&#xff0c;正常1倍速没有这个问题&#xff0c;尽管这个破音间隔很短&#xff0c;要放大音量才能听到&#xff0c;但是总归是不完美的&#xff0c;后面发现&#xff0c;通过修改qaudiooutpu…

2024年美赛数学建模E题思路分析 - 财产保险的可持续性

# 1 赛题 问题E&#xff1a;财产保险的可持续性 极端天气事件正成为财产所有者和保险公司面临的危机。“近年来&#xff0c;世界已经遭受了1000多起极端天气事件造成的超过1万亿美元的损失”。[1]2022年&#xff0c;保险业的自然灾害索赔人数“比30年的平均水平增加了115%”。…

[Java]JDK 安装后运行环境的配置

这篇文章用于介绍jdk.exe安装之后的运行环境配置&#xff0c;以及如何检查是否安装成功 检查自己是否安装jdk环境&#xff0c;记住这个安装的改的路径: (应该要安装2个&#xff0c;一个是jdk,一个是jre) 安装后的在文件夹的样子(路径自定义&#xff0c;在java下面): 参考如下…

【Springcloud篇】学习笔记二(四至六章):Eureka、Zookeeper、Consul

第四章_Eureka服务注册与发现 1.Eureka基础知识 1.1Eureka工作流程-服务注册 1.2Eureka两大组件 2.单机Eureka构建步骤 IDEA生成EurekaServer端服务注册中心&#xff0c;类似于物业公司 EurekaClient端cloud-provider-payment8081将注册进EurekaServer成为服务提供者provide…

Pytest框架测试

Pytest 是什么? pytest 能够支持简单的单元测试和复杂的功能测试;pytest 可以结合 Requests 实现接口测试; 结合 Selenium、Appium 实现自动化功能测试;使用 pytest 结合 Allure 集成到 Jenkins 中可以实现持续集成。pytest 支持 315 种以上的插件;为什么要选择 Pytest 丰…

VUE项目导出excel

导出excel主要可分为以下两种&#xff1a; 1. 后端主导实现 流程&#xff1a;前端调用到导出excel接口 -> 后端返回excel文件流 -> 浏览器会识别并自动下载 场景&#xff1a;大部分场景都有后端来做 2. 前端主导实现 流程&#xff1a;前端获取要导出的数据 -> 把常规数…

跨平台开发:浅析uni-app及其他主流APP开发方式

随着智能手机的普及&#xff0c;移动应用程序&#xff08;APP&#xff09;的需求不断增长。开发一款优秀的APP&#xff0c;不仅需要考虑功能和用户体验&#xff0c;还需要选择一种适合的开发方式。随着技术的发展&#xff0c;目前有多种主流的APP开发方式可供选择&#xff0c;其…

亚马逊新店铺视频怎么上传?视频验证失败怎么办?——站斧浏览器

亚马逊新店铺视频怎么上传&#xff1f; 登录亚马逊卖家中心&#xff1a;首先&#xff0c;卖家需要登录亚马逊卖家中心。在登录后&#xff0c;可以点击左侧导航栏上的“库存”选项&#xff0c;然后选择“新增或管理商品”。 选择商品&#xff1a;接下来&#xff0c;在“新增或…

【Vue】3-2、组合式 API

一、setup 选项 <script> export default {/*** 1、setup 执行时机早于 beforeCreate* 2、setup 中无法获取 this* 3、数据和函数需要在 setup 最后 return&#xff0c;才能在模板中使用* 4、可以通过 setup 语法糖简化代码*/setup(){// console.log(setup function, thi…

云服务器安全组、防火墙、端口问题,结合telnet解决项目部署无法访问

无论是运维还是后台亲自操刀在云服务器上部署项目&#xff0c;往往会遇到项目部署上去了&#xff0c;也确定项目正常运行&#xff0c;但还是没法访问的问题。 如果没有经验的小伙伴&#xff0c;很容易陷入疑惑的状态&#xff0c;无从下手解决。 其实这涉及到云平台安全组、服…

计算机毕业设计社区居民服务管理系统SSM

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; vue mybatis Maven mysql5.7或8.0等等组成&#xff0c;B…

25.云原生之ArgoCD-app of apps模式

文章目录 app of apps 模式介绍app如何管理apphelm方式管理kustomize方式管理 app of apps 模式介绍 通过一个app来管理其他app&#xff0c;当有多个项目要发布创建多个app比较麻烦&#xff0c;此时可以创建一个管理app&#xff0c;管理app创建后会创建其他app。比较适合项目环…

【Power Platform】实现对SharePoint文档库中上传的文件进行审批

这次要分享的案例还是来自于我们客户的一个新需求。 我们这个客户主要是在使用SharePoint的List来搭建申请单&#xff0c;然后对申请单进行审批&#xff0c;但由于我们之前给客户提出的生成PDF打印件的方案&#xff0c;是需要先在SharePoint或OneDrive中放一个文档模板的&…

2024 高级前端面试题之 HTTP模块 「精选篇」

该内容主要整理关于 HTTP模块 的相关面试题&#xff0c;其他内容面试题请移步至 「最新最全的前端面试题集锦」 查看。 HTTP模块精选篇 1. HTTP 报文的组成部分2. 常见状态码3. 从输入URL到呈现页面过程3.1 简洁3.2 详细 4. TCP、UDP相关5. HTTP2相关6. https相关7. WebSocket的…

计算存储设备(Computational Storage Drive, CSD)

随着云计算、企业级应用以及物联网领域的飞速发展&#xff0c;当前的数据处理需求正以前所未有的规模增长&#xff0c;以满足存储行业不断变化的需求。这种增长导致网络带宽压力增大&#xff0c;并对主机计算资源&#xff08;如内存和CPU&#xff09;造成极大负担&#xff0c;进…

网络协议与攻击模拟_14DNS欺骗

DNS欺骗就是利用某种方式将我们访问的域名解析到其他服务器上&#xff0c;从而使得我们无法正常访问到原本我们想要访问的网站。 一、DNS欺骗过程 首先在windows server 2016虚拟机上搭建网站服务&#xff0c;kali作为攻击机从而实现中间人攻击&#xff0c;使用ettercap工具的…

构造函数 | 初始化列表 | 拷贝构造 | 赋值运算符重载

文章目录 1.类的6个默认成员函数2.构造函数2.1基本概念和用法2.2初始化列表2.3explicit关键字 3.拷贝构造函数4.重载赋值运算符 1.类的6个默认成员函数 如果定义一个空类&#xff0c;其实并不是什么都没有&#xff0c;编译器会默认生成6个默认的成员函数&#xff01;默认成员函…

从源代码看Chrome 版本号

一直以来都是用Chrome 浏览器&#xff0c;但是看到Chrome 点分4 组数据的表达方式&#xff0c;总是感觉怪怪的&#xff0c;遂深入源代码了解她的版本号具体表示的内容 chrome 浏览器中显示的版本号 源代码中的版本号标识 版本号文件位于 chrome/VERSION &#xff0c; 看到源代…

网络流数据集处理(深度学习数据处理基础)

一、数据集处理 处理数据集是一个文件夹 一个文件夹处理的&#xff0c;将原网络流数据集 放入一个文件夹 处理转换成 Json文件。&#xff08;数据预处理&#xff09;然后将这些文件处理成目标文件格式 再分割成训练集和测试集。每次运行只会处理一个文件夹。 运行train.py 导入…