SpringBoot请求处理
开篇:一个 HTTP 请求在 SpringBoot 中经历了什么?
当你在浏览器里输入一个 URL 敲下回车,到页面显示出结果,中间发生了什么?对于 SpringBoot 来说,这个过程就像一封信在邮局里的旅程:先过安检(Filter),再由分拣员(DispatcherServlet)根据地址(URL)找到对应的投递员(Controller),投递员拆开信件读取内容(参数绑定),处理完业务后写好回信(响应),最后寄回去。
如果途中信件格式不对(参数校验失败)、地址写错了(404)、或者投递员生病了(异常),邮局还有一套完善的异常处理机制来兜底。
这篇文章会带你走完这封"信"的完整旅程。
一、请求处理流程
核心链路
一个 HTTP 请求在 SpringBoot 中的处理流程如下:
DispatcherServlet:所有请求的入口
DispatcherServlet 是 SpringMVC 的核心。它继承自 HttpServlet,所有的 HTTP 请求都会先到达它的 doDispatch() 方法。这个方法是整个请求处理的骨架:
// 简化后的 doDispatch() 核心逻辑
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
// 1. 检查是否文件上传请求
processedRequest = checkMultipart(request);
// 2. 找到能处理当前请求的 Handler(核心!)
HandlerExecutionChain mappedHandler = getHandler(processedRequest);
// 3. 找到对应的 HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 4. 执行拦截器的 preHandle
mappedHandler.applyPreHandle(processedRequest, response);
// 5. 真正执行目标方法
ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 6. 执行拦截器的 postHandle
mappedHandler.applyPostHandle(processedRequest, response, mv);
// 7. 处理派发结果(视图渲染或异常处理)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}HandlerMapping:URL 到 Controller 的映射
getHandler() 方法会遍历所有的 HandlerMapping,找到能处理当前请求的那一个。SpringBoot 默认注册了多个 HandlerMapping,最常用的是 RequestMappingHandlerMapping。
它在应用启动时就扫描了所有 @RequestMapping 注解,建立了 URL 与 Controller 方法之间的映射关系。当请求进来时,它会根据 URL、HTTP 方法、请求头等信息进行匹配,找到对应的 HandlerMethod。
如果 URL 相同但 HTTP 方法不同(比如 GET 和 POST 映射到同一路径),Spring 会通过 RequestMappingInfo 中的各种 Condition 来精确匹配。
参数解析:26 种解析器各显神通
找到 Controller 方法后,Spring 需要给方法参数赋值。这一步由参数解析器完成,SpringMVC 内置了 26 种参数解析器,覆盖了各种注解:
| 注解 | 作用 | 解析器 |
|---|---|---|
@PathVariable | 获取路径变量 | PathVariableMethodArgumentResolver |
@RequestParam | 获取请求参数 | RequestParamMethodArgumentResolver |
@RequestBody | 获取请求体(JSON) | RequestResponseBodyMethodProcessor |
@RequestHeader | 获取请求头 | RequestHeaderMethodArgumentResolver |
@CookieValue | 获取 Cookie 值 | ServletCookieValueMethodArgumentResolver |
Map/Model | 向 request 域放数据 | MapMethodProcessor / ModelMethodProcessor |
参数解析的逻辑很简单:遍历所有参数解析器,问它"你支持这个参数类型吗?",找到支持的那个,让它解析出参数值。
Map 和 Model 参数的秘密
当你在 Controller 方法中声明 Map 或 Model 类型的参数时,往里面放的数据最终都会被放到 request.setAttribute() 中。因为它们底层共享同一个 BindingAwareModelMap 对象,在视图渲染阶段会被合并到 request 域。
返回值处理与内容协商
方法执行完后,返回值由返回值处理器处理。如果方法标注了 @ResponseBody,就由 RequestResponseBodyMethodProcessor 处理。
处理过程中有一个关键环节——内容协商(Content Negotiation):
- 获取客户端能接受的媒体类型(从 Accept 请求头中读取)
- 获取服务端能生产的媒体类型(遍历所有 MessageConverter 看谁能处理)
- 两者匹配,选出最佳的媒体类型
- 用对应的 MessageConverter 将对象转换为目标格式(如 JSON)并写出
SpringBoot 默认引入了 MappingJackson2HttpMessageConverter,它的 supports() 方法直接返回 true——什么类型都能转,最终利用 Jackson 把对象序列化为 JSON。
二、参数绑定与校验
JSR303 后端校验
前端校验可以被绕过,后端校验才是数据安全的最后一道防线。SpringBoot 整合了 JSR303(Bean Validation),使用起来很简单。
第一步:给实体类属性加校验注解
public class UserDTO {
@NotBlank(message = "用户名不能为空")
private String username;
@Email(message = "邮箱格式不正确")
private String email;
@Min(value = 0, message = "年龄不能为负数")
@Max(value = 150, message = "年龄不合理")
private Integer age;
@NotEmpty(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
}第二步:在 Controller 方法参数上加 @Valid 或 @Validated
@PostMapping("/user")
public Result createUser(@Valid @RequestBody UserDTO user) {
userService.create(user);
return Result.ok();
}如果校验不通过,Spring 会抛出 MethodArgumentNotValidException,配合全局异常处理器可以统一返回错误信息。
分组校验:不同场景(新增 vs 修改)对同一字段的校验规则可能不同。通过分组接口来区分:
public interface AddGroup {}
public interface UpdateGroup {}
public class BrandDTO {
@Null(message = "新增时不能指定ID", groups = AddGroup.class)
@NotNull(message = "修改时必须指定ID", groups = UpdateGroup.class)
private Long id;
@NotBlank(message = "品牌名不能为空", groups = {AddGroup.class, UpdateGroup.class})
private String name;
}使用时用 @Validated(AddGroup.class) 指定分组。
三、过滤器与拦截器
Filter vs Interceptor
两者都能对请求进行拦截处理,但作用层面不同:
| 对比维度 | Filter(过滤器) | Interceptor(拦截器) |
|---|---|---|
| 规范 | Servlet 规范 | Spring 框架 |
| 作用范围 | 所有请求(包括静态资源) | 只拦截 DispatcherServlet 处理的请求 |
| 执行顺序 | 在 DispatcherServlet 之前 | 在 DispatcherServlet 之后、Controller 之前 |
| 能否获取 Handler 信息 | 不能 | 可以(参数中有 handler 对象) |
| 注入 Spring Bean | 需要额外配置 | 天然支持 |
简单说:Filter 更底层,拦截范围更广;Interceptor 更上层,和 Spring 生态集成更好。
Filter 的两种注册方式
方式一:@WebFilter + @ServletComponentScan
@WebFilter(urlPatterns = "/*")
public class LogFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
long start = System.currentTimeMillis();
chain.doFilter(req, res);
System.out.println("请求耗时: " + (System.currentTimeMillis() - start) + "ms");
}
}启动类上加 @ServletComponentScan 启用扫描。
方式二:FilterRegistrationBean(推荐)
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<LogFilter> logFilter() {
FilterRegistrationBean<LogFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new LogFilter());
bean.setUrlPatterns(Arrays.asList("/*"));
bean.setOrder(-1); // 数字越小优先级越高
return bean;
}
}推荐方式二,因为 @WebFilter 配合 @Order 指定优先级可能会失效。
Interceptor 的使用
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
// 目标方法执行前:登录检查
if (request.getSession().getAttribute("user") == null) {
response.sendRedirect("/login");
return false; // 返回 false 则不再继续执行
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) {
// 目标方法执行后、视图渲染前
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
// 视图渲染后,无论是否异常都会执行(适合资源清理)
}
}注册拦截器:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login", "/css/**", "/js/**");
}
}拦截器的执行顺序
如果有多个拦截器,执行顺序如下:
preHandle-1 → preHandle-2 → Controller → postHandle-2 → postHandle-1
→ 视图渲染 → afterCompletion-2 → afterCompletion-1注意:preHandle 是正序执行,postHandle 和 afterCompletion 是倒序执行。如果任一个 preHandle 返回 false,后续的 preHandle 不再执行,已执行的 preHandle 对应的 afterCompletion 会倒序执行。
四、全局异常处理
默认行为
SpringBoot 默认会将所有未处理的异常映射到 /error 路径,由内置的 BasicErrorController 处理:
- 浏览器访问:返回 Whitelabel 错误页面(HTML)
- API 调用:返回 JSON 格式的错误信息
你也可以在 resources/static/error/ 或 resources/templates/error/ 下放置自定义错误页面,按 HTTP 状态码命名:404.html、5xx.html。
@ControllerAdvice + @ExceptionHandler(推荐)
这是最常用的全局异常处理方案:
@RestControllerAdvice
public class GlobalExceptionHandler {
// 处理参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidException(MethodArgumentNotValidException e) {
Map<String, String> errors = new HashMap<>();
e.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return Result.fail(400, "参数校验失败", errors);
}
// 处理业务异常
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e) {
return Result.fail(e.getCode(), e.getMessage());
}
// 兜底:处理所有其他异常
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
log.error("系统异常", e);
return Result.fail(500, "系统内部错误");
}
}异常处理的底层流程
当 Controller 方法抛出异常时,doDispatch() 的 catch 块会捕获它,然后交给 processDispatchResult() 处理。这个方法会遍历所有的 HandlerExceptionResolver:
DefaultErrorAttributes:先把错误信息存到 request 域,然后返回 null(表示自己不处理)ExceptionHandlerExceptionResolver:查找@ExceptionHandler方法来处理异常ResponseStatusExceptionResolver:处理带有@ResponseStatus注解的异常DefaultHandlerExceptionResolver:处理 Spring 内置的标准异常(如 404、405)
如果所有 Resolver 都无法处理,异常会被重新抛出,Tomcat 会发起一个 /error 请求,由 BasicErrorController 兜底处理。
五、跨域解决方案
什么是跨域?
浏览器的同源策略要求:协议、域名、端口三者必须完全相同,否则 AJAX 请求会被拦截。比如前端跑在 localhost:5173,后端在 localhost:8080,端口不同就会触发跨域。
注意:跨域限制是浏览器行为,Postman 等工具不受影响。
三种解决方案
方案一:@CrossOrigin 注解(简单场景)
@CrossOrigin(origins = "http://localhost:5173")
@RestController
public class UserController {
// 该 Controller 下的所有接口允许来自指定源的跨域请求
}方案二:全局 CORS 配置(推荐)
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:5173")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}方案三:CorsFilter(网关场景)
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}如果项目使用了 Spring Cloud Gateway,CORS 配置应该放在网关层,避免重复配置导致响应头冲突。
方案四:前端代理(开发阶段)
在 Vite/Webpack 的开发配置中设置代理,让前端请求先到本地代理服务器,由代理转发给后端,绕过浏览器的跨域限制:
// vite.config.js
proxy: {
"/api": {
target: "http://127.0.0.1:8080",
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, "")
}
}这种方式只在开发阶段有效,生产环境还是需要后端配置 CORS 或使用 Nginx 反向代理。
六、JSON 请求体的 IO 流问题
在使用拦截器时,如果你在 preHandle() 中通过 request.getReader() 读取了 JSON 请求体,后面 Controller 中的 @RequestBody 就会报错——因为 Servlet 的输入流只能读一次。
解决方案是使用装饰者模式包装 Request 对象,在 Filter 中将请求体缓存起来:
public class CachedBodyRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public CachedBodyRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
body = request.getInputStream().readAllBytes();
}
@Override
public ServletInputStream getInputStream() {
ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
public int read() { return bais.read(); }
public boolean isFinished() { return bais.available() == 0; }
public boolean isReady() { return true; }
public void setReadListener(ReadListener l) {}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
}然后在 Filter 中包装请求并传递下去:
public class CachingFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
if ("application/json".equalsIgnoreCase(request.getContentType())) {
chain.doFilter(new CachedBodyRequestWrapper(request), res);
} else {
chain.doFilter(req, res);
}
}
}这样,拦截器和 Controller 都能正常读取请求体了。
七、常见面试题精选
1. SpringMVC 的请求处理流程?
请求到达 DispatcherServlet 的 doDispatch() 方法 -> HandlerMapping 找到对应的 Handler 和拦截器链 -> HandlerAdapter 适配并执行 Handler -> 参数解析器解析方法参数 -> 执行 Controller 方法 -> 返回值处理器处理返回值 -> MessageConverter 序列化响应。
2. SpringMVC 如何将不同的 URL 路由到不同的 Controller?
启动时扫描所有 @RequestMapping 注解,将 URL 和 Handler 方法的映射关系注册到 RequestMappingHandlerMapping 中。请求进来时,根据 URL、HTTP 方法、请求头等信息在映射表中查找匹配的 HandlerMethod。
3. Filter 和 Interceptor 有什么区别?
Filter 是 Servlet 规范,作用于 DispatcherServlet 之前,拦截所有请求;Interceptor 是 Spring 框架的,作用于 DispatcherServlet 之后、Controller 之前,只拦截 Spring 管理的请求。Interceptor 可以获取 Handler 信息,更容易注入 Spring Bean。
4. 如何实现全局异常处理?
推荐使用 @RestControllerAdvice + @ExceptionHandler。为不同类型的异常定义不同的处理方法,返回统一的错误响应格式。底层由 ExceptionHandlerExceptionResolver 支持。
小结
一个 HTTP 请求在 SpringBoot 中的旅程,可以概括为四个阶段:
- 过滤:Filter 做前置处理(认证、日志、请求包装)
- 路由:DispatcherServlet + HandlerMapping 找到目标 Controller
- 执行:参数解析 -> 业务逻辑 -> 返回值序列化
- 兜底:全局异常处理确保任何情况都有合理的响应
掌握了这条链路,你就能清楚地知道在哪个环节做什么事情——Filter 适合做通用的请求/响应处理,Interceptor 适合做业务相关的拦截(如权限校验),@ControllerAdvice 负责异常兜底。各司其职,整个请求处理流程就井然有序了。
附录:请求处理核心组件速查
参数解析器一览
SpringMVC 内置的 26 种参数解析器覆盖了几乎所有场景。除了前面提到的注解型解析器,还有几个常用的:
| 参数类型 | 解析器 | 说明 |
|---|---|---|
HttpServletRequest | ServletRequestMethodArgumentResolver | 直接注入原生 Request |
HttpServletResponse | ServletResponseMethodArgumentResolver | 直接注入原生 Response |
HttpSession | ServletRequestMethodArgumentResolver | 获取当前会话 |
MultipartFile | RequestPartMethodArgumentResolver | 文件上传 |
@RequestAttribute | RequestAttributeMethodArgumentResolver | 获取 request 域属性 |
@MatrixVariable | MatrixVariableMethodArgumentResolver | 矩阵变量 |
| 自定义 POJO | ServletModelAttributeMethodProcessor | 自动绑定请求参数到对象属性 |
当方法参数是一个自定义 POJO(没有加任何注解)时,Spring 会尝试将请求参数按属性名匹配绑定到对象上。这就是为什么你写一个 User 对象作为参数,表单提交的 name、age 字段就能自动填充到对象中。
返回值处理器
SpringMVC 默认提供了 15 种返回值处理器。最常用的几个:
| 返回类型 | 处理器 | 场景 |
|---|---|---|
@ResponseBody 标注的方法 | RequestResponseBodyMethodProcessor | 返回 JSON/XML |
String(无 @ResponseBody) | ViewNameMethodReturnValueHandler | 视图名跳转 |
ModelAndView | ModelAndViewMethodReturnValueHandler | 传统 MVC 视图 |
ResponseEntity | HttpEntityMethodProcessor | 自定义状态码和响应头 |
void | — | 使用请求路径作为默认视图名 |
内容协商的匹配策略
内容协商时,客户端通过 Accept 请求头表明自己能接受的类型,服务端则根据 MessageConverter 的能力来匹配:
客户端 Accept: application/json, application/xml;q=0.9
↓
服务端遍历 MessageConverter,找到能处理目标类型的转换器
↓
匹配成功: MappingJackson2HttpMessageConverter → 输出 JSONq 值表示客户端的偏好权重(0-1),值越大优先级越高。当有多个匹配时,Spring 会按照权重和具体程度排序,选出最佳的媒体类型。
文件上传
SpringBoot 通过 MultipartAutoConfiguration 自动配置了文件上传解析器。默认限制:
- 单个文件最大 1MB
- 总上传大小最大 10MB
可以在配置文件中修改:
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 50MBController 中使用 @RequestPart 接收文件:
@PostMapping("/upload")
public Result upload(@RequestPart("avatar") MultipartFile avatar,
@RequestPart("photos") MultipartFile[] photos) {
if (!avatar.isEmpty()) {
avatar.transferTo(new File("/upload/" + avatar.getOriginalFilename()));
}
for (MultipartFile photo : photos) {
photo.transferTo(new File("/upload/" + photo.getOriginalFilename()));
}
return Result.ok();
}底层使用 StandardServletMultipartResolver 解析 multipart 请求,通过 MultipartFile.transferTo() 方法将文件写入目标位置,内部使用 FileCopyUtils 完成流拷贝。