问题引入
2023 年夏,某团队的新人在调试接口时遇到了一个诡异的现象:GET /orders/123 能正常返回订单详情,但 POST /orders 却报了 405 Method Not Allowed。他检查了 Controller 代码,确认路径没错,方法上也有 @PostMapping 注解。经验丰富的同事看了一眼就发现了问题——另一个 Controller 里有一个 @RequestMapping("/orders") 的方法,Spring MVC 优先匹配了那个更宽泛的映射,导致 POST 请求被错误路由。
这个案例暴露了很多人对 Spring MVC 的认知停留在"DispatcherServlet → Controller → 返回"的表层。面试追问下去:
- 一个 HTTP 请求到达 Tomcat 后,到 Controller 方法执行前,经历了哪些组件?
- HandlerMapping 是怎么根据 URL 找到 Controller 方法的?如果多个方法都匹配怎么办?
- 拦截器(Interceptor)和过滤器(Filter)有什么区别?执行顺序是什么?
- @RestController 返回的 JSON 是怎么序列化的?为什么返回 String 时有时会变成视图名?
- @ControllerAdvice 统一异常处理的底层是怎么实现的?
本文从 HTTP 请求进入 Servlet 容器开始,穿透 Spring MVC 处理请求的完整链路。
核心概念
1. Spring MVC 的核心组件
| 组件 | 职责 | 典型实现 |
|---|---|---|
| DispatcherServlet | 前端控制器,所有请求的入口和分发中枢 | DispatcherServlet |
| HandlerMapping | 将请求 URL 映射到处理器(Controller 方法) | RequestMappingHandlerMapping |
| HandlerAdapter | 适配不同类型的处理器,统一调用方式 | RequestMappingHandlerAdapter |
| HandlerInterceptor | 拦截器,在请求处理前后插入逻辑 | HandlerInterceptor 接口 |
| HandlerExceptionResolver | 异常解析器,统一处理 Controller 抛出的异常 | ExceptionHandlerExceptionResolver |
| ViewResolver | 视图解析器,将逻辑视图名解析为实际视图 | InternalResourceViewResolver、ThymeleafViewResolver |
| LocaleResolver | 区域解析器,国际化支持 | AcceptHeaderLocaleResolver |
| MultipartResolver | 文件上传解析器 | StandardServletMultipartResolver |
2. DispatcherServlet:前端控制器
DispatcherServlet 是 Spring MVC 的核心入口,它本身是一个 Servlet,继承自 HttpServlet,但在设计模式上采用了 **Front Controller(前端控制器)**模式——所有请求都先到达它,再由它分发给后续组件。
HTTP 请求
│
▼
┌─────────────────┐
│ Tomcat │
│ (Servlet容器) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Filter Chain │ ← Filter1 → Filter2 → Filter3
│ (Servlet规范) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ DispatcherServlet│ ← Spring MVC 的入口
│ (Front Controller)│
└────────┬────────┘
│
▼
Spring MVC 内部处理流程
读图导引:请求先经过 Tomcat 的 Filter Chain(Servlet 层面),然后进入 DispatcherServlet(Spring MVC 层面)。Filter 和 Spring MVC 的 Interceptor 处于不同的层次。
DispatcherServlet 的初始化时会加载 Spring 容器(WebApplicationContext),并初始化上述所有组件。
3. @Controller vs @RestController
java
@Controller
public class OrderController {
@GetMapping("/order/{id}")
@ResponseBody // 必须显式标注,否则返回视图名
public Order getOrder(@PathVariable Long id) {
return orderService.getById(id);
}
@GetMapping("/order/page")
public String orderPage() {
return "order"; // 解析为视图名 order.html
}
}
@RestController // = @Controller + @ResponseBody
public class OrderApiController {
@GetMapping("/api/order/{id}")
public Order getOrder(@PathVariable Long id) {
return orderService.getById(id); // 自动序列化为 JSON
}
}
@RestController 的陷阱:如果方法返回类型是 String,且类上标注了 @RestController,Spring 会将这个 String 当作普通文本返回(Content-Type: text/plain),而不是 JSON。如果需要返回 JSON 格式的字符串,必须包装成对象或使用 ResponseEntity。
4. 过滤器(Filter)vs 拦截器(Interceptor)
| 维度 | Filter | Interceptor |
|---|---|---|
| 规范来源 | Servlet 规范(javax/jakarta.servlet) | Spring MVC 机制 |
| 执行时机 | 请求进入 Servlet 之前 / 响应离开 Servlet 之后 | DispatcherServlet 内部 |
| 可以操作 | Request/Response 对象(可修改请求体/响应体) | Handler、ModelAndView、Exception |
| 依赖 Spring | 否(Tomcat 层面) | 是(Spring 容器管理) |
| 配置方式 | web.xml 或 @WebFilter | 实现 HandlerInterceptor + 注册 |
| 使用场景 | 编码设置、登录校验、日志记录 | 权限校验、性能监控、事务控制 |
请求流向与执行顺序
客户端
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Filter Chain │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Filter1 │───▶│ Filter2 │───▶│ Filter3 │ │
│ │ doFilter│ │ doFilter│ │ doFilter│ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ │ chain.doFilter() │ │
│ │ 进入 DispatcherServlet │ │
└───────┼──────────────┼──────────────┼────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ DispatcherServlet │
│ │ │
│ ▼ │
│ HandlerInterceptor.preHandle() │
│ │ │
│ ▼ │
│ Controller 方法执行 │
│ │ │
│ ▼ │
│ HandlerInterceptor.postHandle() │
│ │ │
│ ▼ │
│ 视图渲染 / JSON 序列化 │
│ │ │
│ ▼ │
│ HandlerInterceptor.afterCompletion() │
└─────────────────────────────────────────────────────────────┘
│
▼
Filter 的 doFilter 后续代码(响应处理)
│
▼
客户端
读图导引:Filter 在 DispatcherServlet 外,Interceptor 在 DispatcherServlet 内。请求先经过所有 Filter 的 chain.doFilter() 之前逻辑,进入 DispatcherServlet,经过 Interceptor 链,执行 Controller,返回时再经过 Interceptor 和 Filter 的后续逻辑。
原理分析
1. 请求处理的完整九步流程
① 接收请求 ② 查找 Handler
│ │
▼ ▼
┌─────────────┐ ┌─────────────────┐
│ Dispatcher │ │ HandlerMapping │
│ Servlet.do │────────────▶│ RequestMapping │
│ Dispatch() │ │ HandlerMapping │
└──────┬──────┘ └────────┬────────┘
│ │
│ ▼
│ ③ 返回 HandlerExecutionChain
│ │
│ ┌─────────┴─────────┐
│ │ Handler + │
│ │ Interceptor List │
│ └─────────┬─────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────────┐
│ ④ 获取 │ │ ⑤ 执行 │
│ Handler │────────────▶│ Interceptor │
│ Adapter │ │ preHandle() │
└──────┬──────┘ └────────┬────────┘
│ │
│ ▼
│ ⑥ 调用 Handler
│ │
│ ┌─────────┴─────────┐
│ │ Controller 方法 │
│ │ 执行业务逻辑 │
│ └─────────┬─────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────────┐
│ ⑦ 处理 │ │ ⑧ 执行 │
│ 返回结果 │◀────────────│ Interceptor │
│ │ │ postHandle() │
└──────┬──────┘ └─────────────────┘
│
│ 如果是 ModelAndView
▼
┌─────────────┐
│ ⑨ ViewResolver│
│ 解析视图 │
│ 渲染输出 │
└─────────────┘
│
▼
HTTP 响应
读图导引:九步流程从请求接收到响应输出。关键点:② HandlerMapping 返回的不是单纯的 Handler,而是 HandlerExecutionChain(包含 Handler + Interceptor 链);⑥ HandlerAdapter 负责统一调用不同类型的 Handler;⑦ 如果是 @ResponseBody,跳过 ViewResolver 直接序列化。
源码级别的流程(DispatcherServlet.doDispatch)
java
protected void doDispatch(HttpServletRequest request,
HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
// 1. 检查是否是文件上传请求
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 2. 通过 HandlerMapping 获取 HandlerExecutionChain
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 3. 获取 HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 4. 执行 Interceptor 的 preHandle
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return; // preHandle 返回 false,中断处理
}
// 5. 调用 Handler 执行业务逻辑
mv = ha.handle(processedRequest, response,
mappedHandler.getHandler());
// 6. 应用默认视图名
applyDefaultViewName(processedRequest, mv);
// 7. 执行 Interceptor 的 postHandle
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
// 8. 处理分发结果(视图渲染或异常处理)
processDispatchResult(processedRequest, response,
mappedHandler, mv, dispatchException);
}
finally {
// 9. 清理资源
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
2. HandlerMapping:URL 到 Controller 方法的映射
Spring MVC 默认注册了多个 HandlerMapping:
| HandlerMapping | 优先级 | 用途 |
|---|---|---|
| RequestMappingHandlerMapping | 0 | 处理 @RequestMapping 注解的 Controller 方法 |
| BeanNameUrlHandlerMapping | 1 | 将 Bean 名称作为 URL(如 /foo → id="/foo" 的 Bean) |
| RouterFunctionMapping | 2 | Spring 5.2+ 的函数式路由 |
| SimpleUrlHandlerMapping | 3 | 静态资源映射 |
RequestMappingHandlerMapping 的初始化:
容器启动时,RequestMappingHandlerMapping.afterPropertiesSet() 会扫描所有 @Controller Bean,解析类和方法上的 @RequestMapping 注解,建立 URL → HandlerMethod 的映射表:
java
// 简化的注册逻辑
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = handler.getClass();
// 获取所有方法
Map<Method, RequestMappingInfo> methods = MethodIntrospector
.selectMethods(handlerType, method -> getMappingForMethod(method, handlerType));
// 注册到映射表
methods.forEach((method, mapping) -> {
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
registerHandlerMethod(handler, method, mapping);
});
}
URL 匹配规则:
当请求到达时,DispatcherServlet 遍历所有 HandlerMapping,调用 getHandler(request),返回第一个匹配的 HandlerExecutionChain。
java
protected HandlerExecutionChain getHandler(HttpServletRequest request) {
for (HandlerMapping hm : this.handlerMappings) {
HandlerExecutionChain handler = hm.getHandler(request);
if (handler != null) {
return handler; // 返回第一个匹配的
}
}
return null;
}
匹配冲突解决:
如果同一个 URL 被多个方法映射,Spring MVC 会按精确度排序:
/orders/{id}vs/orders/123→ 后者更精确(没有路径变量)/orders/*vs/orders/{id}→ 路径变量比通配符更精确- HTTP 方法匹配(GET vs POST)也是判断条件
3. HandlerAdapter:统一调用不同类型的 Handler
Spring MVC 支持多种类型的 Handler:
@Controller注解的方法(最常用)- 实现
HttpRequestHandler接口的 Bean - 实现
Controller接口的 Bean - 实现
Servlet接口的 Bean
HandlerAdapter 的作用是将不同类型的 Handler 统一包装,让 DispatcherServlet 可以用相同的方式调用。
不同类型的 Handler HandlerAdapter 适配
┌─────────────────┐ ┌─────────────────────────┐
│ @Controller方法 │ │ RequestMappingHandler │
│ (HandlerMethod) │ ──────────────▶│ Adapter │
└─────────────────┘ │ handle() → invoke │
│ HandlerMethod │
┌─────────────────┐ ├─────────────────────────┤
│ HttpRequestHandler│ │ HttpRequestHandlerAdapter│
│ (handleRequest) │ ──────────────▶│ handle() → 直接调用 │
└─────────────────┘ ├─────────────────────────┤
│ SimpleControllerHandler │
┌─────────────────┐ │ Adapter │
│ Controller接口 │ ──────────────▶│ handle() → handleRequest│
│ (handleRequest) │ └─────────────────────────┘
└─────────────────┘
读图导引:HandlerAdapter 是适配器模式的应用——DispatcherServlet 不需要关心 Handler 的具体类型,只需要调用 HandlerAdapter.handle(),由 Adapter 负责实际调用。
RequestMappingHandlerAdapter 的核心:
java
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
// 1. 构建 WebDataBinderFactory(参数绑定)
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
// 2. 构建 ModelFactory(Model 管理)
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
// 3. 包装为 ServletInvocableHandlerMethod
ServletInvocableHandlerMethod invocableMethod =
createInvocableHandlerMethod(handlerMethod);
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
// 4. 创建 ModelAndViewContainer
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(request, mavContainer, invocableMethod);
// 5. 执行方法
invocableMethod.invokeAndHandle(request, response, mavContainer);
// 6. 返回 ModelAndView(或 null,如果是 @ResponseBody)
return getModelAndView(mavContainer, modelFactory, request);
}
4. 参数解析与返回值处理
参数解析器(HandlerMethodArgumentResolver)
Spring MVC 内置了 30+ 个参数解析器:
| 解析器 | 处理的注解/类型 | 示例 |
|---|---|---|
| RequestParamMethodArgumentResolver | @RequestParam | (@RequestParam String name) |
| PathVariableMethodArgumentResolver | @PathVariable | (@PathVariable Long id) |
| RequestResponseBodyMethodProcessor | @RequestBody | (@RequestBody Order order) |
| RequestHeaderMethodArgumentResolver | @RequestHeader | (@RequestHeader("X-Token") String token) |
| ServletRequestMethodArgumentResolver | HttpServletRequest | (HttpServletRequest request) |
| ModelMethodProcessor | Model | (Model model) |
@RequestBody 的解析过程:
java
// RequestResponseBodyMethodProcessor
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
// 1. 读取请求体 InputStream
InputStream inputStream = webRequest.getNativeRequest(HttpServletRequest.class)
.getInputStream();
// 2. 使用 HttpMessageConverter 反序列化
// 默认使用 MappingJackson2HttpMessageConverter(JSON)
return readWithMessageConverters(webRequest, parameter, parameter.getParameterType());
}
返回值处理器(HandlerMethodReturnValueHandler)
| 处理器 | 处理的类型 | 示例 |
|---|---|---|
| RequestResponseBodyMethodProcessor | @ResponseBody | @ResponseBody Order → JSON |
| ViewNameMethodReturnValueHandler | String(无@ResponseBody) | return "order" → 视图名 |
| ModelAndViewMethodReturnValueHandler | ModelAndView | return new ModelAndView("order") |
| ResponseEntityResultHandler | ResponseEntity | return ResponseEntity.ok(order) |
@ResponseBody 的序列化过程:
java
// RequestResponseBodyMethodProcessor
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
// 1. 标记请求已处理(不走 ViewResolver)
mavContainer.setRequestHandled(true);
// 2. 使用 HttpMessageConverter 序列化
// 根据 Accept 头选择合适的 Converter
// 默认 JSON:MappingJackson2HttpMessageConverter
writeWithMessageConverters(returnValue, returnType, webRequest);
}
5. 统一异常处理机制
Spring MVC 的异常处理由 HandlerExceptionResolver 链负责:
Controller 抛出异常
│
▼
┌─────────────────────────────────────────┐
│ DispatcherServlet.processHandlerException│
│ │ │
│ ▼ │
│ 遍历 HandlerExceptionResolver 链 │
│ │ │
│ ├── ExceptionHandlerExceptionResolver ← @ExceptionHandler
│ │ (解析 @ControllerAdvice) │
│ ├── ResponseStatusExceptionResolver ← @ResponseStatus
│ ├── DefaultHandlerExceptionResolver ← Spring 默认异常
│ └── 自定义 Resolver │
│ │ │
│ ▼ │
│ 返回 ModelAndView 或写入响应 │
└─────────────────────────────────────────┘
读图导引:异常处理也是通过链式 resolver 实现的。ExceptionHandlerExceptionResolver 会扫描所有 @ControllerAdvice 类,找到匹配异常类型的 @ExceptionHandler 方法执行。
@ControllerAdvice 的扫描时机:
容器启动时,ExceptionHandlerExceptionResolver.afterPropertiesSet() 会扫描所有 @ControllerAdvice Bean,缓存其中的 @ExceptionHandler 方法:
java
// ExceptionHandlerMethodResolver 的初始化
public ExceptionHandlerMethodResolver(Class<?> handlerType) {
// 遍历类中所有方法
for (Method method : MethodIntrospector.selectMethods(handlerType,
EXCEPTION_HANDLER_METHODS)) {
// 解析方法上 @ExceptionHandler 注解的异常类型
for (Class<? extends Throwable> exceptionType :
detectExceptionMappings(method)) {
// 注册:异常类型 → 处理方法
addExceptionMapping(exceptionType, method);
}
}
}
实战/源码
1. 自定义拦截器实现登录校验
java
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token == null || !jwtUtil.validate(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}");
return false; // 中断请求
}
// 将用户信息存入 ThreadLocal
UserContext.set(jwtUtil.parseUser(token));
return true;
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler, ModelAndView modelAndView) {
// 可以修改 ModelAndView
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
UserContext.clear(); // 清理 ThreadLocal,防止内存泄漏
}
}
// 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/**") // 拦截所有路径
.excludePathPatterns("/login", // 排除登录接口
"/health"); // 排除健康检查
}
}
2. @ControllerAdvice 统一异常处理
java
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// 处理业务异常
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
return Result.fail(e.getCode(), e.getMessage());
}
// 处理参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidationException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining(", "));
return Result.fail(400, message);
}
// 处理参数绑定异常(如 @RequestBody JSON 解析失败)
@ExceptionHandler(HttpMessageNotReadableException.class)
public Result<Void> handleHttpMessageNotReadable(HttpMessageNotReadableException e) {
log.warn("请求体解析失败: {}", e.getMessage());
return Result.fail(400, "请求格式错误");
}
// 兜底异常处理
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常", e);
return Result.fail(500, "系统繁忙,请稍后重试");
}
}
// 统一响应包装
@Data
public class Result<T> {
private int code;
private String msg;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg("success");
result.setData(data);
return result;
}
public static <T> Result<T> fail(int code, String msg) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMsg(msg);
return result;
}
}
3. RESTful API 设计示例
java
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
@Autowired
private OrderService orderService;
// 查询列表(支持分页、排序、过滤)
@GetMapping
public Result<PageResult<Order>> listOrders(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String status,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate) {
PageResult<Order> result = orderService.list(page, size, status, startDate);
return Result.success(result);
}
// 查询单个资源
@GetMapping("/{id}")
public Result<Order> getOrder(@PathVariable Long id) {
Order order = orderService.getById(id);
if (order == null) {
throw new BusinessException(404, "订单不存在");
}
return Result.success(order);
}
// 创建资源
@PostMapping
public Result<Order> createOrder(@Valid @RequestBody OrderCreateRequest request) {
Order order = orderService.create(request);
return Result.success(order);
}
// 全量更新
@PutMapping("/{id}")
public Result<Order> updateOrder(@PathVariable Long id,
@Valid @RequestBody OrderUpdateRequest request) {
Order order = orderService.update(id, request);
return Result.success(order);
}
// 部分更新
@PatchMapping("/{id}/status")
public Result<Void> updateStatus(@PathVariable Long id,
@RequestParam String status) {
orderService.updateStatus(id, status);
return Result.success(null);
}
// 删除资源
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteOrder(@PathVariable Long id) {
orderService.delete(id);
}
}
4. 文件上传与下载
java
@RestController
@RequestMapping("/api/files")
public class FileController {
// 单文件上传
@PostMapping("/upload")
public Result<String> upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
throw new BusinessException(400, "文件为空");
}
String originalFilename = file.getOriginalFilename();
String extension = FilenameUtils.getExtension(originalFilename);
// 校验文件类型
if (!Arrays.asList("jpg", "png", "pdf").contains(extension.toLowerCase())) {
throw new BusinessException(400, "不支持的文件类型");
}
// 校验文件大小(Spring 已自动校验 multipart.max-file-size)
String newFileName = UUID.randomUUID() + "." + extension;
Path targetPath = Paths.get(uploadDir, newFileName);
try {
Files.copy(file.getInputStream(), targetPath);
} catch (IOException e) {
throw new BusinessException(500, "文件上传失败");
}
return Result.success("/files/" + newFileName);
}
// 文件下载
@GetMapping("/download/{filename}")
public ResponseEntity<Resource> download(@PathVariable String filename) {
Path filePath = Paths.get(uploadDir, filename);
Resource resource = new FileSystemResource(filePath);
if (!resource.exists()) {
throw new BusinessException(404, "文件不存在");
}
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + filename + "\"")
.body(resource);
}
}
5. 跨域配置(CORS)
java
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000", "https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH")
.allowedHeaders("*")
.exposedHeaders("X-Request-Id")
.allowCredentials(true)
.maxAge(3600);
}
}
常见问题
Q1:@Controller 方法返回 String 时,什么情况下是视图名、什么情况下是 JSON?
答:取决于类上是否有 @ResponseBody(或 @RestController):
| 类注解 | 方法返回 String | 结果 |
|---|---|---|
| @Controller | return "order" |
视图名,解析为 order.html |
| @Controller + @ResponseBody | return "order" |
纯文本 "order"(text/plain) |
| @RestController | return "order" |
纯文本 "order"(text/plain) |
常见坑:在 @RestController 中想返回 JSON 格式的字符串,结果变成了 text/plain。
java
@RestController
public class TestController {
@GetMapping("/test")
public String test() {
return "{\"name\":\"test\"}"; // 错误!返回的是纯文本,不是 JSON
}
// 正确做法1:返回对象,由 Jackson 自动序列化
@GetMapping("/test2")
public Map<String, String> test2() {
return Map.of("name", "test"); // Content-Type: application/json
}
// 正确做法2:使用 ResponseEntity 显式指定
@GetMapping("/test3")
public ResponseEntity<String> test3() {
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body("{\"name\":\"test\"}");
}
}
Q2:多个方法匹配同一个 URL 怎么办?
答:Spring MVC 按以下优先级选择:
- 路径精确度:
/orders/123>/orders/{id}>/orders/* - HTTP 方法匹配:
@GetMapping("/orders")优先于@RequestMapping("/orders") - 参数条件:
@GetMapping(value="/orders", params="format=json")更精确 - ** consumes/produces**:
@GetMapping(value="/orders", produces="application/json")会检查 Accept 头
如果优先级相同,启动时会报错:
IllegalStateException: Ambiguous mapping. Cannot map 'orderController' method
Q3:Filter 和 Interceptor 的执行顺序是什么?
答:
请求阶段:
Filter1.doFilter() 前半部分
Filter2.doFilter() 前半部分
Filter3.doFilter() 前半部分
DispatcherServlet
Interceptor1.preHandle()
Interceptor2.preHandle()
Controller 方法执行
Interceptor2.postHandle()
Interceptor1.postHandle()
(视图渲染 / JSON 序列化)
Interceptor2.afterCompletion()
Interceptor1.afterCompletion()
Filter3.doFilter() 后半部分
Filter2.doFilter() 后半部分
Filter1.doFilter() 后半部分
关键区别:
- Filter 可以修改请求体和响应体(如编码转换、压缩)
- Interceptor 可以访问 Controller 返回的 ModelAndView
- Filter 在异常发生前/后都能执行,Interceptor 的
afterCompletion在异常后也能执行(用于清理资源)
Q4:@RequestBody 的 JSON 反序列化失败怎么处理?
答:Spring MVC 默认使用 Jackson 进行 JSON 反序列化。常见失败场景:
- 字段类型不匹配:JSON 中的
"age": "abc"无法转为Integer - 缺少无参构造器:Jackson 需要无参构造器(或
@JsonCreator) - 日期格式错误:默认要求 ISO-8601 格式(
2024-01-01T12:00:00)
解决方案:
java
// 1. 自定义 Jackson 配置
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
// 忽略未知字段
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 允许空字符串转 null
mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
// 自定义日期格式
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
// 2. DTO 中使用 @JsonFormat
public class OrderRequest {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
}
// 3. 全局异常处理
@ExceptionHandler(HttpMessageNotReadableException.class)
public Result<Void> handleHttpMessageNotReadable(HttpMessageNotReadableException e) {
String message = "请求体格式错误";
if (e.getCause() instanceof InvalidFormatException) {
InvalidFormatException ife = (InvalidFormatException) e.getCause();
message = String.format("字段 '%s' 格式错误,期望类型: %s",
ife.getPath().get(0).getFieldName(),
ife.getTargetType().getSimpleName());
}
return Result.fail(400, message);
}
Q5:SpringBoot 中 DispatcherServlet 的映射路径是怎么配置的?
答:SpringBoot 自动配置 DispatcherServlet,默认映射路径为 /:
java
// DispatcherServletAutoConfiguration
@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet) {
DispatcherServletRegistrationBean registration =
new DispatcherServletRegistrationBean(dispatcherServlet, this.webMvcProperties.getServlet().getPath());
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
registration.setLoadOnStartup(this.webMvcProperties.getServlet().getLoadOnStartup());
return registration;
}
可以通过配置修改映射路径:
yaml
server:
servlet:
context-path: /api # 应用根路径
spring:
mvc:
servlet:
path: /mvc # DispatcherServlet 映射路径(默认 /)
这样 DispatcherServlet 只会处理 /api/mvc/* 的请求。
如果需要多个 DispatcherServlet(如前后端分离 + 管理后台):
java
@Configuration
public class ServletConfig {
@Bean
public ServletRegistrationBean<DispatcherServlet> apiServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setContextConfigLocation("classpath:api-servlet.xml");
return new ServletRegistrationBean<>(servlet, "/api/*");
}
@Bean
public ServletRegistrationBean<DispatcherServlet> adminServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setContextConfigLocation("classpath:admin-servlet.xml");
return new ServletRegistrationBean<>(servlet, "/admin/*");
}
}
总结
Spring MVC 不是"几个注解就能搞定"的简单框架,从 HTTP 请求到 Controller 方法,中间经历了精密的组件协作:
- DispatcherServlet 是 Front Controller 模式的实现:所有请求的统一入口,负责分发、协调、异常处理
- HandlerMapping 负责 URL → 方法的映射:启动时扫描 @RequestMapping 建立映射表,请求到达时按精确度匹配
- HandlerAdapter 是适配器模式的应用:将不同类型的 Handler(注解方法、HttpRequestHandler、Servlet)统一包装,让 DispatcherServlet 无需关心具体类型
- 拦截器与过滤器处于不同层次:Filter 是 Servlet 规范(Tomcat 层面),可以修改请求/响应体;Interceptor 是 Spring MVC 机制,可以访问 Controller 和 ModelAndView
- 统一异常处理通过 HandlerExceptionResolver 链实现:@ControllerAdvice + @ExceptionHandler 是最优雅的方式,但异常处理也有优先级和匹配规则
- @RestController 的 JSON 序列化由 HttpMessageConverter 完成:默认使用 Jackson,序列化和反序列化都在这里发生
理解 Spring MVC 的请求处理流程后,SpringBoot 的自动配置(如 DispatcherServlet 的自动注册、Jackson 的自动配置、静态资源映射)就有了清晰的上下文——SpringBoot 只是在启动时帮你做了 Spring MVC 的手动配置。