Spring(五):Spring MVC / Spring Web 全流程
Spring(五):Spring MVC / Spring Web 全流程
导语:Spring Web 是"每天都在用、却很少有人讲清底层"的一块。本篇按一次请求的生命周期展开:DispatcherServlet 的九大组件与完整处理流程、
HandlerMapping/HandlerAdapter为什么分离、参数解析与返回值处理(ArgumentResolver+HttpMessageConverter)、Filter/Interceptor/AOP的边界与执行顺序、全局异常与 CORS,最后收口到 MVC 异步、WebFlux 与 Web 客户端(RestTemplate/WebClient) 的选型,共 15 题。
一、请求处理流程与九大组件
1. 请描述一次 HTTP 请求在 Spring MVC 中的完整处理流程。
答: 以 DispatcherServlet#doDispatch() 为主线:
① 请求到达 → Filter 链(CharacterEncodingFilter 等)
② DispatcherServlet.doDispatch()
a. getHandler(request) → 遍历 HandlerMapping 找到 HandlerExecutionChain(Handler + 拦截器链)
b. getHandlerAdapter(handler) → 找到能处理该 Handler 的 HandlerAdapter
c. mappedHandler.applyPreHandle() → 依序执行 HandlerInterceptor#preHandle(任一返回 false 则中断)
d. ha.handle(...) → HandlerAdapter 调用目标方法
· 参数解析:HandlerMethodArgumentResolver 逐个解析参数(@RequestParam/@PathVariable/@RequestBody…)
· 执行 Controller 方法(可能经过 AOP 增强)
· 返回值处理:HandlerMethodReturnValueHandler 处理(JSON / 视图 / ResponseEntity…)
e. mappedHandler.applyPostHandle() → 逆序执行 postHandle
f. processDispatchResult() → 有异常 → HandlerExceptionResolver 处理;否则渲染视图(ViewResolver)
g. mappedHandler.triggerAfterCompletion() → 逆序执行 afterCompletion(清理资源)
③ 响应写回客户端关键点:
DispatcherServlet本身不直接处理业务,它是"前端控制器(Front Controller)"——只负责编排,把"找处理器、调处理器、解析参数、渲染结果、处理异常"全部委托给九大组件。
2. DispatcherServlet 的九大组件分别是什么?
答: 来自 DispatcherServlet#initStrategies() 的初始化:
| 组件 | 职责 |
|---|---|
HandlerMapping | 根据请求找到处理器 + 拦截器链(RequestMappingHandlerMapping 解析 @RequestMapping) |
HandlerAdapter | 按处理器类型适配调用(RequestMappingHandlerAdapter 处理 @RequestMapping 方法) |
HandlerExceptionResolver | 异常解析与处理(ExceptionHandlerExceptionResolver 处理 @ExceptionHandler) |
ViewResolver | 逻辑视图名 → 具体 View(InternalResourceViewResolver、Thymeleaf 等) |
RequestToViewNameTranslator | 请求没返回视图名时,按请求路径推导视图名 |
LocaleResolver | 解析请求的 Locale(国际化) |
ThemeResolver | 解析主题(基本不用) |
MultipartResolver | 文件上传请求解析(StandardServletMultipartResolver) |
FlashMapManager | 重定向时的 flash 属性(跨重定向传参) |
加分点:能说出"Spring Boot 的
WebMvcAutoConfiguration会自动注册绝大多数组件,开发者只需自定义需要覆盖的那一个(如加WebMvcConfigurer配置拦截器)",比死背九个名字更显功底。
3. HandlerMapping 和 HandlerAdapter 为什么要分开?
答: 这是适配器模式的经典应用,目的是解耦"找处理器"与"调处理器":
HandlerMapping:只负责"这个请求该由谁处理",返回HandlerExecutionChain;Spring 中 Handler 可以是:@RequestMapping注解的方法(HandlerMethod)- 实现
Controller接口的类 HttpRequestHandler- 甚至一个 Servlet
HandlerAdapter:只负责"怎么调用这个 Handler"——supports(handler)判断能否处理,handle()完成参数解析、方法调用、返回值处理。
如果只有 HandlerMapping 会怎样? DispatcherServlet 就必须 instanceof 判断处理器类型并写一长串 if-else,新增一种处理器类型就要改框架源码——而有了 HandlerAdapter,新增处理器类型只需新增一个适配器实现,符合开闭原则。
// DispatcherServlet 中只面向两个抽象编程,完全不感知具体处理器类型
HandlerExecutionChain mappedHandler = getHandler(processedRequest);
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());4. 为什么加了 @ResponseBody 就不走视图解析了?返回值是怎么变成 JSON 的?
答: 因为返回值处理器不同:
| 场景 | 适用的 HandlerMethodReturnValueHandler | 处理方式 |
|---|---|---|
返回 String/ModelAndView(无 @ResponseBody) | ViewNameMethodReturnValueHandler / ModelAndViewMethodReturnValueHandler | 产出一个逻辑视图名,交给 ViewResolver 解析成 View 并渲染 |
@ResponseBody / @RestController | RequestResponseBodyMethodProcessor | 不走视图,用 HttpMessageConverter 把返回值序列化直接写入响应体 |
返回 ResponseEntity | HttpEntityMethodProcessor | 可完全控制状态码与响应头 |
@RestController=@Controller+@ResponseBody,因此整个类的返回值都交给HttpMessageConverter(Boot 中默认注册MappingJackson2HttpMessageConverter输出 JSON)。- 内容协商(Content Negotiation):到底输出 JSON 还是 XML,由
ContentNegotiationManager根据Accept头 / 路径后缀 / 请求参数 决定,再从已注册的HttpMessageConverter中挑出支持该媒体类型的转换器。
常见坑:明明加了
@RestController却报"找不到视图"——说明返回值被当成了视图名,通常是类上漏了@ResponseBody,或方法返回值类型没有被HttpMessageConverter支持。
二、参数解析与返回值处理
5. 常用参数绑定注解的区别?
答:
| 注解 | 来源 | 适用场景 | 说明 |
|---|---|---|---|
@RequestParam | Query / Form 表单 | 单值/多值简单参数 | required=false 可空,defaultValue 兜底 |
@PathVariable | URL 路径(RESTful) | /users/{id} | 是路径的一部分,不是查询参数 |
@RequestBody | 请求体(Body) | JSON/XML 请求体 | 用 HttpMessageConverter 反序列化为对象;一个方法只能有一个(因为请求体只能读一次) |
@RequestHeader / @CookieValue | 请求头 / Cookie | 鉴权、灰度标识 | — |
@ModelAttribute | Query + Form | 表单对象绑定 | 会做数据绑定 + 校验,并放入 Model |
@RequestPart | multipart | 文件 + JSON 混合上传 | 比 @RequestParam 更适合 MultipartFile + 对象 |
@RequestParam vs @RequestBody 的高频追问:
@RequestParam处理的是 URL 查询串 /application/x-www-form-urlencoded表单,逐参数绑定;@RequestBody处理的是 整个请求体,靠HttpMessageConverter一次读入;- 因此
@RequestBody不能与两个同时使用(HttpServletRequest#getInputStream只能读一次);若既要用 body 又要用 query,直接混用@RequestBody+@RequestParam是允许的(一个读 body、一个读 query)。
6. Spring MVC 是如何把请求参数"喂"给方法参数的?
答: 靠 HandlerMethodArgumentResolver(参数解析器) 的责任链:
ServletInvocableHandlerMethod.invokeAndHandle()
→ 为每个方法参数调用 argumentResolvers:
for (resolver : argumentResolvers) {
if (resolver.supportsParameter(parameter)) { // 谁能处理这个参数
args[i] = resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
break;
}
}
→ doInvoke(args) 反射调用目标方法常用的解析器与对应注解:
| 解析器 | 负责的参数 |
|---|---|
RequestParamMethodArgumentResolver | @RequestParam、简单类型(Spring 6 中简单类型不再自动解析,需注解或默认解析器) |
PathVariableMethodArgumentResolver | @PathVariable |
RequestResponseBodyMethodProcessor | @RequestBody(同时负责 @ResponseBody 返回值) |
ModelAttributeMethodProcessor | @ModelAttribute / 非简单类型对象 |
ServletRequestMethodArgumentResolver | HttpServletRequest、WebRequest、InputStream 等 |
PrincipalMethodArgumentResolver | java.security.Principal |
加分点:自定义参数解析器是常见扩展——比如"自动注入当前登录用户"(
@CurrentUser User user),只需实现HandlerMethodArgumentResolver并注册到RequestMappingHandlerAdapter(Boot 中通过WebMvcConfigurer#addArgumentResolvers)。
7. HttpMessageConverter 的作用是什么?什么时候用它?
答: 它是 HTTP 报文 ↔ Java 对象 的转换器,是 @RequestBody/@ResponseBody、RestTemplate/WebClient 的共同底座:
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> clazz, MediaType mediaType); // 能否反序列化
boolean canWrite(Class<?> clazz, MediaType mediaType); // 能否序列化
T read(Class<? extends T> clazz, HttpInputMessage inputMessage);
void write(T t, MediaType contentType, HttpOutputMessage outputMessage);
}| 转换器 | 支持类型 |
|---|---|
MappingJackson2HttpMessageConverter | application/json(Boot 默认) |
MappingJackson2XmlHttpMessageConverter | application/xml |
StringHttpMessageConverter | text/plain |
FormHttpMessageConverter | application/x-www-form-urlencoded |
ByteArrayHttpMessageConverter | application/octet-stream |
自定义场景:需要支持新格式(如自定义 Protobuf)或定制 JSON 规则(如 Long 转 String 防前端精度丢失)时,实现/配置 HttpMessageConverter 即可:
@Bean
public Jackson2ObjectMapperBuilderCustomizer longToString() {
return builder -> builder.serializerByType(Long.class, ToStringSerializer.instance);
}8. 类型转换(Converter / Formatter / ConversionService)在什么时候生效?
答: 参数解析的前半步就是"类型转换":
@RequestParam Integer age中,"18"(String)→Integer由ConversionService完成;- 请求是字符串来源(Query/Form),需要转成方法参数类型 → 走
Converter<S,T>/Formatter<T>(如@DateTimeFormat(pattern="yyyy-MM-dd")); - 请求体(JSON)则不走 ConversionService,而由
HttpMessageConverter+ Jackson 反序列化。
自定义示例:
@Component
public class StringToEnumConverter implements Converter<String, OrderStatus> {
public OrderStatus convert(String source) { return OrderStatus.valueOf(source.toUpperCase()); }
}
// Boot 中注册到 WebMvcConfigurer#addFormatters 或直接声明为 Bean(Boot 会自动收集)9. Spring MVC 的参数校验是怎么做的?
答:
@PostMapping("/users")
public Result create(@Valid @RequestBody UserForm form, BindingResult bindingResult) {
if (bindingResult.hasErrors()) { ... } // 方式一:手动接收错误
// ...
}
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class) // 方式二:统一处理(推荐)
public Result onInvalid(MethodArgumentNotValidException e) { ... }
}要点:
- 校验注解来自 Jakarta Bean Validation(
@NotNull、@Size、@Pattern、@Email…),实现通常是 Hibernate Validator;Boot 引入spring-boot-starter-validation后自动装配。 @Validated是 Spring 的变体,支持分组校验与方法级校验;@Valid是标准注解。- 校验失败抛的异常:
@RequestBody校验失败 →MethodArgumentNotValidException;@RequestParam/@PathVariable约束失败 →ConstraintViolationException(Spring 6.1 起在 Controller 方法参数上直接使用约束注解会抛HandlerMethodValidationException)。 - 统一处理:通过
@RestControllerAdvice+@ExceptionHandler收敛为业务错误码,避免每个方法写if (bindingResult.hasErrors())。
三、过滤器、拦截器与异常
10. Filter、HandlerInterceptor、AOP 三者有什么区别?
答: 三者都是"在请求链路上做增强",但所属层次与能力完全不同:
| 维度 | Filter | HandlerInterceptor | AOP |
|---|---|---|---|
| 规范 | Servlet 规范(容器级) | Spring MVC 提供 | Spring AOP(代理) |
| 作用范围 | 所有请求(含静态资源、非 MVC 请求) | 进入 Spring MVC 的请求(Handler 级) | Spring Bean 的方法调用(任意层) |
| 能否拿到处理器信息 | 不能(只知道 URL) | 能(HandlerMethod,可拿到方法/注解) | 能(能拿到方法签名) |
| 能否改 request/response | 能(可包装/替换) | 只能读取(不能替换请求体) | 不能 |
| 注册方式 | FilterRegistrationBean / @WebFilter / OncePerRequestFilter | WebMvcConfigurer#addInterceptors | @Aspect |
| 典型用途 | 编码、CORS、请求日志、链路追踪、XSS 过滤 | 登录鉴权、权限校验、接口耗时统计、灰度 | 事务、缓存、业务埋点 |
执行顺序(一次请求):
Filter#doFilter 前置
→ Interceptor#preHandle
→ Controller(可能被 AOP 切面包裹)
→ Interceptor#postHandle
→ 视图渲染
→ Interceptor#afterCompletion
Filter#doFilter 后置选择原则:能拿到 URL 就够、要改请求/响应 → Filter;要基于 Handler/注解做权限与业务前后置 → Interceptor;要基于方法/Bean 做通用增强 → AOP。
11. HandlerInterceptor 的三个方法执行顺序如何?异常时谁会被调用?
答:
| 方法 | 时机 | 返回 false 的效果 |
|---|---|---|
preHandle | Handler 执行之前,正序执行 | 中断后续所有流程(后续 preHandle、Controller 都不执行;已返回 true 的 afterCompletion 仍会执行) |
postHandle | Handler 执行之后、视图渲染之前,逆序执行 | — |
afterCompletion | 视图渲染之后,逆序执行 | 用于清理资源 |
异常场景(高频):
- Controller 抛异常 →
postHandle不会执行; - 异常被
HandlerExceptionResolver处理后,afterCompletion仍会执行(前提是它对应的preHandle已经返回 true); - 因此清理逻辑(如 ThreadLocal 的 remove)必须放在
afterCompletion,而不是postHandle。
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
if (!auth(req)) { res.setStatus(401); return false; } // 中断
UserContext.set(currentUser(req));
return true;
}
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object handler, Exception ex) {
UserContext.remove(); // 必须放这里清理
}12. Spring MVC 如何做全局异常处理?
答: 靠 HandlerExceptionResolver 体系,日常使用分三层:
@RestControllerAdvice // = @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class) // 业务异常 → 200 + 业务码
public Result<Void> onBusiness(BusinessException e) { ... }
@ExceptionHandler(MethodArgumentNotValidException.class) // 参数校验异常
public Result<Void> onInvalid(MethodArgumentNotValidException e) { ... }
@ExceptionHandler(Exception.class) // 兜底
public Result<Void> onUnknown(Exception e) { log.error("unknown", e); ... }
}要点与追问:
- 处理顺序:方法级
@ExceptionHandler→ 类级 →@ControllerAdvice(按@Order/ 精确匹配优先);Exception兜底要放最后。 ResponseEntityExceptionHandler:@ControllerAdvice的基类,用于统一改写 Spring MVC 内置异常(MethodArgumentNotValidException、HttpMessageNotReadableException、NoHandlerFoundException等)的响应。@Order控制多个@ControllerAdvice的优先级(值越小优先级越高)。@ExceptionHandler只能处理"进入 Handler 之后"的异常——Filter 中抛出的异常、以及 404 未匹配到任何 Handler 的情况需要另外配置(ErrorController/ResponseEntityExceptionHandler+throwExceptionIfNoHandlerFound)。ErrorControllervs@ControllerAdvice:前者处理"到达 Servlet 容器/Spring Boot 错误页"的请求(如 404、500 容器级错误),后者处理"业务 Handler 内抛出的异常"。
13. Spring MVC 如何处理跨域(CORS)?原理是什么?
答: 跨域源于浏览器的同源策略(协议 + 域名 + 端口任一不同即跨域),服务端需要返回正确的 CORS 响应头(Access-Control-Allow-Origin 等)。
三种配置方式:
| 方式 | 写法 | 适用 |
|---|---|---|
| 注解 | @CrossOrigin(origins = "https://a.com")(类/方法级) | 少量接口 |
| 全局配置 | WebMvcConfigurer#addCorsMappings → 配置 CorsRegistry | 应用内统一规则 |
| Filter(推荐) | CorsFilter / OncePerRequestFilter 设置 CORS 头 | 需要拦截前置(OPTIONS 预检)+ 全局生效,优先级高于 MVC 层 |
原理与坑:
- 简单请求(GET/POST + 简单头)直接带
Origin发出,服务端返回Access-Control-Allow-Origin即可; - 非简单请求(自定义头、
PUT/DELETE、application/json)会先发OPTIONS预检请求(Preflight),服务端必须正确响应Access-Control-Allow-Methods/-Headers/-Max-Age,否则真实请求不会发出; allowCredentials = true时Allow-Origin不能是*(必须回显具体 Origin)——这是最常见的报错;- 生产实践:CORS 统一在网关层处理(Spring Cloud Gateway 的
CorsWebFilter),避免每个服务各自配置、规则不一致;同时别用*放开所有来源 + 允许携带凭证(等于放弃防护)。
四、异步与 Web 客户端
14. Spring MVC 的异步处理(Callable / DeferredResult)与 WebFlux 有什么区别?
答:
| 维度 | Spring MVC 异步 | Spring WebFlux |
|---|---|---|
| 底层 | Servlet 3.0 异步支持(AsyncContext) | Reactor + Netty(非阻塞 IO),基于 Reactive Streams |
| 线程模型 | 一个请求仍对应一个 Servlet 线程,只是"业务处理"交给 TaskExecutor,线程被释放回容器处理其他请求 | 少量事件循环线程处理大量连接(不占请求线程) |
| 返回方式 | Callable<T>(简单,交给 TaskExecutor)、DeferredResult<T>(跨线程/外部事件回调 setResult)、WebAsyncTask(带超时/回调)、SseEmitter(服务端推送) | Mono<T> / Flux<T> |
| 超时 | WebAsyncTask 可设超时,超时后走 AsyncRequestTimeoutException | timeout() 操作符 |
| 适用 | 已有 Spring MVC 应用,只有少数慢接口需要异步化 | 高并发 IO 密集、长连接、流式场景(网关、推送) |
| 生态 | Servlet 栈(阻塞 JDBC 等可用) | 需要全链路非阻塞(R2DBC、Reactive Redis),阻塞调用会拖垮事件循环(必须隔离到弹性调度器) |
Callable vs DeferredResult 的关键区别:
Callable:由 Spring MVC 自己把任务提交给TaskExecutor执行,结果由框架回填;DeferredResult:由业务代码自行决定何时、在哪个线程setResult()——适合"请求发起后等待外部事件(MQ、第三方回调)"的场景。
最重要的一句结论:MVC 的"异步"只解决"容器线程被占用",不解决"IO 阻塞";如果底层还是阻塞 JDBC,线程池照样会被打满——真正的非阻塞方案是 WebFlux + 全链路响应式。
15. RestTemplate 和 WebClient 有什么区别?Spring Web 的客户端怎么选?
答:
RestTemplate | WebClient | (Spring 6.1)RestClient | |
|---|---|---|---|
| 模型 | 同步阻塞 | 异步非阻塞(Reactor) | 同步阻塞(RestTemplate 的现代替代) |
| 底层 | ClientHttpRequestFactory(默认 JDK HttpURLConnection,生产建议换 Apache HttpClient / OkHttp 连接池) | Reactor Netty(也可切其他) | 复用 ClientHttpRequestFactory |
| 状态 | 维护模式(Maintenance) | 推荐(响应式栈) | Spring 6.1 新增,推荐的同步客户端 |
| 适用 | 老项目、简单同步调用 | 高并发、流式、响应式应用 | 新项目的同步调用 |
要点:
RestTemplate默认工厂不支持连接池、且没有默认超时——生产必须显式配置连接池 + 连接/读超时(否则一个慢下游就会拖死调用方,见《高可用(一)》超时篇)。WebClient支持超时、重试、背压、流式,但要求调用方处于响应式上下文(或.block(),此时阻塞就没意义了)。- Spring 6.1 起的 HTTP Interface(
@HttpExchange+HttpServiceProxyFactory) 让同步客户端也能"声明式"定义——等价于RestTemplate阵营的 OpenFeign(见《SpringCloud(二)》)。
一句话:新项目同步调用用
RestClient,响应式/高并发用WebClient,RestTemplate只在存量代码里维护;无论用哪个,连接池 + 超时 + 错误处理三件套都不能省。
本章小结:Spring Web 的全部考点都可以挂到"一次请求的生命周期"这棵树上——Filter → DispatcherServlet(九组件编排)→ Interceptor → Handler(参数解析 + 业务 + 返回值处理)→ 异常解析 → 视图/消息转换 → Interceptor 收尾 → Filter 收尾。把这条链路记住,MVC 相关问题几乎都能顺势回答。
