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的源码如下所示:

java 复制代码
	/**
	 * 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.初始化变量
java 复制代码
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
  • processedRequest :表示当前处理的请求,初始化为原始请求 request
  • mappedHandler :存储找到的处理器链(HandlerExecutionChain)。
  • multipartRequestParsed:标记当前请求是否是多部分请求。
  • asyncManager:管理异步请求的状态。
2.检测和解析多部分请求
java 复制代码
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

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

在 SpringBoot中,MultipartResolver 接口负责解析这种请求。

  • 调用 checkMultipart 方法检测并解析多部分请求。
  • 如果请求被解析为 MultipartHttpServletRequest,则 processedRequest 会被替换,并标记 multipartRequestParsed = true
3.查找请求处理器
java 复制代码
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
    noHandlerFound(processedRequest, response);
    return;
}
  • 调用 getHandler 方法,根据当前请求查找对应的处理器。
  • 如果找不到处理器,调用 noHandlerFound 方法返回 404 响应。
4.获取处理器适配器
java 复制代码
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
  • 根据找到的处理器获取对应的处理器适配器(HandlerAdapter)。
  • Spring MVC 中有多种处理器(如 ControllerHttpRequestHandler 等),处理器适配器负责适配不同类型的处理器。
5.检查 Last-Modified缓存
java 复制代码
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.执行处理器前置拦截器
java 复制代码
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    return;
}
  • 执行所有拦截器的 preHandle 方法。
  • 如果任意一个拦截器返回 false,请求处理链会中断。
7.调用处理器
java 复制代码
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
    return;
}
  • 调用 HandlerAdapterhandle 方法,执行实际的请求处理逻辑。
  • 如果处理器启动了异步处理,立即返回,等待异步处理完成。
8.应用默认视图名并执行后置拦截器
java 复制代码
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
  • 如果返回的 ModelAndView 没有指定视图名称,应用默认视图名称。
  • 执行所有拦截器的 postHandle 方法。
9.异常处理
java 复制代码
catch (Exception ex) {
    dispatchException = ex;
}
catch (Throwable err) {
    dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
  • 捕获请求处理过程中的异常,交由 processDispatchResult 统一处理。
10.清理资源
java 复制代码
finally {
    if (asyncManager.isConcurrentHandlingStarted()) {
        if (mappedHandler != null) {
            mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
        }
    } else {
        if (multipartRequestParsed) {
            cleanupMultipart(processedRequest);
        }
    }
}

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

  1. 请求解析:解析多部分请求。
  2. 处理器查找:根据请求找到对应的处理器和拦截器链。
  3. 请求处理:执行拦截器前置逻辑、调用处理器、执行拦截器后置逻辑。
  4. 异常处理:捕获并处理请求处理过程中的异常。
  5. 清理阶段:清理多部分请求资源或异步请求资源。
相关推荐
Q_19284999061 分钟前
基于Spring Boot的电影网站系统
java·spring boot·后端
豌豆花下猫13 分钟前
Python 潮流周刊#83:uv 的使用技巧(摘要)
后端·python·ai
轻口味16 分钟前
【每日学点鸿蒙知识】Web请求支持Http、PDF展示、APP上架应用搜索问题、APP备案不通过问题、滚动列表问题
前端·http·harmonyos
凡人的AI工具箱20 分钟前
每天40分玩转Django:Django部署概述
开发语言·数据库·后端·python·django
SomeB1oody42 分钟前
【Rust自学】7.2. 路径(Path)Pt.1:相对路径、绝对路径与pub关键字
开发语言·后端·rust
SomeB1oody1 小时前
【Rust自学】7.3. 路径(Path)Pt.2:访问父级模块、pub关键字在结构体和枚举类型上的使用
开发语言·后端·rust
Liveweb视频汇聚平台1 小时前
FFmpeg来从HTTP拉取流并实时推流到RTMP服务器
服务器·http·ffmpeg
Bony-1 小时前
Go语言反射从入门到进阶
开发语言·后端·golang
涔溪1 小时前
如何在Express.js中定义多个HTTP方法?
javascript·http·express
阿moments2 小时前
SpringBoot3-第十篇(整合Web安全)
spring boot·安全·web安全