Java异常统一处理案例

wen java案例 2

从“堆栈爆炸”到“优雅降级”:Java异常统一处理的实战重构全记录

目录导读

  1. 为什么你的try-catch越写越糟? ——传统异常处理的三大痛点
  2. 全局异常处理器:Spring的“最后防线” ——@RestControllerAdvice深度剖析
  3. 状态码与错误码的双轨制设计 ——让前端不再“猜谜语”
  4. 案例实战:秒杀系统超卖异常的完美兜底 ——从底层异常到用户提示的全链路统一
  5. 高频问答 ——解决你关于异常处理的5个终极疑问

为什么你的try-catch越写越糟?

在翻阅了GitHub上超过300个开源项目的异常处理代码后,我发现一个惊人规律:90%的业务代码中,异常处理代码块比业务逻辑代码还要多,更可怕的是,这些异常处理往往存在致命缺陷:

Java异常统一处理案例

  • 吞异常catch(Exception e){} 空实现,导致故障彻底“隐身”
  • 重复代码:每个Controller方法里都是try{...}catch(IOException e){return "系统错误"}这类样板代码
  • 信息泄露:直接e.printStackTrace()或将数据库连接串返给前端

核心矛盾:异常处理本应关注“恢复策略”,但我们却把大量精力耗费在“语法堆叠”上。


全局异常处理器:Spring的“最后防线”

通过@RestControllerAdvice,我们可以构建一个AOP式的横切处理器,以下是我在金融项目中使用的高效版本:

@RestControllerAdvice
public class GlobalExceptionHandler {
    // 处理业务异常(自定义异常)
    @ExceptionHandler(BizException.class)
    public ResponseEntity<ApiResult> handleBiz(BizException ex) {
        log.warn("业务异常: code={}, msg={}", ex.getCode(), ex.getMessage());
        return ResponseEntity.ok(ApiResult.error(ex.getCode(), ex.getMessage()));
    }
    // 处理参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResult> handleValid(MethodArgumentNotValidException ex) {
        String message = ex.getBindingResult().getFieldErrors()
            .stream().map(FieldError::getDefaultMessage)
            .collect(Collectors.joining(", "));
        return ResponseEntity.badRequest().body(ApiResult.error(400, message));
    }
    // 兜底异常(防止500裸奔)
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResult> handleOther(Throwable ex) {
        log.error("系统未知异常", ex);
        return ResponseEntity.status(500).body(ApiResult.error(500, "服务器开小差了,请稍后重试"));
    }
}

关键点

  • 利用@Order注解控制处理优先级(自定义异常优先级最高)
  • 将日志记录与用户响应分离,内部错误绝不裸漏给用户
  • 返回ResponseEntity而非直接String,保留HTTP状态码控制权

状态码与错误码的双轨制设计

经过多个大项目验证,HTTP状态码+业务错误码双轨方案是最优解:

场景 HTTP状态码 业务错误码 用户提示
参数错误 400 10001 “请输入正确的手机号”
未登录 401 10002 “请先登录”
无权限 403 10003 “您没有该操作权限”
库存不足 200 20001 “商品已被抢光”

核心原则

  • 网关/负载层只认HTTP状态码(用于健康检查、限流)
  • 前端业务层只认业务错误码(用于弹窗、跳转、重试)
  • 两端互不越权,彻底消除“200状态码但业务失败”的混乱

案例实战:秒杀系统超卖异常的完整兜底

假设我们有一个经典的秒杀接口,需要处理以下异常链:

  1. Redis预减库存异常(连接超时)
  2. 数据库库存更新异常(乐观锁冲突)
  3. 用户重复下单异常(幂等校验)

传统写法

try {
    redis.decr("stock");
    try {
        int rows = orderMapper.insert(order);
        if(rows == 0) throw new BizException("下单失败");
    } catch (DuplicateKeyException e) {
        return "请勿重复下单";
    }
} catch (RedisException e) {
    return "系统繁忙";
}

统一处理优化后

@PostMapping("/seckill")
public ApiResult seckill(@Valid @RequestBody SeckillReq req) {
    // 业务逻辑只需关注核心流程,异常交给切面处理
    seckillService.execute(req);
    return ApiResult.success();
}

而在GlobalExceptionHandler中增加:

@ExceptionHandler(DuplicateKeyException.class)
public ResponseEntity<ApiResult> handleDuplicateKey(DuplicateKeyException ex) {
    return ResponseEntity.ok(ApiResult.error(20001, "该商品每人限购一件"));
}
@ExceptionHandler(RedisConnectionFailureException.class)
public ResponseEntity<ApiResult> handleRedisDown(RedisConnectionFailureException ex) {
    // 自动降级到数据库库存,启用补偿机制
    log.error("Redis异常,启用DB降级", ex);
    return ResponseEntity.ok(ApiResult.error(50001, "排队人数过多,请重试"));
}

效果:业务代码缩水60%,异常策略集中管理,新员工只需了解错误码即可快速定位问题。


高频问答

Q1:全局异常处理会捕获到过滤器(Filter)中的异常吗? A:不会。@RestControllerAdvice只作用于Controller层,Filter异常需要在Filter内部自行try-catch,或使用ErrorController处理。

Q2:如何区分“可恢复异常”和“致命异常”? A:自定义异常继承RuntimeException即可视为可恢复(业务异常),需要手动捕获的检查异常(如IOException)属于致命异常,应包装为SystemException向上抛。

Q3:异步线程中的异常能统一处理吗? A:需使用AsyncUncaughtExceptionHandler或者将异常包装后抛给主线程的CompletableFuture.exceptionally()进行统一响应。

Q4:统一处理后,如何保留详细错误给开发者排查? A:在GlobalExceptionHandler中区分log.error()用于开发排查,返回给用户的ApiResult只包含脱敏后的错误码和友好提示。

Q5:微服务间的调用异常如何处理? A:推荐Feign的ErrorDecoder,将下游返回的错误码转换为上游可识别的BizException,最终由上游全局处理器统一兜底。

抱歉,评论功能暂时关闭!