服务层中的已检查异常与未检查异常


问题内容

我在一个带有旧服务层的项目上工作,如果请求的记录不存在,或者由于调用者未得到授权而无法访问,则在很多地方返回null。我说的是ID要求的特定记录。例如,类似:

UserService.get(userId);

最近,我一直在努力更改此API,或者用引发异常的新API进行补充。随之而来的是关于检查与未检查的异常的争论。

从JPA /
Hibernate等所有设计师的笔记中,我建议未检查的异常可能是最合适的。我的观点是,不能合理地期望API用户从这些异常中恢复,并且在99%的情况下,我们最多只能通知应用程序用户发生了一些错误。

将运行时异常传播到通用处理机制显然会减少很多复杂性,并减少处理边缘情况异常所需的分支处理。但是,围绕这种方法有很多担忧(正确地如此)。

为什么诸如JPA /
EJB和Hibernate之类的项目的设计人员选择使用未经检查的异常模型?有很好的理由吗?有什么优点/缺点。使用这些框架的开发人员是否应该仍在处理运行时异常,例如使用适配器包装程序抛出异常的地方?

我希望这些问题的答案可以帮助我们针对我们自己的服务层做出“正确”的决定。


问题答案:

尽管我同意未经检查的异常可以使API更加方便的观点,但我认为这并不是最重要的好处。而是这样的:

抛出未经检查的异常可以帮助您避免严重的错误。

这就是为什么。鉴于十个开发人员被迫处理受检查的异常,您将获得二十种不同的策略来处理它们,其中许多是完全不合适的。以下是一些较常见的错误方法:

  • 吞。 捕获异常并完全忽略它。即使应用程序现在处于不稳定状态,也可以继续进行,好像什么都没发生。
  • 记录并吞下。 捕获异常并记录下来,以为现在我们要负责了。然后继续前进,好像什么也没发生。
  • 神秘默认。 捕获异常,然后将某些字段设置为某个默认值,通常无需告知用户。例如,如果您无法加载某些用户角色,则只需选择一个低特权角色并进行分配即可。用户想知道发生了什么。
  • 愚蠢/危险的神秘预设。 捕获异常,并将某些字段设置为某些 非常差的 默认值。我在现实生活中看到的一个示例:无法加载用户的角色,因此继续尝试并发挥最佳作用(即,给他们一个高特权角色,以免给任何人带来不便)。
  • 错误报道。 开发人员不知道异常意味着什么,因此只提出自己的想法。IOException即使建立连接与该问题无关,也会变为“无法连接到服务器”。
  • 通过广泛的掩盖和误报。 尝试通过捕获Exception或(ugh)Throwable而不是方法实际引发的两个检查的异常来清理代码。异常处理代码不尝试将资源可用性问题(例如IOExceptions)与直接代码错误(例如NullPointerException)区分开。实际上,它经常会任意选择一个例外,并将每个例外都错误地报告为此类例外。
  • 通过大量尝试掩盖,并误报。 先前策略的一种变体是将一大堆异常声明调用放在单个大try块的范围内,然后捕获其中一个,Exception或者Throwable因为没有别的东西可以处理所有抛出的异常。
  • 抽象不合适的重新抛出。 即使该异常不适合抽象,也要抛出该异常(例如,从应该隐藏资源的服务接口中抛出与资源相关的异常)。
  • 重新包装而不包裹。 抛出一个异常(未检查或抽象适当的检查),但只需删除嵌套的异常,该异常将使任何人都有机会真正了解正在发生的情况。
  • 戏剧性的反应。 通过退出JVM来响应非致命异常。(感谢这篇博客文章。)

以我的经验,看到上面的方法比看到正确的答案要普遍得多。许多开发人员,甚至是“高级”开发人员,都有这样的想法:必须不惜一切代价抑制异常,即使这意味着在不稳定的状态下运行应用程序。这是危险的错误。

未经检查的异常有助于避免此问题。不知道如何处理异常的开发人员倾向于将异常视为要克服的不便之处,并且他们不会全力以赴地捕捉异常。因此,异常只会冒泡地到达顶部,在那里它们提供了堆栈跟踪,并可以以一致的方式对其进行处理。在极少数情况下,实际上要做的事要比使异常冒出来要好得多,没有什么可以阻止您的。