星云点击:星空遥控器
120.47M · 2026-02-04
刚入行那会儿,我写 Java 代码有个“强迫症”:
每个方法都包一层 try-catch,生怕程序崩。
public User getUserById(Long id) {
try {
return userMapper.selectById(id);
} catch (Exception e) {
log.error("查询用户失败", e);
return null;
}
}
看起来很负责?其实这是在给系统埋雷。
直到去年一个线上事故,我才彻底醒悟:乱用 try-catch,比不处理异常更危险。
我们有个订单导出功能,逻辑大概是:
因为 getUserById 在异常时返回 null,而下游代码没判空,直接调用了 user.getName() —— 结果 NPE,整个导出任务中断。
更要命的是,因为异常被“吃掉”了,日志里只有一行模糊的 “查询用户失败”,根本不知道是哪个订单、什么参数导致的。运维翻了半小时日志才定位到问题。
客户那边,几百个商户的对账单全卡住,差点终止合作。
异常是系统的“警报器”。你把它 catch 了又不做有效处理(比如返回 null 或空字符串),等于把警报关了,但火还在烧。
上游调用者以为方法执行成功了(毕竟没抛异常),结果拿到一个 null,后续逻辑全乱套。这比直接抛异常更难排查。
如果你用 APM(比如 SkyWalking、Arthas)或 ELK 做异常监控,被吞掉的异常根本不会上报。等你发现时,可能已经影响成千上万用户。
我的原则就一条:只在你能真正“处理”异常的地方 catch 它。
什么叫“能处理”?举几个例子:
除此之外,别 catch,让它往上抛!
Spring Boot 里,用 @ControllerAdvice 全局处理异常,既干净又可控:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException e) {
return ResponseEntity.status(404).body(new ErrorResponse(e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpected(Exception e) {
log.error("系统异常", e); // 这里才该打 error 日志!
return ResponseEntity.status(500).body(new ErrorResponse("服务暂时不可用"));
}
}
这样,Service 层完全不用写 try-catch,专注业务逻辑。出问题时,日志清晰、响应规范、监控也能抓到。
比如读取配置文件失败,你可以自定义一个 ConfigLoadException,并在方法签名 throws 出去。调用方必须面对它,而不是假装没事发生。
如果真要在中间层记录日志(比如重要业务入口),一定要带上关键参数:
try {
processOrder(orderId);
} catch (Exception e) {
log.error("处理订单失败, orderId={}", orderId, e); // 关键!
throw e; // 别吞掉,继续抛
}
这样排查时,直接搜 orderId 就能找到完整链路。
见过太多人这么写:
catch (Exception e) { ... }
这会把 NullPointerException、IllegalArgumentException 这类本该快速失败的编程错误也吞掉。
正确的做法是:
RuntimeException,别用 Exception。删掉那些无意义的 try-catch 后,我们的代码:
作为不想打工的码农,我们接项目、做外包,最怕的就是半夜被叫起来修“莫名其妙”的 bug。
而很多“莫名其妙”,其实都是当初为了“省事”埋下的。
真正的健壮,不是不让程序出错,而是让错误暴露得足够快、足够清楚。