优雅应对异常,从“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...
  • 要分层:校验异常、业务异常、系统异常,各回各家

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


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

搞错了,再来:

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


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

相关推荐
波波00718 小时前
ASP.NET MVC 中的返回类型全集详解
后端·asp.net·mvc
菜鸡儿齐18 小时前
Unsafe方法学习
java·python·学习
汤姆yu18 小时前
IDEA接入Claude Code保姆级教程(Windows专属+衔接前置安装)
java·windows·intellij-idea·openclaw·openclasw安装
prince0521 小时前
用户积分系统怎么设计
java·大数据·数据库
96771 天前
理解IOC控制反转和spring容器,@Autowired的参数的作用
java·sql·spring
SY_FC1 天前
实现一个父组件引入了子组件,跳转到其他页面,其他页面返回回来重新加载子组件函数
java·前端·javascript
糟糕好吃1 天前
我让 AI 操作网页之后,开始不想点按钮了
前端·javascript·后端
耀耀_很无聊1 天前
09_Jenkins安装JDK环境
java·运维·jenkins
ノBye~1 天前
Centos7.6 Docker安装redis(带密码 + 持久化)
java·redis·docker
黑臂麒麟1 天前
openYuanrong:多语言运行时独立部署以库集成简化 Serverless 架构 & 拓扑感知调度:提升函数运行时性能
java·架构·serverless·openyuanrong