Skip to content

Spring MVC 异常处理机制

一、异常处理概述

为什么需要统一异常处理

java
// 没有统一异常处理时
@PostMapping("/users")
public User create(@RequestBody User user) {
    try {
        return userService.create(user);
    } catch (ValidationException e) {
        return "参数校验失败";  // 处理不一致
    } catch (DuplicateKeyException e) {
        return "数据重复";     // 处理不一致
    } catch (Exception e) {
        return "服务器错误";   // 处理不一致
    }
}

统一异常处理的好处

┌─────────────────────────────────────────────────────────────────┐
│                    统一异常处理架构                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Controller                                                      │
│     │                                                             │
│     ▼                                                             │
│  Service → Exception                                             │
│     │                                                             │
│     ▼                                                             │
│  DAO → Exception                                                  │
│     │                                                             │
│     ▼                                                             │
│  ┌─────────────────────────────────────────────┐                │
│  │        ExceptionHandlerExceptionResolver     │                │
│  │  ┌───────────────────────────────────────┐  │                │
│  │  │  @ExceptionHandler 方法               │  │                │
│  │  │  @ControllerAdvice 全局增强           │  │                │
│  │  └───────────────────────────────────────┘  │                │
│  └─────────────────────────────────────────────┘                │
│                        │                                        │
│                        ▼                                        │
│              统一的错误响应格式                                   │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

二、@ExceptionHandler

基本用法

java
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @ExceptionHandler(ValidationException.class)
    public Map<String, Object> handleValidation(ValidationException e) {
        return Map.of("code", 400, "message", e.getMessage());
    }
    
    @ExceptionHandler(UserNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Map<String, Object> handleNotFound(UserNotFoundException e) {
        return Map.of("code", 404, "message", e.getMessage());
    }
    
    @ExceptionHandler(Exception.class)
    public Map<String, Object> handleGeneral(Exception e) {
        return Map.of("code", 500, "message", "服务器内部错误");
    }
}

异常参数

java
@ExceptionHandler
public String handleException(Exception e, HttpServletRequest request) {
    // 可以注入 HttpServletRequest
    request.setAttribute("error", e.getMessage());
    return "error";
}

@ExceptionHandler
public String handleException(Exception e, Model model) {
    // 可以注入 Model
    model.addAttribute("error", e.getMessage());
    return "error";
}

三、@ControllerAdvice

全局异常处理器

java
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(UserNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Map<String, Object> handleNotFound(UserNotFoundException e) {
        return Map.of("code", 404, "message", e.getMessage());
    }
    
    @ExceptionHandler(ValidationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, Object> handleValidation(ValidationException e) {
        List<String> errors = e.getErrors().stream()
            .map(error -> error.getField() + ": " + error.getDefaultMessage())
            .collect(Collectors.toList());
        return Map.of("code", 400, "errors", errors);
    }
    
    @ExceptionHandler(Exception.class)
    public Map<String, Object> handleGeneral(Exception e) {
        return Map.of("code", 500, "message", "服务器内部错误");
    }
}

限定范围

java
// 1. 限定 Controller
@ControllerAdvice(annotations = RestController.class)
public class RestExceptionHandler { }

// 2. 限定包
@ControllerAdvice(basePackages = "com.example.controller")
public class PackageExceptionHandler { }

// 3. 限定类
@ControllerAdvice(assignableTypes = {UserController.class, OrderController.class})
public class SpecificExceptionHandler { }

处理校验异常

java
@RestControllerAdvice
public class ValidationExceptionHandler {
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, Object> handleValidException(MethodArgumentNotValidException e) {
        Map<String, String> errors = new HashMap<>();
        e.getBindingResult().getFieldErrors().forEach(error -> 
            errors.put(error.getField(), error.getDefaultMessage())
        );
        
        return Map.of(
            "code", 400,
            "message", "参数校验失败",
            "errors", errors
        );
    }
    
    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, Object> handleBindException(BindException e) {
        // 处理 @ModelAttribute 校验失败
        return Map.of("code", 400, "message", "绑定失败");
    }
    
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, Object> handleConstraintViolation(ConstraintViolationException e) {
        Set<String> errors = e.getConstraintViolations().stream()
            .map(v -> v.getPropertyPath() + ": " + v.getMessage())
            .collect(Collectors.toSet());
        
        return Map.of("code", 400, "errors", errors);
    }
    
    @ExceptionHandler(MissingServletRequestParameterException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, Object> handleMissingParam(MissingServletRequestParameterException e) {
        return Map.of("code", 400, "message", "缺少参数: " + e.getParameterName());
    }
}

四、@ResponseStatus

标注异常类

java
// 方式1:注解在异常类上
@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(String message) {
        super(message);
    }
    
    public UserNotFoundException(Long id) {
        super("用户不存在: " + id);
    }
}

// 方式2:注解在方法上
@GetMapping("/user/{id}")
@ResponseStatus(HttpStatus.OK)
public User getUser(@PathVariable Long id) {
    return userService.findById(id);
}

HttpStatus 常用状态码

状态码含义常用场景
400Bad Request参数错误、校验失败
401Unauthorized未登录
403Forbidden无权限
404Not Found资源不存在
409Conflict数据冲突
500Internal Server Error服务器错误

五、HandlerExceptionResolver

异常处理链路

┌─────────────────────────────────────────────────────────────────┐
│                    异常处理解析器链                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1. ExceptionHandlerExceptionResolver                           │
│     └─ 处理 @ExceptionHandler 标注的方法                        │
│                                                                  │
│  2. ResponseStatusExceptionResolver                             │
│     └─ 处理 @ResponseStatus 标注的异常                         │
│                                                                  │
│  3. DefaultHandlerExceptionResolver                             │
│     └─ 处理 Spring MVC 标准异常                                │
│        (NoSuchRequestHandlingMethod, HttpRequestMethodNotSupported,│
│         HttpMediaTypeNotSupported, etc.)                        │
│                                                                  │
│  4. SimpleMappingExceptionResolver                             │
│     └─ 处理映射到视图的异常                                     │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

SimpleMappingExceptionResolver

java
@Configuration
public class ExceptionConfig {
    
    @Bean
    public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
        SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
        
        Properties mappings = new Properties();
        mappings.setProperty("UserNotFoundException", "error/404");
        mappings.setProperty("ValidationException", "error/400");
        mappings.setProperty("Exception", "error/500");
        
        resolver.setExceptionMappings(mappings);
        
        // 设置异常属性名
        resolver.setExceptionAttribute("ex");
        
        // 设置状态码映射
        Properties statusCodes = new Properties();
        statusCodes.setProperty("error/404", "404");
        statusCodes.setProperty("error/500", "500");
        resolver.setStatusCodes(statusCodes);
        
        return resolver;
    }
}

自定义异常解析器

java
@Component
public class CustomExceptionResolver implements HandlerExceptionResolver {
    
    private final Logger log = LoggerFactory.getLogger(getClass());
    
    @Override
    public ModelAndView resolveException(HttpServletRequest request, 
                                        HttpServletResponse response, 
                                        Object handler, 
                                        Exception ex) {
        // 记录日志
        log.error("请求处理异常: {} {}", request.getRequestURI(), ex.getMessage(), ex);
        
        // 判断是否已处理(通过请求属性)
        if (Boolean.TRUE.equals(request.getAttribute("exceptionHandled"))) {
            return null;
        }
        
        ModelAndView mav = new ModelAndView("error/general");
        mav.addObject("exception", ex);
        mav.addObject("url", request.getRequestURI());
        
        if (ex instanceof UserNotFoundException) {
            mav.setViewName("error/404");
            mav.setStatus(HttpStatus.NOT_FOUND);
        } else if (ex instanceof ValidationException) {
            mav.setViewName("error/400");
            mav.setStatus(HttpStatus.BAD_REQUEST);
        }
        
        request.setAttribute("exceptionHandled", true);
        return mav;
    }
}

六、统一响应格式

响应封装类

java
public class ApiResponse<T> {
    private int code;
    private String message;
    private T data;
    private long timestamp;
    
    private ApiResponse() {
        this.timestamp = System.currentTimeMillis();
    }
    
    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.code = 200;
        response.message = "success";
        response.data = data;
        return response;
    }
    
    public static <T> ApiResponse<T> error(int code, String message) {
        ApiResponse<T> response = new ApiResponse<>();
        response.code = code;
        response.message = message;
        return response;
    }
    
    // getters/setters
}

全局响应包装

java
@RestControllerAdvice
public class GlobalResponseHandler implements ResponseBodyAdvice<Object> {
    
    @Override
    public boolean supports(MethodParameter returnType, 
                           Class<? extends HttpMessageConverter<?>> converterType) {
        // 判断是否需要包装
        return !returnType.getParameterType().equals(ApiResponse.class);
    }
    
    @Override
    public Object beforeBodyWrite(Object body, 
                                  MethodParameter returnType, 
                                  MediaType selectedContentType, 
                                  Class<? extends HttpMessageConverter<?>> converterType,
                                  ServerHttpRequest request, 
                                  ServerHttpResponse response) {
        if (body instanceof Map && ((Map<?, ?>) body).containsKey("code")) {
            return body;  // 已经是标准响应
        }
        return ApiResponse.success(body);
    }
}

使用

java
@RestController
public class UserController {
    
    @GetMapping("/{id}")
    public ApiResponse<User> getUser(@PathVariable Long id) {
        User user = userService.findById(id);
        return ApiResponse.success(user);
    }
    
    @PostMapping
    public ApiResponse<User> create(@RequestBody @Valid User user) {
        User created = userService.save(user);
        return ApiResponse.success(created);
    }
}

七、异常处理最佳实践

异常定义

java
// 基础异常
public class BusinessException extends RuntimeException {
    private int code = 500;
    
    public BusinessException(String message) {
        super(message);
    }
    
    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }
}

// 具体业务异常
public class UserNotFoundException extends BusinessException {
    public UserNotFoundException(Long id) {
        super(404, "用户不存在: " + id);
    }
}

public class ValidationException extends BusinessException {
    private Map<String, String> errors;
    
    public ValidationException(String message, Map<String, String> errors) {
        super(400, message);
        this.errors = errors;
    }
}

完整异常处理器

java
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    private final Logger log = LoggerFactory.getLogger(getClass());
    
    @ExceptionHandler(BusinessException.class)
    public ApiResponse<?> handleBusiness(BusinessException e) {
        log.warn("业务异常: {}", e.getMessage());
        return ApiResponse.error(e.getCode(), e.getMessage());
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResponse<?> handleValidation(MethodArgumentNotValidException e) {
        Map<String, String> errors = new HashMap<>();
        e.getBindingResult().getFieldErrors().forEach(fe -> 
            errors.put(fe.getField(), fe.getDefaultMessage()));
        return ApiResponse.error(400, "参数校验失败");
    }
    
    @ExceptionHandler(Exception.class)
    public ApiResponse<?> handleGeneral(Exception e) {
        log.error("系统异常", e);
        return ApiResponse.error(500, "服务器内部错误");
    }
}

八、异常处理与日志

日志记录

java
@RestControllerAdvice
public class ExceptionHandlerWithLogging {
    
    private final Logger log = LoggerFactory.getLogger(getClass());
    
    @ExceptionHandler(Exception.class)
    public Result<?> handleException(Exception e, HandlerMethod handlerMethod) {
        // 记录方法信息
        log.error("Controller: {}.{} 发生异常", 
            handlerMethod.getBeanType().getSimpleName(),
            handlerMethod.getMethod().getName(),
            e);
        
        // 区分业务异常和系统异常
        if (e instanceof BusinessException) {
            return Result.error(((BusinessException) e).getCode(), e.getMessage());
        }
        
        // 系统异常不暴露具体信息给前端
        return Result.error(500, "系统繁忙,请稍后重试");
    }
}

敏感信息过滤

java
@RestControllerAdvice
public class SafeExceptionHandler {
    
    @ExceptionHandler(Exception.class)
    public Result<?> handleException(Exception e) {
        // 过滤敏感异常信息
        if (e instanceof DataAccessException) {
            log.error("数据库异常", e);
            return Result.error(500, "数据库操作失败");
        }
        
        if (e instanceof HttpMessageNotReadableException) {
            return Result.error(400, "请求格式错误");
        }
        
        if (e instanceof MethodArgumentTypeMismatchException) {
            return Result.error(400, "参数类型错误");
        }
        
        log.error("未知异常", e);
        return Result.error(500, "系统繁忙");
    }
}

九、面试高频问题

Q1: Spring MVC 异常处理流程?

  1. Controller 抛出异常
  2. DispatcherServlet 异常检测
  3. 遍历 HandlerExceptionResolver 链
  4. 找到能处理的 resolver,执行处理
  5. 返回 ModelAndView 或 null

Q2: @ExceptionHandler 和 @ControllerAdvice 区别?

  • @ExceptionHandler:方法级,标注在 Controller 内
  • @ControllerAdvice:类级,全局增强,所有 Controller 的异常都能处理

Q3: 如何自定义异常响应格式?

  1. 定义统一响应类 Result
  2. 使用 @ControllerAdvice 全局处理
  3. 异常转 Result.error()

Q4: SimpleMappingExceptionResolver 用途?

将异常映射到视图,如 UserNotFoundException → error/404

Q5: 异常处理返回 String vs ModelAndView?

  • String:视图名
  • ModelAndView:视图+模型
  • Object/@ResponseBody:JSON 响应

十、下一章预告

下一章我们将学习 Spring Data 数据访问

  • SpringJDBC
  • Spring Data JPA
  • Spring Data Redis
  • 事务管理

基于 MIT 许可发布