批判我的异常处理策略

时间:2009-08-26 16:05:38

标签: java exception-handling

在过去7年的大部分时间里,我一直在进行面向对象编程,在此期间使用Java打开和关闭。有些事情我确信我有很好的把握,比如最有用的设计模式。实际上,下面的代码允许我在一天的时间内创建一个小系统,它将处理我们现在准备实现的一个特定实例,同时具有足够的灵活性来处理我已经被告知的未来需求:

public void importAndArchive(File detectedFile) throws FileNotFoundException, IOException {
        File workingCopy = returnWorkingCopy(detectedFile);
        List<String[]> csvData = csvData(workingCopy);
        setHeaderFields(csvData.get(0));

        importData(csvData); //subclass will implement this abstract method

        archiveWorkingCopy(workingCopy);
    }

我没有表现出上面的夸耀我对模板方法的掌握,而是作为一个起点来讨论我的能力差异,根据我的OO设计能力。这种差距是异常处理的系统方法。事实上你可以在那个方法签名中看到我暂时重新抛出一些异常,但实际上我已经在应用程序的另一个角落里根除了同样的东西。

在我走得更远之前,由于这是我第一次特别系统化的尝试,我想对我到目前为止所做的事情进行一些验证。我们的应用程序循环遍历一堆文件,“处理”它们并“归档”它们。非常标准的票价。我为了尽快推出原型而做出的一个决定如下。应用程序根据(ResourceBundled)属性文件中的数据进行初始化。 ResourceBundle API中的各种方法抛出未经检查的异常,但我暂时并没有真正处理它们,理由是它们会阻止应用程序启动,而堆栈跟踪暂时就足够了。

但我选择了一个用于CSV处理的库,它会抛出已检查的异常。然后,NetBeans很容易将这些异常扩散到一开始;)以下是我重新编写后实际处理这些异常的方法:

private String detectImportHandler(File detectedFile) throws Exception {
    String[] firstLine = null;

    try {
        /*
         * CSVReader throws checked exceptions
         */
        CSVReader csvReader = new CSVReader(new FileReader(detectedFile));
        firstLine = csvReader.readNext();
        csvReader.close();
    }
    catch(Exception x) {
        throw new Exception("CSVReader unable to process file: " + x.getMessage());
    }

    try {
        return firstLine[1];
    }
    catch(Exception x) {
        /*
         * since we're re-throwing CSVReader's checked exceptions it seems easiest
         * to also re-throw null-pointer and/or array-out-of-bounds errors
         */
        throw new Exception(
                "First line null or did not have importHandlerType field: " + x.getMessage());
    }
}

因此,在处理文件的循环中调用上述方法:

    try {
        importHandlerType = detectImportHandler(detectedFile);
    }
    catch(Exception x) {
        unusableFileErrors.put(detectedFile.getName(), x.getMessage());
        continue;
    }

unusableFileErrors是一个Map,我想当我完成对文件的迭代时,我可以使用这个地图和它包含的特定于文件的消息来处理更高级别的事情,例如记录,在某处移动文件系统上的其他东西等。

无论如何,我已经走了很长时间。我订购了这本书"Robust Java",我希望它与SO社区之间,我可以改善这个被忽视的能力方面。我在SO上看过其他类似的问题,但我认为在实际代码的上下文中请求具体的建议可能会有所帮助。

8 个答案:

答案 0 :(得分:15)

一些评论:

  1. 包装异常e时,只需使用e.getMessage()。相反,我建议你使用新的例外(消息,e)。这将设置异常的原因,因此整个堆栈跟踪还将包含原始异常的堆栈跟踪。这对于调试异常原因非常有用。

  2. 我建议您抛出更明确的异常,而不是抛出异常,例如FileNotFoundException异常。在有意义的情况下使用Java API定义的异常。

  3. 我建议您明确说明要捕获的异常,而不是捕获异常。

  4. (2的可选替换)一些开发人员更喜欢 - 而不是为每种异常定义异常类 - 抛出一个包含指示异常类型的错误键的常规异常,例如:新的DetailedException(GeneralBusinessErrors.PARSING_ERROR)。这样可以更轻松地为业务错误分配语言相关的资源文件。

  5. 将调试数据添加到异常中可能很有用。例如,如果找不到文件,则可能是您尝试打开的文件等信息。这在维护中非常有用,您可能无法使用调试器重现问题和/或进行调试。

  6. 让您的系统记录未捕获的任何抛出的未经检查的异常可能很有用。一般来说,记录抛出(捕获或未捕获)的任何异常可能很有用。

答案 1 :(得分:6)

如果我想将特定于实现的异常转换为我希望通过我的API公开的异常,我会采用catch-rethrow方法。

例如,假设您示例中的导入方法采用URL而不是File。如果此URL恰好引用File我不希望在API上公开FileNotFoundException(这是一个实现细节),而是捕获并重新抛出它(例如ImportException但仍然链接到潜在的FileNotFoundException原因(正如大卫所建议的那样)。

此外,我通常不会捕获并重新抛出未经检查的异常(例如NullPointerException),但在某些情况下(例如Spring DataAccessException)。

答案 2 :(得分:4)

已经有一些非常好的答案,但我想做出更多的考虑。

并非只有一种错误处理策略适合所有情况。

一般来说,你应该有意识地在两个根本相反的情况下做出选择。选择将根据您正在构建的内容以及您当前正在处理的应用程序中的哪个层而有所不同。两种选择是:

  1. 快速失败。立即出现错误,您收集所有信息并确保它可以到达可以对其执行操作的人 - 无论是在您的调用代码中,通过抛出异常,还是调用帮助台,谁可以在任何问题之前修复代码发生数据损坏。掩盖错误或未能获取有关它的足够信息将使得更难弄清楚出了什么问题。

  2. 要防弹。无论如何继续,因为停止将是一场灾难。当然,您仍然需要捕获有关失败的信息并以某种方式报告,但在这种情况下,您需要弄清楚如何恢复并继续进行。

  3. 引入了许多错误,因为程序员在采用第一种策略时采用了第二种策略。如果不清楚你应该如何从这种情况中恢复过来,你可能应该抛出一个例外,让任何打电话给你的人做出明智的选择 - 他们可能比你更了解做什么,但他们需要有足够的信息来决定那是什么。

    另一个有用的建议是确保你抛出的异常在API级别有意义。如果您的API旨在以html格式呈现数据,那么您不希望仅仅因为有人决定启用审计跟踪而看到抛出的数据库异常。像这样的情况通常最好使用链式异常来处理 - 例如throw new ApiException(causeException);

    最后,我想对未检查和检查的异常进行处理。我喜欢为那些意味着程序员犯了错误的情况保留未经检查的异常。例如,将null传递给需要对象引用的方法。即使程序员是完美的,也可能出现错误(例如,如果文件已被程序员检查其存在,则FileNotFound已被删除),检查异常通常是合适的。从本质上讲,创建API的程序员会说“即使你输入的所有输入都是正确的,那么这个问题可能仍然会出现,你可能不得不处理它”。

答案 3 :(得分:3)

我已经给了@Kolibri一个向上投票,所以请先阅读,但我还有一些事情要补充。

异常处理是一种黑色艺术,预期的实践从语言变为语言。在Java中,将方法签名视为方法和调用者之间的契约。

如果调用者违反了他们的合同结束(例如将“detectedFile”作为null传递),则需要抛出未经检查的异常。你的API给了他们合同,他们明确地破坏了它。他们的问题。 (忽略现在检查FileNotFoundException,这对我用Java来说是一种牛肉)

如果调用者传入了正确的数据但您无法完成他们的请求,那么就是在您抛出已检查的异常时。例如,如果由于磁盘错误(例如IOException)而无法读取其文件,则表示您违反了合同,并且抛出已检查的异常是有意义的。检查异常会向调用者发出警告,告诉他们调用方法时可能出现的所有问题。

为此,它取决于您希望用户处理异常的粒度类型。抛出“异常”通常是不可取的,因为像其他人所说的那样,它没有给你处理发生的事情的细粒度。调用者不会立即知道他们是否违反了合同或您的方法,因为Exception包括已检查和未检查的异常。此外,您的调用者可能希望在代码中以不同方式处理FileNotFoundException和IOException。如果你只是抛出异常,他们就失去了轻松做到这一点的能力。定义自己的异常可以扩展这个概念,并允许您的方法的调用者在更细微的事情中处理来自您的方法的异常。这里的关键是,如果你抛出10个不同的异常并且调用者不关心差异,他们可以只做“捕获(异常e)”并一次性捕获它们。如果你只是“抛出异常”,那么用户不再可以选择是以不同方式处理不同的异常,还是只抓住所有异常。

我也非常赞同使用“throw new XXXException(message,e)”而不是“throw new Exception(message + e.getMessage())”的注释。如果使用第一种方法,则会保持内部堆栈跟踪一直回到原始错误。如果使用第二种方法,堆栈跟踪现在只会追溯到“抛出新异常”行,并且您将被迫进行更多调试以找出导致该异常发生的原因。这在过去一直困扰着我。

最后,如果它会帮助你,不要害怕在catch中使用Exception.printStackTrace()。用户讨厌看到堆栈跟踪,你应该尽可能地处理异常,但是如果它是一个真正意想不到的异常并且有一个堆栈跟踪将帮助你将来进行大量调试,只需打印堆栈跟踪。

哇,这就像一篇文章。我现在就停下来。祝好运! :-D

=====================

附录:我刚刚抓住了@pjp的评论,并且需要重复......你应该在“catch”语句之后在“finally”块中调用“.close()”方法。否则,在异常情况下,您的方法将返回而不立即关闭您分配的资源&amp;打开。

答案 4 :(得分:3)

我认为问题的另一个方面尚未涵盖。那就是异常处理不只是关于编码技术,而是关于错误条件以及你想要做些什么。 Java的检查异常是一个混合包,因为它迫使你在你可能不想要的时候考虑错误条件,导致一些开发人员做的事情比他们希望让编译器脱离他们的方式更糟糕。 (未经检查的异常存在相反的问题 - 它会导致开发人员忘记错误处理,即使是非常常见的情况,例如网络连接断开)。

可以说有三类异常,它们对应于Java中的三种类型:Checked Exceptions,Runtime Exceptions和Errors。

错误只应在应用程序中以非常高的级别处理,通常应视为不可恢复。即使OutOFMemoryError在实践中有些可恢复,但在你没有想到的代码序列(例如对象初始化)中出现了太多错误,这些代码会使事情处于一种糟糕的状态,只有你真的可以尝试隔离代码(例如在应用程序服务器中)。

运行时异常,简而言之,应该是确定性的,并且在给定相同输入的情况下总是会出错。例如,如果将空引用传递给不允许空值的方法(NullPointerException),或者如果执行Integer.parseInt,则给定的String每次都会产生相同的结果。

检查异常,再次以简短的定义,如果出现错误,你提前无法知道的事情。 FileNotFoundException,例如,当你创建File对象时,文件可能就在那里,你甚至可以检查,但是毫秒之后会有东西将它从你的下面删除。

如果你设计并对那些类别的异常做出反应(意识到并非所有API设计者都遵循它们 - 例如,即使行为对于给定的XML输入是行为确定的,也会检查XML异常,当然Spring刚刚决定完全避免检查异常)那么你将有清楚的方法来思考你需要什么样的错误处理。当网络连接断开时,你需要做什么(当然,取决于你的项目),而不是你在JVM内存不足时所做的事情,而且两者都与你必须解析的情况非常不同整数,但不要从用户输入中得到一个。

一旦您考虑这些术语,您对异常处理所做的工作将更自然地从您尝试实现的错误处理类型和稳健性方面流动。

答案 5 :(得分:2)

小记:我建议将实际原因链接起来,而不是仅仅附加原始消息;否则你将失去堆栈跟踪,调试将比它需要的更痛苦。

除此之外:我在detectImportHandler()内的try / catch块中看不到很多价值。由于该方法已经抛出Exception,因此无需重新包装CSVReader引发的任何内容,NullPointerException将在调用try / catch中捕获,就像其他任何东西一样。

如果finally可以抛出异常,那么更有价值的可能是关闭FileReader(和/或CSVReader)的readNext()子句。或CSVReader构造函数。

答案 6 :(得分:2)

捕获并抛出普通的旧Exception是不好的 - 无论你是否想要它们(可能不是),你都会获得所有运行时异常。

我尝试抛出在它们来自的方法的上下文中有意义的已检查异常。例如,如果我有loadCustomer(long id),我会抛出ObjectLoadException而不是SQLException

是的,你读得对,我的偏好是检查异常 - 我喜欢这样一个事实:方法的使用者必须明确决定在发生异常时该做什么,它通过想象提高了可读性读取代码imho时调用堆栈。

答案 7 :(得分:1)

通常我会在两个地方之一处理异常:

  1. 如果我可以在用户不知情的情况下处理异常,我会尽可能接近错误处理它。
  2. 如果错误需要某种输出(记录,弹出对话框,向stdout写入消息),我会将其传递给某个集中点,此时它会被捕获并分发到适当的输出机制。
  3. 这对我来说似乎运作得很好。