乐闻世界logo
搜索文章和话题

Spring Boot 中如何实现全局异常处理?

3月6日 21:58

Spring Boot 全局异常处理详解

为什么需要全局异常处理

在 Web 应用中,异常处理是必不可少的:

  • 用户体验:友好的错误提示而非堆栈信息
  • 系统安全:隐藏内部实现细节
  • 代码整洁:避免每个方法都写 try-catch
  • 统一规范:统一的错误响应格式

实现方式一:@ControllerAdvice + @ExceptionHandler(推荐)

1. 基础异常处理类

java
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 处理业务异常 */ @ExceptionHandler(BusinessException.class) public ResponseEntity<Result<Void>> handleBusinessException(BusinessException e) { log.warn("业务异常: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(Result.error(e.getCode(), e.getMessage())); } /** * 处理参数校验异常 */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Result<Void>> handleValidationException(MethodArgumentNotValidException e) { String message = e.getBindingResult().getFieldErrors().stream() .map(error -> error.getField() + ": " + error.getDefaultMessage()) .collect(Collectors.joining(", ")); log.warn("参数校验失败: {}", message); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(Result.error(400, message)); } /** * 处理参数绑定异常 */ @ExceptionHandler(BindException.class) public ResponseEntity<Result<Void>> handleBindException(BindException e) { String message = e.getFieldErrors().stream() .map(error -> error.getField() + ": " + error.getDefaultMessage()) .collect(Collectors.joining(", ")); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(Result.error(400, message)); } /** * 处理缺少请求参数异常 */ @ExceptionHandler(MissingServletRequestParameterException.class) public ResponseEntity<Result<Void>> handleMissingParam(MissingServletRequestParameterException e) { String message = "缺少必要参数: " + e.getParameterName(); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(Result.error(400, message)); } /** * 处理请求方法不支持异常 */ @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public ResponseEntity<Result<Void>> handleMethodNotSupported(HttpRequestMethodNotSupportedException e) { String message = "请求方法不支持: " + e.getMethod(); return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED) .body(Result.error(405, message)); } /** * 处理资源未找到异常 */ @ExceptionHandler(NoHandlerFoundException.class) public ResponseEntity<Result<Void>> handleNoHandlerFound(NoHandlerFoundException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(Result.error(404, "请求路径不存在")); } /** * 处理空指针异常 */ @ExceptionHandler(NullPointerException.class) public ResponseEntity<Result<Void>> handleNullPointer(NullPointerException e) { log.error("空指针异常", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Result.error(500, "系统内部错误")); } /** * 处理其他所有异常 */ @ExceptionHandler(Exception.class) public ResponseEntity<Result<Void>> handleException(Exception e) { log.error("系统异常", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Result.error(500, "系统繁忙,请稍后重试")); } }

2. 统一响应结果类

java
@Data @NoArgsConstructor @AllArgsConstructor public class Result<T> { private Integer code; private String message; private T data; private Long timestamp; public Result() { this.timestamp = System.currentTimeMillis(); } public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> success() { return success(null); } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } public static <T> Result<T> error(String message) { return error(500, message); } }

3. 自定义业务异常

java
@Getter public class BusinessException extends RuntimeException { private final Integer code; public BusinessException(String message) { super(message); this.code = 500; } public BusinessException(Integer code, String message) { super(message); this.code = code; } public BusinessException(ErrorCode errorCode) { super(errorCode.getMessage()); this.code = errorCode.getCode(); } }

4. 错误码枚举

java
@Getter @AllArgsConstructor public enum ErrorCode { SUCCESS(200, "操作成功"), PARAM_ERROR(400, "参数错误"), UNAUTHORIZED(401, "未授权"), FORBIDDEN(403, "禁止访问"), NOT_FOUND(404, "资源不存在"), INTERNAL_ERROR(500, "系统内部错误"), // 业务错误码 USER_NOT_FOUND(1001, "用户不存在"), USER_ALREADY_EXISTS(1002, "用户已存在"), PASSWORD_ERROR(1003, "密码错误"), ACCOUNT_LOCKED(1004, "账户已锁定"), ORDER_NOT_FOUND(2001, "订单不存在"), ORDER_STATUS_ERROR(2002, "订单状态错误"), INSUFFICIENT_STOCK(2003, "库存不足"); private final Integer code; private final String message; }

实现方式二:实现 HandlerExceptionResolver 接口

java
@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class CustomExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); PrintWriter writer = response.getWriter(); Result<Void> result; if (ex instanceof BusinessException) { BusinessException be = (BusinessException) ex; response.setStatus(HttpServletResponse.SC_BAD_REQUEST); result = Result.error(be.getCode(), be.getMessage()); } else { result = Result.error(500, "系统错误"); } writer.write(JSON.toJSONString(result)); writer.flush(); writer.close(); } catch (IOException e) { e.printStackTrace(); } return new ModelAndView(); } }

实现方式三:@ResponseStatus 注解

java
@ResponseStatus(HttpStatus.NOT_FOUND) public class ResourceNotFoundException extends RuntimeException { public ResourceNotFoundException(String message) { super(message); } } @ResponseStatus(HttpStatus.BAD_REQUEST) public class InvalidParameterException extends RuntimeException { public InvalidParameterException(String message) { super(message); } }

参数校验异常处理(详细)

1. 实体类添加校验注解

java
@Data public class UserCreateDTO { @NotBlank(message = "用户名不能为空") @Size(min = 3, max = 20, message = "用户名长度必须在3-20之间") private String username; @NotBlank(message = "密码不能为空") @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$", message = "密码必须包含大小写字母和数字,且至少8位") private String password; @NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式不正确") private String email; @Min(value = 0, message = "年龄不能小于0") @Max(value = 150, message = "年龄不能大于150") private Integer age; @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") private String phone; }

2. 分组校验

java
public interface CreateGroup {} public interface UpdateGroup {} @Data public class UserDTO { @NotNull(groups = UpdateGroup.class, message = "ID不能为空") private Long id; @NotBlank(groups = CreateGroup.class, message = "用户名不能为空") private String username; @NotBlank(groups = CreateGroup.class, message = "密码不能为空") private String password; }

3. Controller 使用

java
@RestController @RequestMapping("/users") public class UserController { @PostMapping public Result<Void> create(@Validated(CreateGroup.class) @RequestBody UserDTO dto) { // 创建用户 return Result.success(); } @PutMapping public Result<Void> update(@Validated(UpdateGroup.class) @RequestBody UserDTO dto) { // 更新用户 return Result.success(); } }

4. 详细的校验异常处理

java
@RestControllerAdvice @Slf4j public class ValidationExceptionHandler { /** * 处理 @Valid 校验失败 */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Result<Void>> handleMethodArgumentNotValid(MethodArgumentNotValidException e) { Map<String, String> errors = new HashMap<>(); e.getBindingResult().getFieldErrors().forEach(error -> { String fieldName = error.getField(); String errorMessage = error.getDefaultMessage(); errors.put(fieldName, errorMessage); }); log.warn("参数校验失败: {}", errors); return ResponseEntity.badRequest() .body(Result.error(400, "参数校验失败: " + errors)); } /** * 处理 @Validated 校验失败(@RequestParam) */ @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity<Result<Void>> handleConstraintViolation(ConstraintViolationException e) { String message = e.getConstraintViolations().stream() .map(ConstraintViolation::getMessage) .collect(Collectors.joining(", ")); return ResponseEntity.badRequest() .body(Result.error(400, message)); } /** * 处理参数类型不匹配 */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) public ResponseEntity<Result<Void>> handleTypeMismatch(MethodArgumentTypeMismatchException e) { String message = String.format("参数 '%s' 类型不匹配,期望类型: %s", e.getName(), e.getRequiredType().getSimpleName()); return ResponseEntity.badRequest() .body(Result.error(400, message)); } /** * 处理缺少请求体 */ @ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity<Result<Void>> handleMissingBody(HttpMessageNotReadableException e) { return ResponseEntity.badRequest() .body(Result.error(400, "请求体不能为空或格式错误")); } }

404 异常处理配置

Spring Boot 默认不会抛出 404 异常,需要手动开启:

yaml
spring: web: resources: add-mappings: false mvc: throw-exception-if-no-handler-found: true
java
@RestControllerAdvice public class NotFoundExceptionHandler { @ExceptionHandler(NoHandlerFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public Result<Void> handleNoHandlerFound(NoHandlerFoundException e) { return Result.error(404, "请求路径不存在: " + e.getRequestURL()); } }

日志记录最佳实践

java
@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public ResponseEntity<Result<Void>> handleBusinessException(BusinessException e, WebRequest request) { // 记录警告日志 log.warn("业务异常 [{}] - URL: {}, Message: {}", e.getCode(), request.getDescription(false), e.getMessage()); return ResponseEntity.badRequest() .body(Result.error(e.getCode(), e.getMessage())); } @ExceptionHandler(Exception.class) public ResponseEntity<Result<Void>> handleException(Exception e, WebRequest request) { // 生成错误追踪ID String traceId = UUID.randomUUID().toString(); // 记录错误日志,包含堆栈信息 log.error("系统异常 [TraceId: {}] - URL: {}", traceId, request.getDescription(false), e); // 返回给客户端时隐藏详细错误信息 return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Result.error(500, "系统繁忙,请稍后重试 [TraceId: " + traceId + "]")); } }

国际化错误消息

java
@Configuration public class MessageConfig { @Bean public MessageSource messageSource() { ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); messageSource.setBasename("classpath:messages"); messageSource.setDefaultEncoding("UTF-8"); return messageSource; } @Bean public LocalValidatorFactoryBean validator(MessageSource messageSource) { LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean(); bean.setValidationMessageSource(messageSource); return bean; } }
properties
# messages.properties error.user.notfound=User not found error.user.exists=User already exists # messages_zh_CN.properties error.user.notfound=用户不存在 error.user.exists=用户已存在
java
@ExceptionHandler(BusinessException.class) public ResponseEntity<Result<Void>> handleBusinessException(BusinessException e, Locale locale) { String message = messageSource.getMessage(e.getMessageKey(), null, locale); return ResponseEntity.badRequest() .body(Result.error(e.getCode(), message)); }

测试验证

java
@SpringBootTest @AutoConfigureMockMvc public class GlobalExceptionHandlerTest { @Autowired private MockMvc mockMvc; @Test public void testBusinessException() throws Exception { mockMvc.perform(get("/test/business-error")) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value(400)) .andExpect(jsonPath("$.message").value("业务错误")); } @Test public void testValidationException() throws Exception { mockMvc.perform(post("/users") .contentType(MediaType.APPLICATION_JSON) .content("{\"username\": \"ab\", \"email\": \"invalid\"}")) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value(400)); } @Test public void testNotFound() throws Exception { mockMvc.perform(get("/non-existent-path")) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.code").value(404)); } }

总结

方式适用场景优点缺点
@ControllerAdvice大多数场景集中管理、灵活
HandlerExceptionResolver需要完全控制最高优先级较复杂
@ResponseStatus简单异常简洁不够灵活

全局异常处理是构建健壮 REST API 的基础,建议:

  1. 统一错误响应格式
  2. 定义清晰的错误码体系
  3. 合理记录日志(区分 warn/error)
  4. 生产环境隐藏敏感信息
标签:Spring Boot