
从踩坑到避坑:Java异常捕获的12个典型错误
先说个真实案例:去年帮朋友公司排查一个支付系统的bug,用户支付后偶尔显示“支付失败”,但钱却被扣了。查日志发现,关键代码里有这么一段:
try {
// 调用支付接口
paymentService.pay(orderId, amount);
} catch (Exception e) {
// 忽略异常,返回失败
return "支付失败";
}
就因为这个“万能catch(Exception)”,把支付接口抛出的“网络超时异常”和“余额不足异常”全吞了——用户其实支付成功了,只是接口超时,系统却返回失败,钱也没退。后来我们在catch块里加了日志打印,才发现问题根源。这就是异常捕获第一个坑:捕获范围过大,把不该抓的异常也抓了。
最容易踩的3类基础错误
你刚开始写代码时,是不是也觉得“只要用try-catch包起来,程序就不会崩了”?其实这是最大的误区。我 了项目里最常见的12个错误,先说说3个基础级的“高频坑”:
第一个是“静默处理”异常,就是catch块里啥都不做,既不打印日志也不抛出。比如:
try {
fileReader.read();
} catch (IOException e) {
// 空catch块,相当于把异常“吃了”
}
我之前维护过一个老系统,里面有30多个这样的空catch块。有次服务器磁盘满了,fileReader.read()抛了IOException,但因为被吞了,系统没报错,只是数据没更新——排查了3天才发现是这个原因。记住:异常就像身体的“疼痛信号”,你把它关掉,不是病好了,而是错过了治疗时机。
第二个是日志打印不规范。很多人习惯写e.printStackTrace()
,但这在生产环境是大忌——它会把日志打到控制台,而不是日志文件,而且可能被其他线程的日志覆盖。正确的做法是用日志框架(比如Logback、Log4j)打印,并且带上完整堆栈:log.error("读取文件失败", e)
。我带团队时,强制要求日志必须包含“场景描述+异常对象”,比如“订单号123支付失败”,这样定位问题时直接搜订单号就能找到日志。
第三个是捕获后不处理也不抛出。比如:
try {
// 数据库操作
} catch (SQLException e) {
log.error("数据库异常", e);
// 既不重试也不通知,程序继续往下走
}
// 继续使用可能未正确初始化的数据
有次电商大促,就因为这段代码:数据库连接超时后,catch块只打了日志,没中断流程,导致后面用了空数据生成订单——造成100多笔“幽灵订单”。正确的做法是:如果你不能处理这个异常(比如重试、切换备用库),就别捕获它,让它往上抛,交给能处理的层(比如全局异常处理器)。
进阶坑:架构级的异常处理误区
除了基础错误,架构层面也有几个“隐形坑”,尤其在中大型项目里容易犯。
一个是滥用try-catch嵌套。见过最夸张的代码,一个方法里套了5层try-catch,像“俄罗斯套娃”。比如:
try {
// 业务逻辑A
try {
// 业务逻辑B
try {
// 业务逻辑C
} catch (Exception e) {}
} catch (Exception e) {}
} catch (Exception e) {}
这种代码可读性差到极点,而且异常发生时,你根本分不清是哪一层抛的。我现在要求团队:一个方法最多1个try-catch块,复杂逻辑拆成小方法,用“单一职责”原则减少嵌套——亲测这样改完,代码可读性至少提升40%。
另一个是自定义异常设计混乱。有的项目里自定义异常比Java自带的还多,而且层级混乱:BaseException
、BusinessException
、OrderException
、PayException
……最后谁也分不清该用哪个。其实自定义异常要“少而精”,我通常 分3类就够了:业务异常(用户操作错误,比如“余额不足”)、系统异常(比如数据库连接失败)、第三方异常(比如调用微信支付接口失败)。这样既能区分错误类型,又不会增加维护成本。
为了让你更直观避坑,我整理了一个“错误对比表”,看看你有没有中招:
错误类型 | 错误示例 | 问题后果 | 正确做法 |
---|---|---|---|
空catch块 | catch块无代码 | 异常丢失,无法排查 | 至少打印日志:log.error(“描述”, e) |
捕获范围过大 | catch (Exception e) | 掩盖真正错误(如NullPointerException) | 捕获具体异常(如IOException) |
滥用e.printStackTrace() | e.printStackTrace() | 日志丢失、线程安全问题 | 用日志框架:log.error(“信息”, e) |
(表中3个常见错误的对比,你可以对照自己的代码检查下)
高效异常处理:从代码到架构的实战技巧
避开坑只是基础,真正的高手能把异常处理变成“系统稳定性的保护伞”。这部分我结合自己重构项目的经验,从代码细节到架构设计,分享几个亲测有效的技巧。
代码层:try-with-resources和异常链传递
先说两个写代码时就能用上的“小技巧”,但效果立竿见影。
第一个是用try-with-resources自动关闭资源。Java 7之后提供的这个语法糖,能帮你避免“资源泄漏”——我见过太多因为忘记关流导致的文件句柄耗尽问题。比如读取文件,传统写法是:
FileReader fr = null;
try {
fr = new FileReader("file.txt");
fr.read();
} catch (IOException e) {
log.error("读取失败", e);
} finally {
if (fr != null) {
try {
fr.close(); // 关闭资源也要try-catch,代码臃肿
} catch (IOException e) {}
}
}
而try-with-resources只要一行:
try (FileReader fr = new FileReader("file.txt")) {
fr.read();
} catch (IOException e) {
log.error("读取失败", e);
}
资源会自动关闭,不用写finally——我在团队里推广这个后,资源泄漏的bug减少了70%。记得:所有实现了AutoCloseable接口的类(比如InputStream、OutputStream、Connection),都要用try-with-resources。
第二个是异常链传递。有时候你需要在捕获异常后,包装成新的异常抛出,但别丢了原始异常。比如:
try {
userService.updateUser(user);
} catch (SQLException e) {
// 包装成业务异常,并保留原始异常
throw new BusinessException("更新用户失败", e);
}
这里的e
就是“ cause”(原因异常),打印日志时会显示完整的异常链,像快递单号跟踪一样,从“业务异常”追到“SQL异常”,再到“数据库连接超时”——这是定位复杂问题的“神器”。Oracle的官方文档也强调:传递异常链是“保持诊断信息完整”的最佳实践(Oracle Java异常处理指南{:target=”_blank” rel=”nofollow”})。
架构层:自定义异常+全局异常处理
当项目规模超过10万行代码,光靠代码细节就不够了,需要从架构层面设计异常处理机制。我推荐“自定义异常+全局异常处理器”的组合,这是我在多个中大型项目里验证过的方案。
先说说自定义异常的设计。前面提到过别搞太多异常类,我通常分3种:
BusinessException
:业务逻辑错误,比如“用户不存在”“余额不足”,前端需要展示给用户 SystemException
:系统级错误,比如“数据库连接失败”“缓存服务不可用”,需要告警通知开发 ThirdPartyException
:调用第三方接口失败,比如“微信支付接口超时”,可能需要重试 这3个异常都继承自BaseException
,包含错误码和错误信息。比如:
public class BusinessException extends BaseException {
// 错误码(如BIZ_001)和错误信息(如“用户不存在”)
public BusinessException(String code, String message) {
super(code, message);
}
}
然后用全局异常处理器统一处理。Spring框架里可以用@ControllerAdvice,把所有异常处理逻辑集中到一个类里,不用在每个Controller里写try-catch。比如:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity handleBusinessException(BusinessException e) {
// 返回给前端:错误码+用户友好信息
return ResponseEntity.status(400).body(Result.fail(e.getCode(), e.getMessage()));
}
@ExceptionHandler(SystemException.class)
public ResponseEntity handleSystemException(SystemException e) {
// 记录错误日志,并发送告警(比如钉钉、邮件)
log.error("系统异常", e);
sendAlarm("系统异常:" + e.getMessage());
return ResponseEntity.status(500).body(Result.error("系统繁忙,请稍后再试"));
}
}
我在电商项目里用了这个架构后,代码整洁度提升了不少,而且异常处理逻辑改起来也方便——比如要加一个新的异常类型,只需要加一个@ExceptionHandler方法。
最后再分享一个“笨办法”但有效:写代码时,每次用try-catch前,先问自己3个问题:
想清楚这3个问题,你写的异常处理代码就不会“踩坑”了。
你最近项目里有没有遇到异常处理的问题?或者用过什么好用的技巧?比如自定义异常的设计、全局异常处理器的实现,欢迎在评论区告诉我,咱们一起避坑!
你有没有遇到过这种情况:线上系统报错了,你赶紧登录服务器想找日志,结果翻遍日志文件都找不到异常信息,最后发现异常居然只打在了控制台?这十有八九就是用了e.printStackTrace()的锅。我之前维护过一个老项目,里面有个同事特别喜欢用这个,有次线上订单支付失败,我们查了半天日志文件都没线索,后来登录服务器看控制台历史记录,才发现异常信息孤零零躺在那儿——控制台就像电脑屏幕上的临时窗口,日志打在这里,服务器一重启就没了,生产环境哪有人天天盯着控制台看?而且现在项目大多部署在容器里,控制台日志默认不会持久化,等你发现问题时早就被覆盖了,等于白打印。
更坑的是多线程环境下,e.printStackTrace()还会让日志“打架”。我之前做过一个秒杀系统,用了20个线程处理请求,有次接口报错,控制台里的异常日志简直是“一锅粥”——线程A的异常堆栈刚打一半,线程B的异常信息就插进来了,前后半句都对不上,根本看不出完整的错误路径。后来换成日志框架的log.error(“秒杀接口异常”, e),才算解决问题:日志会乖乖写到指定的文件里,每个异常都带着完整的堆栈信息,线程名、时间戳清清楚楚,就像给每个异常发了个“带编号的快递”,查问题时按时间戳一捋就明白了。现在我带团队写代码,只要看到e.printStackTrace()就打回去重改,这习惯虽然“较真”,但确实少踩了很多日志找不到的坑。
什么时候应该捕获异常(try-catch),什么时候应该抛出异常(throw)?
这取决于你是否能在当前层处理异常。如果能处理(比如重试、切换备用方案、返回用户友好提示),就捕获;如果处理不了(比如数据库连接失败需要通知运维),就抛出,让上层或全局处理器处理。比如业务层遇到“用户不存在”,可以捕获并返回“用户不存在”提示;但如果是“数据库连接超时”,就应该抛出,让全局处理器记录日志并告警。
为什么不 用e.printStackTrace()打印异常?
e.printStackTrace()有两个大问题:一是日志会直接输出到控制台,而不是项目的日志文件,生产环境中很难收集;二是多线程环境下,不同线程的异常日志可能会互相覆盖,导致日志混乱。 用日志框架(如Logback、Log4j)的log.error(“描述信息”, e),这样日志会写入文件,且保留完整堆栈,方便定位问题。
try-with-resources只能用于文件操作吗?还有哪些场景适用?
不是,只要类实现了AutoCloseable接口,都能用try-with-resources。常见的除了文件流(FileReader、FileWriter),还有数据库连接(Connection)、网络连接(Socket)、输入输出流(InputStream、OutputStream)等。比如操作数据库时,用try-with-resources包裹Connection、Statement,可以自动关闭连接,避免连接泄漏导致的性能问题。
自定义异常有什么好处?项目里直接用Java自带的异常不行吗?
自带异常(如NullPointerException、IOException)太笼统,无法区分“业务错误”和“系统错误”。自定义异常可以加错误码、业务标识,方便定位问题。比如用BusinessException(“BIZ_001”, “用户不存在”),一眼就知道是业务层的“用户不存在”错误;而SystemException(“SYS_002”, “缓存连接失败”),明确是系统层的缓存问题。我之前的项目用自定义异常后,日志里直接搜错误码,定位速度快了一倍。
全局异常处理器会影响性能吗?什么项目适合用?
性能影响可以忽略,它本质是用AOP思想集中处理异常,不会增加太多开销。中小型项目用了能减少重复代码(不用每个Controller写try-catch),大型项目用了能统一规范(日志格式、返回码、告警策略)。我带团队时,所有Spring Boot项目都标配全局异常处理器,代码整洁度和问题响应速度都提升了不少。