MVC

Spring MVC 是 Spring 的 Web 框架,基于前端控制器模式,由 DispatcherServlet 统一接收请求并分发给相应的 Controller。Spring Boot 通过自动配置开箱即用。


请求处理流程

客户端请求
  └─► DispatcherServlet
        ├─► HandlerMapping(找到匹配的 Controller 方法)
        ├─► HandlerAdapter(调用 Controller 方法)
        │     └─► Controller 方法执行
        │           ├── @RequestBody → HttpMessageConverter 反序列化
        │           └── 返回值 → HttpMessageConverter 序列化
        ├─► HandlerExceptionResolver(异常处理)
        └─► 响应写回客户端

过滤器(Filter)在 DispatcherServlet 之前;拦截器(Interceptor)在 HandlerAdapter 前后。详见 过滤器与拦截器


一、Controller 基础

@RestController                     // @Controller + @ResponseBody
@RequestMapping("/api/v1/orders")   // 类级别路径前缀
@RequiredArgsConstructor
public class OrderController {
 
    private final OrderService orderService;
 
    // GET /api/v1/orders/{id}
    @GetMapping("/{id}")
    public ResponseEntity<OrderDto> getOrder(@PathVariable Long id) {
        return ResponseEntity.ok(orderService.findById(id));
    }
 
    // GET /api/v1/orders?userId=1&status=PENDING&page=0&size=20
    @GetMapping
    public Page<OrderDto> listOrders(
            @RequestParam Long userId,
            @RequestParam(required = false) OrderStatus status,
            @PageableDefault(size = 20, sort = "createdAt",
                             direction = Sort.Direction.DESC) Pageable pageable) {
        return orderService.findByUser(userId, status, pageable);
    }
 
    // POST /api/v1/orders
    @PostMapping
    public ResponseEntity<OrderDto> createOrder(
            @Valid @RequestBody CreateOrderRequest request) {
        OrderDto created = orderService.create(request);
        URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(created.getId())
            .toUri();
        return ResponseEntity.created(location).body(created);
    }
 
    // PUT /api/v1/orders/{id}
    @PutMapping("/{id}")
    public OrderDto updateOrder(@PathVariable Long id,
                                @Valid @RequestBody UpdateOrderRequest request) {
        return orderService.update(id, request);
    }
 
    // DELETE /api/v1/orders/{id}
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteOrder(@PathVariable Long id) {
        orderService.delete(id);
    }
 
    // PATCH /api/v1/orders/{id}/status
    @PatchMapping("/{id}/status")
    public OrderDto patchStatus(@PathVariable Long id,
                                @RequestBody Map<String, String> body) {
        OrderStatus status = OrderStatus.valueOf(body.get("status"));
        return orderService.updateStatus(id, status);
    }
}

二、参数绑定

@PathVariable / @RequestParam

// /items/{category}/{id}?color=red&size=L
@GetMapping("/{category}/{id}")
public ItemDto getItem(
        @PathVariable String category,
        @PathVariable("id") Long itemId,
        @RequestParam String color,
        @RequestParam(defaultValue = "M") String size,
        @RequestParam(required = false) Integer quantity) {
    // ...
}

@RequestBody(JSON 请求体)

public record CreateOrderRequest(
    @NotBlank String productCode,
    @Min(1) @Max(99) int quantity,
    @NotNull @Valid AddressDto shippingAddress
) { }
 
@PostMapping
public ResponseEntity<OrderDto> create(@Valid @RequestBody CreateOrderRequest req) {
    // req 已经过校验
}

参数校验详见 参数校验

@RequestHeader / @CookieValue

@GetMapping("/profile")
public UserDto getProfile(
        @RequestHeader("Authorization") String authHeader,
        @RequestHeader(value = "X-Trace-Id", required = false) String traceId,
        @CookieValue(value = "session", required = false) String sessionId) {
    // ...
}

@ModelAttribute(表单/multipart)

@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String upload(@ModelAttribute UploadForm form) {
    MultipartFile file = form.getFile();
    // ...
}

文件上传详见 文件上传下载


三、响应处理

ResponseEntity

// 精确控制状态码、响应头、响应体
return ResponseEntity
    .status(HttpStatus.CREATED)
    .header("X-Custom-Header", "value")
    .contentType(MediaType.APPLICATION_JSON)
    .body(dto);
 
// 无响应体
return ResponseEntity.noContent().build();
 
// 404
return ResponseEntity.notFound().build();

统一响应格式

// 通用响应包装类(详见 [[统一响应格式]])
@Getter
@AllArgsConstructor
public class ApiResponse<T> {
    private final boolean success;
    private final T data;
    private final String message;
 
    public static <T> ApiResponse<T> ok(T data) {
        return new ApiResponse<>(true, data, null);
    }
 
    public static <T> ApiResponse<T> error(String message) {
        return new ApiResponse<>(false, null, message);
    }
}
 
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
 
    @GetMapping("/{id}")
    public ApiResponse<ProductDto> get(@PathVariable Long id) {
        return ApiResponse.ok(productService.findById(id));
    }
}

全局异常处理见 全局异常处理


四、消息转换

Spring MVC 通过 HttpMessageConverter 处理请求/响应的序列化。默认集成 Jackson:

# application.yml
spring:
  jackson:
    default-property-inclusion: non_null   # null 字段不序列化
    serialization:
      write-dates-as-timestamps: false      # LocalDateTime → ISO-8601 字符串
    deserialization:
      fail-on-unknown-properties: false     # 忽略 JSON 中多余字段
    property-naming-strategy: SNAKE_CASE    # 驼峰 → 下划线(可选)
@Configuration
public class JacksonConfig {
 
    @Bean
    public ObjectMapper objectMapper() {
        return JsonMapper.builder()
            .addModule(new JavaTimeModule())           // Java 8 时间类型
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .serializationInclusion(JsonInclude.Include.NON_NULL)
            .build();
    }
}

消息转换详见 消息转换


五、拦截器

@Component
public class AuthInterceptor implements HandlerInterceptor {
 
    @Override
    public boolean preHandle(HttpServletRequest request,
                              HttpServletResponse response,
                              Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod method = (HandlerMethod) handler;
        // 检查是否有 @SkipAuth 注解
        if (method.hasMethodAnnotation(SkipAuth.class)) {
            return true;
        }
        String token = request.getHeader("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            response.sendError(HttpStatus.UNAUTHORIZED.value(), "未登录");
            return false;    // 拦截请求
        }
        return true;
    }
}
 
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
 
    private final AuthInterceptor authInterceptor;
 
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
            .addPathPatterns("/api/**")
            .excludePathPatterns("/api/auth/**", "/actuator/**");
    }
}

六、静态资源与视图

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
 
    // 自定义静态资源路径
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
            .addResourceLocations("classpath:/static/")
            .setCacheControl(CacheControl.maxAge(7, TimeUnit.DAYS));
 
        // 上传文件目录
        registry.addResourceHandler("/uploads/**")
            .addResourceLocations("file:/var/uploads/");
    }
 
    // CORS 配置(另有 @CrossOrigin 注解和 CorsFilter 方案)
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://app.example.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH")
            .allowedHeaders("*")
            .allowCredentials(true)
            .maxAge(3600);
    }
}

跨域详见 跨域处理;静态资源详见 静态资源


七、内容协商

同一接口根据请求的 Accept 头返回不同格式:

// 引入 Jackson XML 支持
// <dependency>com.fasterxml.jackson.dataformat:jackson-dataformat-xml</dependency>
 
@GetMapping(value = "/report",
            produces = {MediaType.APPLICATION_JSON_VALUE,
                        MediaType.APPLICATION_XML_VALUE})
public ReportDto getReport() {
    return reportService.generate();
}
GET /report
Accept: application/xml

八、WebMvcConfigurer 常用扩展

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
 
    // 注册自定义参数解析器
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new CurrentUserArgumentResolver());
    }
 
    // 注册自定义返回值处理器
    @Override
    public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
        // ...
    }
 
    // 注册消息转换器(追加,不替换默认)
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(0, new ProtobufHttpMessageConverter());
    }
 
    // 格式化器(日期字符串 → LocalDate 等)
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToLocalDateConverter());
    }
}

自定义注解参数解析器

// 从 ThreadLocal / JWT 中解析当前用户,注入到 Controller 方法参数
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser { }
 
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
 
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(CurrentUser.class);
    }
 
    @Override
    public Object resolveArgument(MethodParameter parameter,
                                   ModelAndViewContainer mavContainer,
                                   NativeWebRequest webRequest,
                                   WebDataBinderFactory binderFactory) {
        return SecurityContextHolder.getContext()
            .getAuthentication()
            .getPrincipal();
    }
}
 
// 使用
@GetMapping("/me")
public UserDto getMe(@CurrentUser UserDetails user) {
    return userService.findByUsername(user.getUsername());
}

九、异步 Controller

@RestController
@RequestMapping("/api/reports")
public class ReportController {
 
    private final ReportService reportService;
 
    // 返回 Callable:在 Spring MVC 线程池中执行,释放 Tomcat 线程
    @GetMapping("/callable")
    public Callable<ReportDto> generateReport() {
        return () -> reportService.generateHeavyReport();
    }
 
    // 返回 CompletableFuture
    @GetMapping("/async")
    public CompletableFuture<ReportDto> generateAsync() {
        return reportService.generateAsync();
    }
 
    // 返回 DeferredResult:手动控制完成时机(适合回调通知)
    @GetMapping("/deferred")
    public DeferredResult<ReportDto> generateDeferred() {
        DeferredResult<ReportDto> result = new DeferredResult<>(30_000L);
        reportService.generateWithCallback(
            dto -> result.setResult(dto),
            ex  -> result.setErrorResult(ex)
        );
        return result;
    }
}

异步与线程池配置详见 异步与线程池


常见配置速查

spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher   # Spring Boot 3 默认 PathPatternParser
 
server:
  servlet:
    context-path: /api              # 全局路径前缀
  tomcat:
    threads:
      max: 200                      # Tomcat 最大线程数
      min-spare: 10
    connection-timeout: 20s
    max-swallow-size: 10MB          # 最大请求体大小(上传限制另见 multipart)
  compression:
    enabled: true                   # Gzip 压缩
    mime-types: application/json,text/html
    min-response-size: 2KB

相关链接