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 常用状态码
| 状态码 | 含义 | 常用场景 |
|---|---|---|
| 400 | Bad Request | 参数错误、校验失败 |
| 401 | Unauthorized | 未登录 |
| 403 | Forbidden | 无权限 |
| 404 | Not Found | 资源不存在 |
| 409 | Conflict | 数据冲突 |
| 500 | Internal 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 异常处理流程?
- Controller 抛出异常
- DispatcherServlet 异常检测
- 遍历 HandlerExceptionResolver 链
- 找到能处理的 resolver,执行处理
- 返回 ModelAndView 或 null
Q2: @ExceptionHandler 和 @ControllerAdvice 区别?
@ExceptionHandler:方法级,标注在 Controller 内@ControllerAdvice:类级,全局增强,所有 Controller 的异常都能处理
Q3: 如何自定义异常响应格式?
- 定义统一响应类
Result - 使用
@ControllerAdvice全局处理 - 异常转
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
- 事务管理