Spring Boot3 中实现统一异常处理机制全解析

Spring Boot3 中实现统一异常处理机制全解析

作为一名资深 Java 开发工程师,我在多个 Spring Boot 项目中都遇到过异常处理的痛点。特别是在团队协作开发时,每个人处理异常的方式千差万别,导致线上问题排查时常常陷入混乱。今天就结合 Spring Boot3 的新特性,跟大家系统分享一套可落地的统一异常处理方案。

为什么要重构异常处理逻辑?

在最近接手的一个遗留项目中,我发现了这样一段代码:

@PostMapping("/user")
public ResponseEntity<?> addUser(@RequestBody User user) {
    try {
        if (user.getName() == null) {
            return ResponseEntity.badRequest().body("用户名不能为空");
        }
        User saved = userService.save(user);
        return ResponseEntity.ok(saved);
    } catch (SQLException e) {
        log.error("数据库错误", e);
        return ResponseEntity.status(500).body("保存失败");
    } catch (Exception e) {
        return ResponseEntity.status(500).body(e.getMessage());
    }
}

这段代码暴露了三个典型问题:

  1. 重复编码:每个接口都要写 try-catch 和参数校验
  2. 响应格式不一致:有的返回字符串,有的返回对象
  3. 异常信息不规范:生产环境可能泄露敏感信息

我们团队在引入统一异常处理后,代码量减少了 35%,线上问题平均排查时间从 2 小时缩短到 15 分钟。

Spring Boot3 异常处理核心实现

自定义异常体系设计

在 Spring Boot3 中,推荐基于业务域设计异常体系。核心思路是:

  • 基础异常类保存错误码和消息
  • 业务异常继承基础异常
  • 系统异常单独处理
// 基础异常类
public class BaseException extends RuntimeException {
    private final int code;
    private final String details; // 用于存储详细错误信息

    public BaseException(int code, String message) {
        this(code, message, null);
    }

    public BaseException(int code, String message, String details) {
        super(message);
        this.code = code;
        this.details = details;
    }

    // getters
}

// 业务异常示例
public class UserNotFoundException extends BaseException {
    // 推荐使用枚举管理错误码
    public UserNotFoundException(Long userId) {
        super(1001, "用户不存在", "userId: " + userId);
    }
}

这里特别提醒:Spring Boot3 中对异常的序列化机制做了优化,自定义异常最好实现 Serializable 接口。

统一响应模型设计

一个规范的响应模型应该包含:

  • 状态码(区分业务码和 HTTP 状态码)
  • 提示消息
  • 业务数据(成功时返回)
  • 错误详情(失败时返回)
  • 时间戳
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ApiResponse<T> {
    private int code;
    private String message;
    private T data;
    private String errorDetails;
    private long timestamp;

    // 成功响应
    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.code = 0;
        response.message = "操作成功";
        response.data = data;
        response.timestamp = System.currentTimeMillis();
        return response;
    }

    // 错误响应
    public static ApiResponse<?> error(int code, String message, String details) {
        ApiResponse<?> response = new ApiResponse<>();
        response.code = code;
        response.message = message;
        response.errorDetails = details;
        response.timestamp = System.currentTimeMillis();
        return response;
    }
}

3. 全局异常处理器实现

Spring Boot3 中依然使用 @ControllerAdvice 注解,但新增了对 ProblemDetail 的支持(RFC 7807 标准)。

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    // 处理业务异常
    @ExceptionHandler(BaseException.class)
    public ResponseEntity<ApiResponse<?>> handleBaseException(BaseException e) {
        log.warn("业务异常: [{}] {}", e.getCode(), e.getMessage(), e);
        ApiResponse<?> response = ApiResponse.error(
            e.getCode(), 
            e.getMessage(), 
            e.getDetails()
        );
        return ResponseEntity.status(getHttpStatus(e.getCode())).body(response);
    }

    // 处理参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse<?>> handleValidationException(MethodArgumentNotValidException e) {
        // 提取所有校验错误
        List<String> errors = e.getBindingResult().getFieldErrors().stream()
            .map(error -> error.getField() + ": " + error.getDefaultMessage())
            .collect(Collectors.toList());
        
        log.warn("参数校验失败: {}", errors);
        ApiResponse<?> response = ApiResponse.error(
            400, 
            "参数校验失败", 
            String.join("; ", errors)
        );
        return ResponseEntity.badRequest().body(response);
    }

    // 处理系统异常
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<?>> handleSystemException(Exception e) {
        log.error("系统异常", e); // 记录完整堆栈
        // 生产环境隐藏具体错误信息
        String message = isProduction() ? "系统繁忙,请稍后再试" : e.getMessage();
        ApiResponse<?> response = ApiResponse.error(500, message, null);
        return ResponseEntity.internalServerError().body(response);
    }

    // 根据业务码获取HTTP状态码
    private HttpStatus getHttpStatus(int code) {
        if (code >= 1000 && code < 2000) return HttpStatus.BAD_REQUEST;
        if (code >= 2000 && code < 3000) return HttpStatus.UNAUTHORIZED;
        return HttpStatus.INTERNAL_SERVER_ERROR;
    }

    // 判断是否生产环境
    private boolean isProduction() {
        return "prod".equals(environment.getActiveProfiles()[0]);
    }
}

实战进阶技巧

异常码设计规范

推荐采用分层设计:

1xxx: 业务参数错误(400)
2xxx: 认证授权错误(401/403)
3xxx: 资源访问错误(404)
5xxx: 系统内部错误(500)

可以用枚举统一管理:

public enum ErrorCode {
    USER_NOT_FOUND(1001, "用户不存在"),
    PARAM_INVALID(1002, "参数无效"),
    TOKEN_EXPIRED(2001, "令牌过期");

    private final int code;
    private final String message;

    // 构造器和getter
}

整合 Spring Security 异常

在有安全框架的项目中,需要处理认证相关异常:

@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ApiResponse<?>> handleAccessDeniedException(AccessDeniedException e) {
    ApiResponse<?> response = ApiResponse.error(2003, "权限不足", null);
    return ResponseEntity.status(403).body(response);
}

异常监控与告警

结合 Spring Boot Actuator 和 Prometheus:

@Aspect
@Component
public class ExceptionMetricsAspect {
    private final MeterRegistry meterRegistry;

    @AfterThrowing(pointcut = "execution(* com.example..*.*(..))", throwing = "e")
    public void recordExceptionMetrics(Exception e) {
        String exceptionType = e.getClass().getSimpleName();
        meterRegistry.counter("app.exceptions.total", "type", exceptionType).increment();
        
        // 严重异常触发告警
        if (e instanceof SQLException || e instanceof IOException) {
            // 发送告警通知
        }
    }
}

线上问题排查指南

异常日志规范

  • 业务异常:WARN 级别,记录错误码和消息
  • 系统异常:ERROR 级别,记录完整堆栈
  • 敏感操作:需记录用户 ID 和操作内容

排查流程

  • 根据响应的错误码定位业务模块
  • 搜索日志中的errorDetails字段
  • 系统异常查看完整堆栈的根源

常见问题解决

  • 异常未被捕获:检查是否是 checked exception
  • 响应格式不对:确认 @ResponseBody 注解是否生效
  • 错误码冲突:建立团队共享的错误码文档

完整项目结构

com.example.exception
├── base/
│   ├── BaseException.java       // 基础异常类
│   └── ApiResponse.java         // 统一响应模型
├── business/
│   ├── UserException.java       // 用户相关异常
│   └── OrderException.java      // 订单相关异常
├── handler/
│   ├── GlobalExceptionHandler.java // 全局异常处理器
│   └── ExceptionMetricsAspect.java // 异常监控切面
└── ErrorCode.java               // 错误码枚举

这套方案已经在我们公司的三个 Spring Boot3 项目中落地,稳定性和可维护性都得到了验证。实际使用时,提议根据团队规模和业务复杂度进行适当调整。如果大家在实施过程中遇到具体问题,欢迎在评论区交流讨论。

© 版权声明

相关文章

暂无评论

none
暂无评论...