我已经用Java编码一段时间了。但有时,我不明白什么时候应该抛出异常,什么时候应该捕获异常。我正在从事一个有很多方法的项目。层次结构是这样的——
Method A will call Method B and Method B will call some Method C and Method C will call Method D and Method E.
所以目前我正在做的是 - 我在所有方法中抛出异常并在方法 A 中捕获它,然后记录为错误。
但我不确定这是否是正确的方法?或者我应该开始在所有方法中捕获异常。所以这就是为什么这种混乱始于我的 - 我什么时候应该捕获异常与何时应该抛出异常。我知道这是一个愚蠢的问题,但不知何故,我正在努力理解这个主要概念。
有人可以给我一个详细的When to catch the Exception vs When to throw the Exceptions
例子,以便我的概念得到澄清吗?就我而言,我是否应该继续抛出异常,然后在主调用方法 A 中捕获它?
在知道该做什么的方法中时,您应该捕获异常。
例如,暂时忘记它的实际工作方式,假设您正在编写一个用于打开和读取文件的库。
所以你有一个类,比如:
public class FileInputStream extends InputStream {
public FileInputStream(String filename) { }
}
现在,假设该文件不存在。你应该怎么做?如果你正在努力想出答案,那是因为没有答案......FileInputStream
不知道如何处理这个问题。所以它把它扔到链上,即:
public class FileInputStream extends InputStream {
public FileInputStream(String filename) throws FileNotFoundException { }
}
现在,假设有人正在使用您的库。他们的代码可能如下所示:
public class Main {
public static void main(String... args) {
String filename = "foo.txt";
try {
FileInputStream fs = new FileInputStream(filename);
// The rest of the code
} catch (FileNotFoundException e) {
System.err.println("Unable to find input file: " + filename);
System.err.println("Terminating...");
System.exit(3);
}
}
}
在这里,程序员知道该怎么做,所以他们捕获异常并处理它。
两种情况下,您应该捕获异常。
1. 尽可能低的水平
这是您与第三方代码集成的级别,例如ORM工具或任何执行IO操作的库(通过HTTP访问资源,读取文件,保存到数据库,您命名)。也就是说,离开应用程序的本机代码以与其他组件交互的级别。
在此级别,可能会出现您无法控制的意外问题,例如连接失败和锁定的文件。
您可能希望通过捕获TimeoutException
来处理数据库连接故障,以便几秒钟后可以重试。访问文件时的异常也是如此,该文件可能目前被进程锁定,但在下一个时刻可用。
此方案中的准则是:
- 仅处理特定异常,例如
SqlTimeoutException
或IOException
。从不处理泛型异常(类型Exception
) - 仅当您有有意义的事情要处理时才处理它,例如重试、触发补偿操作或向异常添加更多数据(例如上下文变量),然后重新抛出它
- 不要在此处执行日志记录
- 让所有其他异常冒泡,因为它们将由第二种情况处理
2. 尽可能高的水平
这将是在将异常直接抛出给用户之前可以处理异常的最后一个位置。
您在这里的目标是记录错误并将详细信息转发给程序员,以便他们识别和更正错误。添加尽可能多的信息,记录下来,然后向用户显示道歉消息,因为他们可能对此无能为力,特别是如果它是软件中的错误。
第二种方案中的准则是:
- 处理泛型异常类
- 从当前执行上下文添加更多信息
- 记录错误并通知程序员
- 向用户道歉 尽快
- 解决
这些准则背后的原因
首先,异常表示不可逆转的错误。它们表示系统中的错误、程序员犯的错误或应用程序无法控制的情况。
在这些情况下,用户通常几乎无能为力。因此,您唯一能做的就是记录错误,采取必要的补偿措施,并向用户道歉。如果这是程序员犯的错误,最好让他们知道并修复它,努力获得更稳定的版本。
其次,try catch
块可以根据它们的使用方式屏蔽应用程序执行流。try catch
块的功能与label
及其goto
伙伴的功能类似,这会导致应用程序执行流从一个点跳到另一个点。
何时引发异常
在开发库的上下文中更容易解释。当你遇到错误时,你应该抛出,除了让你的API的使用者知道它,并让他们决定之外,你无能为力。
假设您是某个数据访问库的开发人员。当您遇到网络错误时,除了引发异常之外,您无能为力。从数据访问库的角度来看,这是一个不可逆转的错误。
当您开发网站时,这是不同的。您可能会捕获此类异常以便重试,但如果您希望从外层收到无效参数,因为它们应该在那里得到验证,则希望抛出异常。
这在表示层中再次有所不同,您希望用户提供无效的参数。在这种情况下,您只需显示友好的消息,而不是引发异常。
正如 https://roaddd.com/the-only-two-cases-when-you-should-handle-exceptions/所展示的那样
当函数遇到故障(即错误)时,应引发异常。
函数是一个工作单元,应根据故障对功能的影响将故障视为错误或其他错误。在函数 f 中,当且仅当故障阻止 f 满足其被调用方的任何前提条件、实现 f 自己的任何后置条件或重新建立 f 共同负责维护的任何不变性时,失败才是错误。
有三种不同类型的错误:
- 阻止函数满足必须调用的另一个函数的前提条件(例如,参数限制)的条件;
- 阻止函数建立自己的后置条件的条件(例如,生成有效的返回值是后置条件);和
- 阻止函数重新建立它负责维护的不变条件。这是一种特殊的后置条件,特别适用于成员函数。每个非私有成员函数的一个基本后置条件是它必须重新建立其类的不变量。
任何其他条件都不是错误,不应报告为错误。
只要函数检测到它无法自行处理的错误,并且阻止它继续任何形式的正常或预期操作,就会报告错误。
在具有足够知识来处理错误、转换错误或强制实施错误策略中定义的边界的位置(例如在主线或线程主线上)处理错误。
来源:C++编码标准:101 条规则、指南和最佳实践
一般来说,抓住可以做一些有用的事情的级别。 例如,用户正在尝试连接到某个数据库,但在方法 D 中失败。
你想如何处理它? 也许通过放置一个对话框说"对不起,无法连接到服务器/数据库"或其他什么。 是方法 A、B 或 C 创建了此 SERVER/DB 信息(例如,通过读取设置文件或请求用户输入)并尝试了连接? 这可能是应该处理异常的方法。 或者距离应该处理它的方法至少 1 个。
它确实因您的应用而异,因此这只能是非常笼统的建议。 我的大部分经验都是使用 Swing/桌面应用程序,您通常可以根据哪些类正在执行程序逻辑(例如"控制器"内容)以及谁在放置对话框(例如"视图"内容)来感受。 通常,"控制器"应该捕获异常并尝试执行某些操作。
在 Web 应用中,这可能有所不同。
一些非常骨架的代码,大多数类都不存在,我不确定数据库的 URL 是否有意义,但你明白了。 隐约摇摆...
/* gets called by an actionListener when user clicks a menu etc... */
public URL openTheDB() {
URL urlForTheDB = MyCoolDialogUtils.getMeAURL(URL somePreviousOneToFillInTheStart);
try {
verifyDBExists(urlForTheDB);
// this may call a bunch of deep nested calls that all can throw exceptions
// let them trickle up to here
// if it succeeded, return the URL
return urlForTheDB;
}
catch (NoDBExeption ndbe) {
String message = "Sorry, the DB does not exist at " + URL;
boolean tryAgain = MyCoolDialogUtils.error(message);
if (tryAgain)
return openTheDB();
else
return null; // user said cancel...
}
catch (IOException joe) {
// maybe the network is down, aliens have landed
// create a reasonable message and show a dialog
}
}
我将分享一种模式,该模式在一两个生产环境中保存了我的培根。
赋予动机
我的目标是确保在午夜试图解决 sev1 支持票证的可怜家伙(也许是我)获得一个很好的"由"错误层次结构,并完成 ID 等数据,所有这些都不会过度混乱代码。
方法
为了实现这一点,我捕获所有已检查的异常,并将它们重新作为未检查的异常抛出。然后,我在每个架构层的边界使用全局捕获(通常是抽象或注入的,因此它只写入一次)。正是在这些点上,我可以向错误堆栈添加额外的上下文,或者决定是否记录和忽略,或者使用变量引发自定义检查异常以保存任何额外的上下文。顺便说一句,我只在顶层记录错误以阻止"双重日志记录"的发生(例如 cron 作业、ajax 的 spring 控制器)
throw new RuntimeException(checked,"Could not retrieve contact " + id);
使用这种方法,GUI 或业务层的方法签名不会因为必须为数据库相关异常声明"抛出"而变得混乱。
这在现实生活中如何工作的示例:
假设我的代码的工作是更新许多保险单的自动化过程。该体系结构支持 GUI 手动触发一个策略的续订。假设评级区域的邮政编码在其中一个策略的数据库中已损坏。
我想要实现的错误日志类型的一个例子是。
日志消息:由于错误,将策略 1234 标记为手动干预:
从堆栈跟踪:续订策略 1234 时出错。正在回滚事务... 此捕获还将涵盖保存错误或生成字母等错误。
从堆栈跟踪:由:错误评级策略 1234 ...此捕获将拾取检索许多其他对象的错误,以及 NPE 等算法错误......
从堆栈跟踪:由:检索评级区域 73932 时出错
...从堆栈跟踪:由以下原因引起:JPA:字段"邮政编码"中出现意外空
您应该在尽可能低的级别处理异常。如果方法无法正确处理异常,则应抛出它。
- catch 如果您有连接到资源的方法(例如打开文件/网络)
- 如果层次结构中较高的类需要有关错误的信息,则抛出
当您想要将某些失败通知调用方的方法时,通常会引发异常。
例如,无效的用户输入、数据库问题、网络中断、文件缺失
正如其他人所说,作为一般规则,您应该在实际可以处理异常时捕获异常,否则,只需抛出它。
例如,如果您正在编写从保存文件中读取有关连接播放器的信息的代码,并且您的某个 I/O 方法抛出IOException
,那么您将希望引发该异常,并且调用 load
方法的代码将希望捕获该异常并相应地处理它(例如断开播放器, 或向客户端发送响应等)。您不想在 load
方法中处理异常的原因是,在该方法中,您无法有意义地处理异常,因此您将异常委托给调用方,希望他们可以处理它。