优雅应对异常,从“try-catch堆砌”到“设计驱动”

一、引言:我大意了,没有闪!

你有没有过这种经历?

  • 写了一堆 try-catch,结果上线后还是被用户投诉"点一下就白屏"?
  • 日志里突然冒出一堆 NullPointerException,你一脸懵:"这代码我写了三年,它怎么敢的啊?"
  • 第三方接口突然挂了,你的服务直接跟着"躺平",连个像样的提示都没有。

看下面几张图:

真实有吐血的感觉,程序若有生命,高低得来一句:来,骗!来,偷袭我这三五年的老程序!我大意了,没有闪!

其实,问题不在你写得不够快,而在异常处理没想清楚。今天我们就用一个用户注册的例子,把"救火式编码"变成"稳如老狗式架构"。

二、真实场景:用户注册,步步惊心

我们要实现一个简单的注册功能,流程如下:

  1. 校验邮箱格式
  2. 检查邮箱是否已存在
  3. 调用短信服务发验证码
  4. 保存用户到数据库

看起来平平无奇,但每一步都可能"翻车":

  • 用户输了个 123@,邮箱格式不对
  • 邮箱被人注册过了
  • 短信服务商限流了,返回"请求太频繁"
  • 数据库连接突然断了(别问,问就是运维在重启)

如果全靠 e.printStackTrace(),那系统就像纸糊的------风一吹就散。

三、我们的目标:稳住!

设计思路三句话:

  1. 业务错误 ≠ 系统崩溃:邮箱重复是正常业务场景,不是 bug!
  2. 前端要看得懂:不能返回"java.lang.NullPointerException",得说"亲,邮箱已被占用哦~"
  3. 统一出口,集中管理:别在每个方法里写 catch,搞个"异常接待处"!

分层异常流向示意图:

四、动手写代码:show you the code!

第一步:定义统一返回格式

java 复制代码
public class ApiResponse<T> {
    private int code;
    private String message;
    // 200 表示成功,其他为业务错误码
    private T data;

    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(200, "操作成功", data);
    }

    public static <T> ApiResponse<T> error(String msg) {
        return new ApiResponse<>(500, msg, null);
    }

    // 构造函数、getter/setter 略
}

第二步:自定义业务异常(重点!)

java 复制代码
public class BusinessException extends RuntimeException {
    private final String errorCode; // 比如 "EMAIL_EXISTS"

    public BusinessException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public String getErrorCode() { return errorCode; }
}

这样前端就能根据 errorCode 做不同提示,比如弹窗 or 跳转。

第三步:Service 层只抛"有意义"的异常

java 复制代码
@Service
public class UserService {

    public void register(String email, String phone) {
        if (!isValidEmail(email)) {
            throw new BusinessException("INVALID_EMAIL", "邮箱格式不正确,请检查");
        }
        if (userRepository.existsByEmail(email)) {
            throw new BusinessException("EMAIL_EXISTS", "该邮箱已经注册啦~");
        }

        try {
            smsService.send(phone);
        } catch (SmsTimeoutException e) {
            // 第三方异常 → 转为业务异常,隐藏技术细节
            throw new BusinessException("SMS_TIMEOUT", "短信发送超时,请稍后再试");
        }

        userRepository.save(new User(email, phone));
    }

    private boolean isValidEmail(String email) {
        return email != null && email.matches("^[\\w.-]+@([\\w-]+\\.)+[\\w-]{2,}$");
    }
}

第四步:全局异常处理器(终极保险)

java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    // 捕获业务异常 → 直接返回给前端
    @ExceptionHandler(BusinessException.class)
    public ApiResponse<?> handleBusiness(BusinessException e) {
        return ApiResponse.error(e.getMessage());
    }

    // 捕获"意外惊喜"(比如 NPE、DB 断连)
    @ExceptionHandler(Exception.class)
    public ApiResponse<?> handleUnexpected(Exception e) {
        log.error("【系统异常】请程序员速来救火!", e); // 记日志,带堆栈
        return ApiResponse.error("系统开小差了,请稍后再试~");
    }
}

现在,哪怕数据库崩了,用户看到的也是温暖提示,而不是一串红色报错。

五、进阶玩法:自动重试?安排!

有时候异常是"暂时的",比如网络抖动。我们可以用责任链模式自动处理:

代码示意:

java 复制代码
public class RetryHandler implements ExceptionHandler {
    public void handle(Exception e, Context ctx) {
        if (e instanceof SmsServiceException && ctx.retryCount < 3) {
            ctx.retry(); // 自动重试
        } else if (next != null) {
            next.handle(e, ctx); // 交给降级处理器
        }
    }
}

这样,用户根本感觉不到"失败",系统自己默默重试+兜底,稳得一批!

六、总结:异常处理三字经

  • 不吞异常 :空 catch 是埋雷,迟早炸
  • 不露细节 :别让前端看到 at com.xxx...
  • 要分层:校验异常、业务异常、系统异常,各回各家

记住:好代码不是不抛异常,而是知道异常来了怎么办。


老兄我最后送大家一句话:年轻人不要太气盛!

搞错了,再来:

写代码如打拳,异常处理就是你的"闪避技能"。练好了,任他风吹雨打,我自岿然不动!


欢迎大家关注我,一起交流。

相关推荐
勇哥java实战分享16 小时前
程序员的明天:AI 时代下的行业观察与个人思考
后端
掘金码甲哥18 小时前
超性感的轻量级openclaw平替,我来给你打call
后端
用户83562907805121 小时前
无需 Office:Python 批量转换 PPT 为图片
后端·python
啊哈灵机一动21 小时前
使用golang搭建一个nes 模拟器
后端
日月云棠1 天前
各版本JDK对比:JDK 25 特性详解
java
间彧1 天前
SpringBoot + ShardingSphere 读写分离实战指南
后端
砍材农夫1 天前
订单超时
后端
树獭叔叔1 天前
06-大模型如何"学习":从梯度下降到AdamW优化器
后端·aigc·openai
用户8307196840821 天前
Spring Boot 项目中日期处理的最佳实践
java·spring boot
得鹿1 天前
MySQL基础架构与存储引擎、索引、事务、锁、日志
后端