AI摘要
文章提出用Spring全局异常处理替代Controller重复try-catch,统一封装ApiResult与自定义异常体系,@RestControllerAdvice按精确度匹配异常,结合参数校验与自定义注解,使业务代码专注逻辑、简洁健壮。
(一) 为什么要全局异常处理?
原始代码的痛点:
@RestController
public class UserController {
@PostMapping("/users")
public ApiResult createUser(@RequestBody @Valid UserDTO userDTO) {
try {
// 参数校验可能失败
if (userService.existsByUsername(userDTO.getUsername())) {
return ApiResult.error("用户名已存在");
}
// 业务操作可能失败
userService.createUser(userDTO);
return ApiResult.success();
} catch (IllegalArgumentException e) {
log.error("参数错误", e);
return ApiResult.error("请求参数错误");
} catch (DataAccessException e) {
log.error("数据库错误", e);
return ApiResult.error("系统繁忙,请稍后重试");
} catch (Exception e) {
log.error("未知错误", e);
return ApiResult.error("系统异常");
}
}
// 每个方法都要重复这个try-catch模板...
}问题分析:
- 代码重复: 每个Controller方法都有相似的异常处理逻辑
- 关注点混杂: 业务逻辑和异常处理代码交织在一起
- 维护困难: 修改异常处理策略需要在每个方法中修改
- 容易遗漏: 新开发的功能可能忘记添加异常处理
(二) 全局异常处理器的完整实现
1. 统一的响应体封装:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResult<T> {
private Integer code;
private String message;
private T data;
private Long timestamp = System.currentTimeMillis();
public static <T> ApiResult<T> success(T data) {
return new ApiResult<>(200, "成功", data, System.currentTimeMillis());
}
public static ApiResult<?> error(Integer code, String message) {
return new ApiResult<>(code, message, null, System.currentTimeMillis());
}
public static ApiResult<?> error(String message) {
return error(500, message);
}
}2. 自定义异常体系:
// 基础业务异常
public class BaseException extends RuntimeException {
private final Integer code;
public BaseException(Integer code, String message) {
super(message);
this.code = code;
}
public BaseException(String message) {
this(400, message);
}
}
// 具体的业务异常
public class BusinessException extends BaseException {
public BusinessException(String message) {
super(400, message);
}
public BusinessException(Integer code, String message) {
super(code, message);
}
}
public class UnauthorizedException extends BaseException {
public UnauthorizedException(String message) {
super(401, message);
}
}
public class ForbiddenException extends BaseException {
public ForbiddenException(String message) {
super(403, message);
}
}3. 全局异常处理器核心实现:
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理业务异常
*/
@ExceptionHandler(BusinessException.class)
public ApiResult<?> handleBusinessException(BusinessException e) {
log.warn("业务异常: code={}, message={}", e.getCode(), e.getMessage());
return ApiResult.error(e.getCode(), e.getMessage());
}
/**
* 处理参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResult<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining("; "));
log.warn("参数校验失败: {}", message);
return ApiResult.error(400, message);
}
/**
* 处理缺少请求参数异常
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
public ApiResult<?> handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
String message = String.format("缺少必要参数: %s", e.getParameterName());
log.warn(message);
return ApiResult.error(400, message);
}
/**
* 处理权限相关异常
*/
@ExceptionHandler({UnauthorizedException.class, AccessDeniedException.class})
public ApiResult<?> handleAccessDeniedException(Exception e) {
log.warn("权限异常: {}", e.getMessage());
return ApiResult.error(403, "权限不足");
}
/**
* 处理数据操作异常
*/
@ExceptionHandler(DataAccessException.class)
public ApiResult<?> handleDataAccessException(DataAccessException e) {
log.error("数据操作异常", e);
return ApiResult.error(500, "数据操作失败,请稍后重试");
}
/**
* 处理所有未识别的异常
*/
@ExceptionHandler(Exception.class)
public ApiResult<?> handleException(Exception e) {
log.error("系统异常: ", e);
// 生产环境不要返回详细错误信息
String message = "生产环境".equals(env) ? "系统繁忙,请稍后重试" : e.getMessage();
return ApiResult.error(500, message);
}
}(三) 高级特性:异常处理器的执行顺序
Spring会按照@ExceptionHandler方法的精确度匹配异常:
- 先匹配最具体的异常类型(如
BusinessException) - 再匹配父类异常(如
RuntimeException) - 最后匹配
Exception.class
// 更精确的异常处理优先
@ExceptionHandler(FileNotFoundException.class) // 优先匹配
public ApiResult<?> handleFileNotFound(FileNotFoundException e) {
return ApiResult.error("文件不存在");
}
@ExceptionHandler(IOException.class) // 其次匹配
public ApiResult<?> handleIO(IOException e) {
return ApiResult.error("IO异常");
}(四) 与参数校验框架的深度集成
1. 自定义验证注解:
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class PhoneValidator implements ConstraintValidator<Phone, String> {
private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return true; // 使用@NotNull处理空值
return PHONE_PATTERN.matcher(value).matches();
}
}2. 在DTO中使用:
@Data
public class UserDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度2-20位")
private String username;
@Phone(message = "手机号格式不正确")
private String phone;
}(五) 最终效果:简洁的Controller
@RestController
@Validated
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/users")
public ApiResult<UserVO> createUser(@RequestBody @Valid UserDTO userDTO) {
// 专注业务逻辑,异常交给全局处理器
UserVO user = userService.createUser(userDTO);
return ApiResult.success(user);
}
@GetMapping("/users/{id}")
public ApiResult<UserVO> getUser(@PathVariable @Min(1) Long id) {
UserVO user = userService.getUserById(id);
return ApiResult.success(user);
}
}总结: 全局异常处理是Spring Boot项目的基础设施,良好的异常处理架构能显著提升代码质量和开发效率。通过分层级的异常体系和统一的处理逻辑,让业务代码更加纯粹和健壮。