Skip to content

Spring MVC 深度解析

Spring MVC 是 Java Web 开发的事实标准。理解 DispatcherServlet 的请求处理流程,能帮你更好地处理各种 Web 场景。

请求处理流程

HTTP 请求

DispatcherServlet(前端控制器)

HandlerMapping(找到处理器)
  - RequestMappingHandlerMapping(@RequestMapping)

HandlerAdapter(适配器,调用处理器)
  - RequestMappingHandlerAdapter

HandlerInterceptor.preHandle()(拦截器前置)

Controller 方法执行
  - 参数解析(HandlerMethodArgumentResolver)
  - 方法调用
  - 返回值处理(HandlerMethodReturnValueHandler)

HandlerInterceptor.postHandle()(拦截器后置)

ViewResolver(视图解析,REST API 通常跳过)

HandlerInterceptor.afterCompletion()

HTTP 响应

Controller 开发

java
@RestController  // = @Controller + @ResponseBody
@RequestMapping("/api/v1/users")
@Validated       // 开启方法级别参数验证
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    // GET /api/v1/users?page=1&size=20&keyword=alice
    @GetMapping
    public PageResult<UserVO> listUsers(
            @RequestParam(defaultValue = "1") @Min(1) int page,
            @RequestParam(defaultValue = "20") @Max(100) int size,
            @RequestParam(required = false) String keyword) {
        return userService.listUsers(page, size, keyword);
    }

    // GET /api/v1/users/42
    @GetMapping("/{id}")
    public UserVO getUser(@PathVariable @Positive Long id) {
        return userService.getById(id)
            .orElseThrow(() -> new ResourceNotFoundException("用户不存在: " + id));
    }

    // POST /api/v1/users
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public UserVO createUser(@RequestBody @Valid CreateUserRequest req) {
        return userService.create(req);
    }

    // PUT /api/v1/users/42
    @PutMapping("/{id}")
    public UserVO updateUser(
            @PathVariable Long id,
            @RequestBody @Valid UpdateUserRequest req) {
        return userService.update(id, req);
    }

    // DELETE /api/v1/users/42
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteUser(@PathVariable Long id) {
        userService.delete(id);
    }
}

请求参数绑定

java
@RestController
@RequestMapping("/demo")
public class BindingDemo {

    // 路径变量
    @GetMapping("/users/{id}/orders/{orderId}")
    public String pathVar(@PathVariable Long id, @PathVariable Long orderId) {
        return id + "/" + orderId;
    }

    // 查询参数
    @GetMapping("/search")
    public String queryParam(
            @RequestParam String q,
            @RequestParam(required = false, defaultValue = "10") int limit) {
        return q + ":" + limit;
    }

    // 请求头
    @GetMapping("/header")
    public String header(
            @RequestHeader("Authorization") String auth,
            @RequestHeader(value = "X-Request-ID", required = false) String requestId) {
        return auth;
    }

    // Cookie
    @GetMapping("/cookie")
    public String cookie(@CookieValue(value = "session", required = false) String session) {
        return session;
    }

    // 请求体(JSON)
    @PostMapping("/body")
    public String body(@RequestBody Map<String, Object> data) {
        return data.toString();
    }

    // 表单数据
    @PostMapping("/form")
    public String form(@ModelAttribute UserForm form) {
        return form.toString();
    }

    // 文件上传
    @PostMapping("/upload")
    public String upload(@RequestParam("file") MultipartFile file) throws IOException {
        String filename = file.getOriginalFilename();
        file.transferTo(Path.of("/uploads/" + filename));
        return "上传成功: " + filename;
    }

    // HttpServletRequest / HttpServletResponse(直接注入)
    @GetMapping("/raw")
    public void raw(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.getWriter().write("raw response");
    }
}

数据验证

java
// 请求 DTO
public class CreateUserRequest {

    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 50, message = "用户名长度 2-50")
    private String name;

    @NotBlank
    @Email(message = "邮箱格式不正确")
    private String email;

    @NotNull
    @Min(value = 0, message = "年龄不能为负")
    @Max(value = 150, message = "年龄不能超过 150")
    private Integer age;

    @NotBlank
    @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).{8,}$",
             message = "密码至少 8 位,包含字母和数字")
    private String password;

    @Valid  // 嵌套验证
    @NotNull
    private AddressRequest address;
}

// 全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {

    // 参数验证失败
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<Void> handleValidationException(MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(e -> e.getField() + ": " + e.getDefaultMessage())
            .collect(Collectors.toList());
        return ApiResponse.fail(400, "参数验证失败", errors);
    }

    // 业务异常
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException ex) {
        return ResponseEntity
            .status(ex.getHttpStatus())
            .body(ApiResponse.fail(ex.getCode(), ex.getMessage()));
    }

    // 资源不存在
    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ApiResponse<Void> handleNotFound(ResourceNotFoundException ex) {
        return ApiResponse.fail(404, ex.getMessage());
    }

    // 兜底异常
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ApiResponse<Void> handleException(Exception ex) {
        log.error("未处理的异常", ex);
        return ApiResponse.fail(500, "服务器内部错误");
    }
}

拦截器

java
@Component
public class AuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest req,
                             HttpServletResponse resp,
                             Object handler) throws Exception {
        // 放行静态资源和公开接口
        if (!(handler instanceof HandlerMethod)) return true;

        HandlerMethod method = (HandlerMethod) handler;
        if (method.hasMethodAnnotation(PublicApi.class)) return true;

        // 验证 Token
        String token = req.getHeader("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            resp.setStatus(401);
            resp.getWriter().write("{\"code\":401,\"message\":\"未认证\"}");
            return false;
        }

        Claims claims = jwtUtil.parseToken(token.substring(7));
        req.setAttribute("userId", claims.getSubject());
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest req,
                                HttpServletResponse resp,
                                Object handler, Exception ex) {
        // 清理 ThreadLocal 等资源
        RequestContext.clear();
    }
}

// 注册拦截器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private AuthInterceptor authInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
            .addPathPatterns("/api/**")
            .excludePathPatterns("/api/auth/**", "/api/public/**");
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("http://localhost:3000")
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
            .allowedHeaders("*")
            .allowCredentials(true)
            .maxAge(3600);
    }
}

统一响应格式

java
// 统一响应体
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ApiResponse<T> {
    private int code;
    private String message;
    private T data;
    private long timestamp = System.currentTimeMillis();

    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(200, "success", data, System.currentTimeMillis());
    }

    public static <T> ApiResponse<T> fail(int code, String message) {
        return new ApiResponse<>(code, message, null, System.currentTimeMillis());
    }
}

// 使用 ResponseBodyAdvice 统一包装
@RestControllerAdvice
public class ResponseWrapper implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType,
                            Class<? extends HttpMessageConverter<?>> converterType) {
        // 只包装 @RestController 的返回值
        return returnType.getDeclaringClass().isAnnotationPresent(RestController.class)
            && !returnType.getParameterType().equals(ApiResponse.class);
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof String) {
            // String 类型需要特殊处理
            return new ObjectMapper().writeValueAsString(ApiResponse.success(body));
        }
        return ApiResponse.success(body);
    }
}

Spring MVC 最佳实践

  • 使用 @RestControllerAdvice 统一处理异常,不要在 Controller 中 try-catch
  • 参数验证用 @Valid + Bean Validation,不要手动 if 判断
  • 返回 ResponseEntity 可以精确控制 HTTP 状态码和响应头
  • 文件上传配置 spring.servlet.multipart.max-file-size=50MB

系统学习 Java 生态,深入底层架构