关于Java中异常处理的一些思考
最近好不容易刷到一个用Java写的、我感兴趣的项目,我fork了 https://github.com/wushuo894/ani-rss 这个项目,想着来学习一下大佬不用spring写的一个Java web,在调试过程中,我发现如果我的qBittorrent如果下载的种子没有人挂着,就会一直报错:
2024-12-28 22:41:15 ERROR ani.rss.task.RenameTask - 3c870c21a31ecc3343f01d1d90d45f45fcbe231f 磁力链接还在获取原数据中
java.lang.IllegalArgumentException: 3c870c21a31ecc3343f01d1d90d45f45fcbe231f 磁力链接还在获取原数据中
at cn.hutool.core.lang.Assert.lambda$notEmpty$9(Assert.java:585)
at cn.hutool.core.lang.Assert.notEmpty(Assert.java:564)
at cn.hutool.core.lang.Assert.notEmpty(Assert.java:585)
at ani.rss.download.qBittorrent.rename(qBittorrent.java:285)
at ani.rss.util.TorrentUtil.rename(TorrentUtil.java:909)
at ani.rss.task.RenameTask.run(RenameTask.java:47)
在这里,我一开始以为是抛了一次异常,又捕获了一次异常,于是便顺藤摸瓜,找到了抛出异常的代码和捕获异常的代码,随后发现其实是作者特意这样写的:
try {
TorrentUtil.rename(torrentsInfo);
TorrentUtil.notification(torrentsInfo);
if (deleteBackRSSOnly) {
continue;
}
TorrentUtil.delete(torrentsInfo);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
作者在log.error
这里打印了异常信息后,又将e打印,就有了上面我以为抛了一次异常,又捕获了一次异常的日志,看到这个后,我就想:这样显得日志信息太冗余了吧,这其实就是qBittorrent没有下载导致的,并非代码的问题,于是,我就尝试优化异常处理,将捕获异常改成了:
try {
TorrentUtil.rename(torrentsInfo);
TorrentUtil.notification(torrentsInfo);
if (deleteBackRSSOnly) {
continue;
}
TorrentUtil.delete(torrentsInfo);
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("磁力链接还在获取原数据中") || e.getMessage().contains("重命名失败")) {
log.error(e.getMessage());
} else {
log.error(e.getMessage(), e);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
我想着这样既能优化日志的显示,又能保留原有的报错格式,不会导致日志的误报,是十分“聪明”的,但是我总觉得有哪里不对劲,其一是我直接按异常信息中是否有字符串来判断是否是需要简化的日志,如果作者将异常信息改动,那我这里的代码也需要跟着改动,这就增加了代码的耦合,其二是在我看这位作者的代码看下来,感觉不会是没考虑到这件事的样子,于是,我便深入研究,发现调用rename
方法的里面,调用的是一个接口,而这个接口有三个实现类,其中有两个实现类中都有磁力链接还在获取原数据中
和重命名失败
的异常信息会抛出。这样一来,我大概能猜到作者为何不细化处理了,也许是直接将异常捕获后打印出来,能更加方便地找到是哪个实现类抛出的异常,方便程序的调试,而且异常的堆栈并不多,对于一个独立项目来说是可以接受的。
但是我又想了,这种情况应该也还是可以优化的,于是我去问了问当前地表最强的o1,他是这么说的:
在你给出的代码中
- 场景:你知道
rename(...)
方法可能抛出的IllegalArgumentException
,有些错误信息(比如“磁力链接还在获取原数据中”“重命名失败”)你希望只打印简单错误日志,不需要打印堆栈;对于其他未知或不常见的 IllegalArgumentException 则需要打印堆栈来排查问题。- 好处:可以在日志里区分“预期的(常见的)业务异常”与“非常见/严重的异常”,从而避免日志冗余。 可能存在的问题
- 依赖
e.getMessage()
的字符串匹配不够稳健
- 代码维护时,如果抛异常的实现类修改了报错信息文本,就会导致这里的逻辑被“静默”改变(比如,字符串关键词变了却没及时同步更新 catch 里的判断)。
- 代码阅读者看到
e.getMessage().contains("...")
时,会觉得比较“脆弱”,因为异常信息是给人读的,而不是给机器判断的。- 大量使用
IllegalArgumentException
而不是更合适的自定义异常
- 如果明知道这是“获取原数据未完成”或“重命名失败”这类业务情形,是否可以抛出更具语义的自定义异常,如
TorrentNotReadyException
、RenameFailException
?- 使用自定义异常可以让调用方更准确、清晰地处理,而不是通过字符串来判断。
- 异常分层、分支比较多,可读性下降
- 如果业务场景多了以后,会不断地在
if-else
中按异常信息做判断,代码可读性和可维护性都会下降。
同时,它也给出了更好的实现思路:自定义异常类和在引入错误码或枚举,我其实是想过用自定义异常类的,不过作者并没有这么做,而我要是新建一个自定义异常类的包,而且只定义我这一个异常的话,就显得和作者的代码风格格格不入了。