问题引入

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(如 /fooid="/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 按以下优先级选择:

  1. 路径精确度/orders/123 > /orders/{id} > /orders/*
  2. HTTP 方法匹配@GetMapping("/orders") 优先于 @RequestMapping("/orders")
  3. 参数条件@GetMapping(value="/orders", params="format=json") 更精确
  4. ** 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 反序列化。常见失败场景:

  1. 字段类型不匹配:JSON 中的 "age": "abc" 无法转为 Integer
  2. 缺少无参构造器:Jackson 需要无参构造器(或 @JsonCreator
  3. 日期格式错误:默认要求 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 方法,中间经历了精密的组件协作:

  1. DispatcherServlet 是 Front Controller 模式的实现:所有请求的统一入口,负责分发、协调、异常处理
  2. HandlerMapping 负责 URL → 方法的映射:启动时扫描 @RequestMapping 建立映射表,请求到达时按精确度匹配
  3. HandlerAdapter 是适配器模式的应用:将不同类型的 Handler(注解方法、HttpRequestHandler、Servlet)统一包装,让 DispatcherServlet 无需关心具体类型
  4. 拦截器与过滤器处于不同层次:Filter 是 Servlet 规范(Tomcat 层面),可以修改请求/响应体;Interceptor 是 Spring MVC 机制,可以访问 Controller 和 ModelAndView
  5. 统一异常处理通过 HandlerExceptionResolver 链实现:@ControllerAdvice + @ExceptionHandler 是最优雅的方式,但异常处理也有优先级和匹配规则
  6. @RestController 的 JSON 序列化由 HttpMessageConverter 完成:默认使用 Jackson,序列化和反序列化都在这里发生

理解 Spring MVC 的请求处理流程后,SpringBoot 的自动配置(如 DispatcherServlet 的自动注册、Jackson 的自动配置、静态资源映射)就有了清晰的上下文——SpringBoot 只是在启动时帮你做了 Spring MVC 的手动配置。