当前位置: 首页 > article >正文

SpringBoot揭秘:URL与HTTP方法如何定位到Controller

1. 引言

相信作为一名程序员,无论是前端还是后端,都曾遇到过计算机网络领域的经典面试题:“当用户在浏览器中输入一个URL会发生什么?”
大多数人对这一问题的回答可能已经烂熟于心:从URL解析、DNS域名解析,到三次握手建立TCP连接,发送HTTP请求,服务器处理请求并返回响应,最后四次挥手关闭连接、页面渲染——整个流程清晰而流畅。

具体问题解答可以参考我的博客: 计算机网络经典面试题

然而,对于后端工程师来说,还有一道更具挑战性的经典问题:“当用户在浏览器中输入一个URL,后端框架(如Spring Boot)是如何精准定位到具体的Controller方法的?”
你或许每天都在使用@Controller@RestController@RequestMapping等注解,也熟悉它们的功能,但你是否想过,这些看似简单的注解背后,隐藏着怎样复杂的逻辑?Spring Boot究竟是如何从千变万化的URL和HTTP方法中,迅速找到唯一匹配的处理方法?

如果你曾对这些问题感到好奇,那么恭喜你!这篇文章将以Spring Boot为例,为你揭开“幕后故事”:

  • URL和HTTP方法的匹配逻辑:Spring如何优雅地解析复杂路径和方法?
  • 底层源码剖析DispatcherServlet到底在忙些什么?
  • 高效请求分发的秘诀:你写的Controller为什么能如此“聪明”?

2.HTTP请求处理流程

在 Spring Boot 中,每个 HTTP 请求的背后,都隐藏着一条严密而高效的执行路径。从请求进入到最终获得响应,框架内部经历了五个关键环节:DispatcherServletHandlerMappingHandlerAdapterHandler(Controller)和执行业务逻辑。我将结合一个流程图,为你详细解析这些环节是如何协作的。

1. DispatcherServlet

DispatcherServlet 是 Spring MVC 的核心前端控制器。所有进入应用的 HTTP 请求,都会首先被它拦截。
其主要职责是分发请求,将其交由合适的处理器(Handler)处理,并最终生成响应。

  • 初始化阶段:在应用启动时,DispatcherServlet 会加载所有的配置文件和组件,例如HandlerMappingHandlerAdapter
  • 请求分发:当请求到达时,它会根据内部组件的协作结果,将请求委派给相应的处理器。
2. HandlerMapping

HandlerMapping 的职责是根据请求的 URL 和 HTTP 方法,找到能够处理该请求的处理器(Handler)。
它内部维护了一系列的映射规则,例如路径匹配规则(@RequestMapping注解配置的路径)。

  • 核心逻辑:通过遍历所有注册的处理器,找到匹配当前请求的那个处理器,并将其封装为HandlerExecutionChain,而HandlerExecutionChain用于封装一个处理器(Handler)和与该处理器相关联的拦截器(Interceptors)。
  • 扩展点:支持自定义路径匹配策略,如正则表达式或路径模板。
3. HandlerAdapter

HandlerAdapter 的职责是适配处理器,让框架能够灵活支持多种类型的处理器。例如,普通的@ControllerRestController,都需要不同的适配方式。

  • 核心逻辑:根据HandlerExecutionChain提供的处理器类型,选择对应的HandlerAdapter来执行处理器逻辑。
  • 扩展点:可以自定义HandlerAdapter,以支持特殊类型的处理器。
4. Handler(Controller)

当请求到达具体的Controller时,业务逻辑开始被执行。
这里的处理器通常是带有@Controller@RestController注解的类。

  • 参数解析:Spring 会通过HandlerMethodArgumentResolver为方法中的参数自动赋值(例如,从请求体或 URL 中提取参数)。
  • 方法执行:执行具体的业务逻辑,并返回ModelAndView(传统 MVC 模式)或直接返回 JSON 数据。
5. 生成响应:结果返回客户端

执行完业务逻辑后,DispatcherServlet 将负责处理返回的结果,并生成最终的 HTTP 响应。

  • 视图解析(传统 MVC):若返回的是ModelAndView,则通过ViewResolver解析为 HTML 页面。
  • 直接响应:对于 RESTful 风格的 API,结果通常直接返回 JSON 格式的数据。

具体整体的业务处理流程如下所示:
在这里插入图片描述

3.DispatcherServlet源码解析

在 Spring MVC 的核心组件 DispatcherServlet 中,doDispatch 方法是其处理 HTTP 请求的核心方法。它实现了从请求到响应的整个控制流程,包括请求解析、处理器分发、业务逻辑执行以及结果返回。

有关doDispatch的源码如下所示:

	/**
	 * Process the actual dispatching to the handler.
	 * <p>The handler will be obtained by applying the servlet's HandlerMappings in order.
	 * The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters
	 * to find the first that supports the handler class.
	 * <p>All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers
	 * themselves to decide which methods are acceptable.
	 * @param request current HTTP request
	 * @param response current HTTP response
	 * @throws Exception in case of any kind of processing failure
	 */
	@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 {
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				// Determine handler for the current request.
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				// Determine handler adapter for the current request.
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// Process last-modified header, if supported by the handler.
				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;
				}

				// Actually invoke the handler.
				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) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				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()) {
				// Instead of postHandle and afterCompletion
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
			}
			else {
				// Clean up any resources used by a multipart request.
				if (multipartRequestParsed) {
					cleanupMultipart(processedRequest);
				}
			}
		}
	}

具体的源码分析如下:

1.初始化变量
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

  • processedRequest:表示当前处理的请求,初始化为原始请求 request
  • mappedHandler:存储找到的处理器链(HandlerExecutionChain)。
  • multipartRequestParsed:标记当前请求是否是多部分请求。
  • asyncManager:管理异步请求的状态。
2.检测和解析多部分请求
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

HTTP 协议中的普通请求通常是表单数据或 JSON 数据,而多部分请求(Multipart Request)通常是用来处理上传文件的请求,内容会以分段的形式组织。
在 SpringBoot中,MultipartResolver 接口负责解析这种请求。

  • 调用 checkMultipart 方法检测并解析多部分请求。
  • 如果请求被解析为 MultipartHttpServletRequest,则 processedRequest 会被替换,并标记 multipartRequestParsed = true
3.查找请求处理器
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
    noHandlerFound(processedRequest, response);
    return;
}
  • 调用 getHandler 方法,根据当前请求查找对应的处理器。
  • 如果找不到处理器,调用 noHandlerFound 方法返回 404 响应。
4.获取处理器适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
  • 根据找到的处理器获取对应的处理器适配器(HandlerAdapter)。
  • Spring MVC 中有多种处理器(如 ControllerHttpRequestHandler 等),处理器适配器负责适配不同类型的处理器。
5.检查 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;
    }
}
  • 如果请求是 GETHEAD,检查处理器是否支持 Last-Modified
  • 如果内容未修改,直接返回 304 响应,不需要重复执行控制器方法处理请求。

这一段代码的主要目的是实现 HTTP 的缓存验证机制:

  1. 优化请求处理:如果资源未修改,直接返回 304 Not Modified,避免重复生成内容。
  2. 节省带宽:客户端可以直接使用缓存的内容,而不需要重新下载。
  3. 提高性能:减少服务器的处理开销。

这里插入介绍一下HTTP各种请求方法的作用及场景:

HTTP方法作用常见场景
GET获取资源数据查询、页面访问
POST创建资源或提交数据表单提交、文件上传
HEAD获取响应头数据检查资源、验证缓存
PUT创建或替换资源更新资源
PATCH局部更新资源更新部分字段
DELETE删除资源删除用户、文件等
OPTIONS检查支持的HTTP方法调式CORS
TRACE回显请求,用于调试调试 HTTP 请求路径(禁用)
6.执行处理器前置拦截器
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    return;
}
  • 执行所有拦截器的 preHandle 方法。
  • 如果任意一个拦截器返回 false,请求处理链会中断。
7.调用处理器
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
    return;
}
  • 调用 HandlerAdapterhandle 方法,执行实际的请求处理逻辑。
  • 如果处理器启动了异步处理,立即返回,等待异步处理完成。
8.应用默认视图名并执行后置拦截器
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
  • 如果返回的 ModelAndView 没有指定视图名称,应用默认视图名称。
  • 执行所有拦截器的 postHandle 方法。
9.异常处理
catch (Exception ex) {
    dispatchException = ex;
}
catch (Throwable err) {
    dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
  • 捕获请求处理过程中的异常,交由 processDispatchResult 统一处理。
10.清理资源
finally {
    if (asyncManager.isConcurrentHandlingStarted()) {
        if (mappedHandler != null) {
            mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
        }
    } else {
        if (multipartRequestParsed) {
            cleanupMultipart(processedRequest);
        }
    }
}

doDispatch 方法的核心业务逻辑可以分为以下几个阶段:

  1. 请求解析:解析多部分请求。
  2. 处理器查找:根据请求找到对应的处理器和拦截器链。
  3. 请求处理:执行拦截器前置逻辑、调用处理器、执行拦截器后置逻辑。
  4. 异常处理:捕获并处理请求处理过程中的异常。
  5. 清理阶段:清理多部分请求资源或异步请求资源。

http://www.kler.cn/a/454555.html

相关文章:

  • C++(7)—inline和nullptr
  • CCF-GESP 等级考试 2023年12月认证C++五级真题解析
  • scala基础学习(数据类型)-数组
  • 模拟——郑益慧_笔记1_绪论
  • 在 RK3568 Linux 系统上使用 TUN 设备:详细教程
  • Java开发经验——数据库开发经验
  • Excel中一次查询返回多列
  • golang实现生产者消费者模式
  • 随机变量是一个函数-如何理解
  • 【MySQL】踩坑笔记——保存带有换行符等特殊字符的数据,需要进行转义保存
  • 算法题(17):删除有序数组中的重复项
  • k8s coredns
  • 简单发布一个npm包
  • Ubuntu 24.04.1 LTS 配置静态固定IP地址
  • 计算机专业文献检索期末论文
  • 计算机网络——期末复习(3)4-6章考试重点
  • 零基础微信小程序开发——页面导航之编程式导航(保姆级教程+超详细)
  • 爬虫数据存储:Redis、MySQL 与 MongoDB 的对比与实践
  • 007-利用切面计算方法耗时
  • vue中el-select选择框带搜索和输入,根据用户输入的值显示下拉列表
  • R语言的数据类型
  • 随手笔记【六】
  • TDesign:Tabs 选项卡
  • Boost之log日志使用
  • Elasticsearch安装和数据迁移
  • [微服务]elasticsearc客服端操作