关于隔离中的异常处理,有许多众所周知的最佳实践。我知道“做与不做”已经足够了,但是在大型环境中的最佳实践或模式方面,事情变得复杂了。 “提早扔,迟到”-我已经听到很多次了,但仍然让我感到困惑。
如果在低层抛出了空指针异常,为什么我应该提早扔并迟到?为什么要在更高层次上进行捕获?对于我来说,在较高级别(例如业务层)捕获低级异常对我来说没有任何意义。似乎违反了每一层的考虑。
想象以下情况:
我有一个计算图形的服务。为了计算图形,服务访问存储库以获取原始数据,并访问其他一些服务以准备计算。如果在数据检索层出现问题,为什么我应该将DataRetrievalException抛出更高级别?相比之下,我更喜欢将异常包装为有意义的异常,例如CalculationServiceException。
为什么要提早抛出,为什么要迟到?

评论

“赶上很晚”的想法是,尽早赶上是有用的,而不是尽可能早地赶上。例如,如果您有文件解析器,那么就没有必要处理找不到的文件。您打算做什么,恢复路径是什么?没有一个,所以不要抓。转到词法分析器,您在那里做什么,如何从中恢复以使程序可以继续?不能,让异常通过。您的扫描仪如何处理?它不能,让它过去。调用代码如何处理呢?它可以尝试其他文件路径或警告用户,因此请抓住。

在极少数情况下应该捕获到NullPointerException(我假设这就是NPE的意思)。如果可能的话,应该首先避免它。如果您遇到NullPointerExceptions,那么您需要修复一些残破的代码。这也很容易解决。

伙计们,在建议重复关闭此问题之前,请确保对其他问题的回答不能很好地回答此问题。

(Citebot)今天。java.net/ article / 2003/11/20 /…如果这不是报价的来源,请提供您认为最可能是原始报价的来源的引用。

这仅是对任何一个提出此问题并进行Android开发的人的提醒。在Android上,必须在本地捕获和处理异常-使用与首次捕获相同的功能。这是因为异常不会在消息处理程序之间传播-如果发生这种情况,您的应用程序将被杀死。因此,在进行Android开发时,您不应引用此建议。

#1 楼

以我的经验,最好在发生错误的地方抛出异常。这样做是因为您最了解触发异常的原因。
当异常展开后,捕获和重新抛出是向异常添加其他上下文的好方法。这可能意味着抛出不同类型的异常,但是在执行此操作时会包含原始异常。

最终,异常将到达一个层,您可以在该层上对代码流进行决策(例如,提示用户采取行动)。这是最终应处理异常并继续正常执行的地方。明智的做法是最终解决错误。

Catch→重新抛出

执行此操作,可以在有用处添加更多信息,从而节省了开发人员必须遍历所有层次的知识。了解问题。

抓住→处理

执行此操作,从而可以最终决定通过软件的合适但不同的执行流程。

Catch→错误返回

在某些情况下,这是适当的,但应考虑捕获异常并将错误值返回给调用方,以便重构为Catch→Rethrow实现。

评论


是的,我已经知道并且毫无疑问,我应该在错误产生的地方抛出异常。但是为什么我不应该抓住NPE而是让它爬上Stacktrace?我总是会抓住NPE并将其包装为有意义的异常。我也看不出为什么我应该向服务或ui层抛出DAO-Exception的任何优势。我总是会在服务层捕获它,并将其包装到带有其他详细信息的服务异常中,为什么调用该服务失败。

– shylynx
2014年3月3日13:40

@shylynx捕获异常,然后重新抛出更有意义的异常是一件好事。您不应该做的是太早捕获异常,然后再不将其抛出。俗语警告的错误是过早捕获异常,然后尝试在代码的错误层处理它。

–西蒙B
2014年3月3日14:27

在接收到异常时使上下文变得明显,使团队中的开发人员的工作更加轻松。 NPE需要进一步调查以了解问题所在

–迈克尔·肖(Michael Shaw)
2014年3月3日14:27

@shylynx一个人可能会问这个问题:“为什么在代码中有一点可以引发NullPointerException?为什么不检查null并尽快引发异常(也许是IllegalArgumentException),以便调用者确切地知道错误的null从何而来?通过了?”我相信这将是俗语中“提早投入”部分所暗示的内容。

– jpmc26
14 Mar 4 '14 at 0:20



@jpmc我仅以NPE为例来强调有关层和异常的关注。我也可以用IllegalArgumentException替换它。

– shylynx
2014年3月4日在7:20

#2 楼

您希望尽快引发异常,因为这样可以更轻松地找到原因。例如,考虑一个可能因某些参数而失败的方法。如果您验证参数并在方法的开头失败,您将立即知道错误在于调用代码中。如果等到失败之前需要使用参数,则必须遵循执行过程,然后确定错误是否在调用代码中(错误的参数)或方法是否存在错误。越早抛出异常,它就越接近其根本原因,就越容易找出问题出在哪里。

之所以在较高级别上处理异常,是因为较低级别没有处理异常不知道处理错误的适当措施是什么。实际上,根据调用代码的不同,可能有多种适当的方法来处理同一错误。以打开文件为例。如果您试图打开一个配置文件,但该文件不存在,则可以忽略该异常并继续使用默认配置。如果要打开对程序的执行至关重要的私有文件,但由于某种原因而丢失了,那么您唯一的选择可能就是关闭程序。

评论


+1可以清楚地说明不同级别的重要性。关于文件系统错误的出色示例。

–胡安·卡洛斯·科托(Juan Carlos Coto)
15年3月6日在21:36

好的答案,我真的很喜欢该文件系统示例。您能告诉我一些很好的资源来遵循这些原则吗?

–苏拉杰(Suraj Jain)
19/12/25在7:09



#3 楼

其他人则很好地总结了为什么提早抛出。让我集中讨论为什么迟到,为什么我的口味没有令人满意的解释。

为什么会有例外?

似乎有首先为什么会存在异常,这很令人困惑。让我在这里分享一个大秘密:异常的原因和异常处理是...摘要。

您是否看到过这样的代码:

 static int divide(int dividend, int divisor) throws DivideByZeroException {
    if (divisor == 0)
        throw new DivideByZeroException(); // that's a checked exception indeed

    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    try {
        int res = divide(a, b);
        System.out.println(res);
    } catch (DivideByZeroException e) {
        // checked exception... I'm forced to handle it!
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}
 


这不是应该使用异常的方式。上面的代码确实存在于现实生活中,但是它们更多的是一种异常,并且确实是例外(双关语)。例如,即使在纯数学中,除法的定义也是有条件的:总是必须处理特殊的零以限制输入域的“调用者代码”。它很丑。呼叫者总是很痛苦。不过,在这种情况下,先检查再做模式是很自然的方法:

 static int divide(int dividend, int divisor) {
    // throws unchecked ArithmeticException for 0 divisor
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt();
    if (b != 0) {
        int res = divide(a, b);
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}
 


另外,您也可以像这样在OOP风格上进行完整突击:

 static class Division {
    final int dividend;
    final int divisor;

    private Division(int dividend, int divisor) {
        this.dividend = dividend;
        this.divisor = divisor;
    }

    public boolean check() {
        return divisor != 0;
    }

    public int eval() {
        return dividend / divisor;
    }

    public static Division with(int dividend, int divisor) {
        return new Division(dividend, divisor);
    }
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Division d = Division.with(a, b);
    if (d.check()) {
        int res = d.eval();
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}
 


如您所见,调用者代码具有预检查的负担,但是此后不执行任何异常处理。如果ArithmeticException是来自调用divideeval的,则您必须执行异常处理并修复代码,因为您忘记了check()。出于类似的原因,捕捉NullPointerException几乎总是错误的事情。

现在有些人说他们想查看方法/函数签名中的特殊情况,即显式扩展输出域。他们是支持检查异常的人。当然,更改输出域应强制任何直接调用者代码进行调整,而这确实可以通过检查异常来实现。但是您不需要例外!这就是为什么要使用Nullable<T>泛型类,案例类,代数数据类型和联合类型的原因。对于一些简单的错误情况,某些面向对象的人甚至更喜欢返回null

 static Integer divide(int dividend, int divisor) {
    if (divisor == 0) return null;
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Integer res = divide(a, b);
    if (res != null) {
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}
 


可以将异常用于上述目的,但要点是:此类用法不存在异常。例外是专业的抽象。异常与间接有关。例外允许扩展“结果”域而不破坏直接的客户合同,并将错误处理推迟到“其他地方”。如果您的代码抛出的异常是在同一代码的直接调用者中处理的,而中间没有任何抽象层,那么您就是在这样做WRONG

如何捕获最新内容?

那么我们来了。我已经指出,在上述情况下使用异常并不是如何使用异常。但是,存在一个真正的用例,其中异常处理提供的抽象和间接是必不可少的。了解这种用法​​也将有助于理解最新建议。

该用例是:反对资源抽象编程...

是的,应该针对抽象而不是具体的实现对业务逻辑进行编程。顶级的IOC“接线”代码将实例化资源抽象的具体实现,并将其传递给业务逻辑。这里没有新内容。但是这些资源抽象的具体实现可能会抛出自己的实现特定的异常,不是吗?

那么谁可以处理那些实现特定的异常呢?那么,是否有可能在业务逻辑中处理任何特定于资源的异常?不,不是。业务逻辑是针对抽象编程的,抽象不排除那些实现特定异常细节的知识。

“ Aha!”,您可能会说:“但这就是为什么我们可以将异常归类并创建异常层次结构” (查看Spring先生!)。我告诉你,这是一个谬论。首先,每本有关OOP的合理书籍都指出具体继承是不好的,但是JVM的核心组件(异常处理)与具体继承紧密相关。具有讽刺意味的是,约书亚·布洛赫(Joshua Bloch)在获得可以使用的JVM的经验之前不可能写过他的《有效Java》一书,对吗?它更多地是下一代的“经验教训”书。其次,更重要的是,如果您捕获了高级异常,那么您将如何处理它? PatientNeedsImmediateAttentionException:我们需要给她服药还是要砍断她的腿!?关于所有可能的子类的switch语句怎么样?您的多态性到了,抽象就到了。您明白了。

那么谁可以处理资源特定的异常呢?一定是知道凝结物的人!实例化资源的人!当然,“接线”代码!检查一下:

根据抽象编码的业务逻辑...没有混凝土资源错误处理!

 static interface InputResource {
    String fetchData();
}

static interface OutputResource {
    void writeData(String data);
}

static void doMyBusiness(InputResource in, OutputResource out, int times) {
    for (int i = 0; i < times; i++) {
        System.out.println("fetching data");
        String data = in.fetchData();
        System.out.println("outputting data");
        out.writeData(data);
    }
}
 


同时,还有其他一些具体的实现...

 static class ConstantInputResource implements InputResource {
    @Override
    public String fetchData() {
        return "Hello World!";
    }
}

static class FailingInputResourceException extends RuntimeException {
    public FailingInputResourceException(String message) {
        super(message);
    }
}

static class FailingInputResource implements InputResource {
    @Override
    public String fetchData() {
        throw new FailingInputResourceException("I am a complete failure!");
    }
}

static class StandardOutputResource implements OutputResource {
    @Override
    public void writeData(String data) {
        System.out.println("DATA: " + data);
    }
}
 


,最后是接线代码。 。谁处理具体的资源异常?知道他们的人!

 static void start() {
    InputResource in1 = new FailingInputResource();
    InputResource in2 = new ConstantInputResource();
    OutputResource out = new StandardOutputResource();

    try {
        ReusableBusinessLogicClass.doMyBusiness(in1, out, 3);
    }
    catch (FailingInputResourceException e)
    {
        System.out.println(e.getMessage());
        System.out.println("retrying...");
        ReusableBusinessLogicClass.doMyBusiness(in2, out, 3);
    }
}
 


现在和我一起忍受。上面的代码很简单。您可能会说您拥有一个具有多个范围的IOC容器管理资源的企业级应用程序/ Web容器,并且需要自动重试和会话或请求范围资源的重新初始化,等等。较低级范围的接线逻辑可能会被赋予抽象工厂以创建资源,因此不知道确切的实现。只有更高级别的示波器才真正知道那些更低级别的资源会引发哪些异常。现在,请稍等!

不幸的是,异常仅允许间接调用堆栈,并且具有不同基数的不同作用域通常在多个不同线程上运行。没有办法通过例外进行沟通。这里我们需要更强大的功能。答:异步消息传递。在较低级别作用域的根目录捕获每个异常。不要忽略任何东西,不要让任何东西漏掉。这将关闭并处置在当前作用域的调用堆栈上创建的所有资源。然后,通过在异常处理例程中使用消息队列/通道将错误消息传播到更高的范围,直到达到已知凝固的级别。那就是知道如何处理它的人。

SUMMA SUMMARUM

因此,根据我的解释,在您再也不会破坏抽象的最方便的地方,捕获延迟意味着捕获异常。不要太早赶上!在创建具体异常并抛出资源抽象实例的层捕获异常,该层知道资源的抽象。 “接线”层。

HTH。祝您编程愉快!

评论


您是正确的,提供接口的代码会比使用接口的代码更了解出错的地方,但是假设一种方法使用接口类型相同的两个资源,并且需要以不同的方式处理故障?或者,如果内部的那些资源之一(作为其创建者不知道的实现细节)使用了相同类型的其他嵌套资源?让业务层抛出WrappedFirstResourceException或WrappedSecondResourceException并要求“接线”层在该异常内部查看以查看问题的根本原因...

–超级猫
2014年6月25日15:13

...可能有点棘手,但比假设任何FailingInputResource异常将是使用in1进行操作的结果要好。实际上,我认为在许多情况下,正确的方法是使布线层传递异常处理对象,并使业务层包含catch,然后该catch调用该对象的handleException方法。该方法可以重新抛出或提供默认数据,或显示“中止/重试/失败”提示,并让操作员根据需要的应用程序来决定要执行的操作等。

–超级猫
2014年6月25日15:16

@supercat我明白你在说什么。我想说一个具体的资源实现负责知道它可能引发的异常。它不必指定所有内容(所谓的未定义行为),但是它必须确保没有歧义。另外,应记录未经检查的运行时异常。如果它与文档相矛盾,那就是一个错误。如果希望调用者代码对异常做任何明智的事情,则最低限度是资源将它们包装在一些UnrecoverableInternalException中,类似于HTTP 500错误代码。

–丹尼尔(Daniel Dinnyes)
2014年7月24日10:02

@supercat关于您对可配置错误处理程序的建议:完全正确!在我的最后一个示例中,错误处理逻辑是硬编码的,调用了静态的doMyBusiness方法。为了简洁起见,完全有可能使其更具动态性。这样的Handler类将使用一些输入/输出资源实例化,并具有handle方法,该方法接收实现ReusableBusinessLogicInterface的类。然后,您可以组合/配置它们以在其上方某处的布线层中使用不同的处理程序,资源和业务逻辑实现。

–丹尼尔(Daniel Dinnyes)
2014年7月24日10:22

您的代码检查是否允许整数除法不正确。除以零不是唯一的问题。 MININT / -1是另一个问题。

– gnasher729
19/12/30在21:22

#4 楼

为了正确回答这个问题,让我们退后一步,提出一个更基本的问题。

为什么首先要有异常?

我们抛出异常以使方法的调用者知道我们无法执行被要求执行的操作做。异常的类型说明了为什么我们不能做我们想做的事情。

让我们看一些代码:

double MethodA()
{
    return PropertyA - PropertyB.NestedProperty;
}


如果PropertyB为null,则此代码显然可以引发null引用异常。在这种情况下,我们可以做两件事来“纠正”这种情况。我们可以:


如果没有自动创建PropertyB;或
让异常冒泡直到调用方法。

在此处创建PropertyB可能非常危险。此方法必须创建PropertyB的原因是什么?当然,这将违反单一责任原则。很可能,如果PropertyB在这里不存在,则表明出了点问题。在部分构造的对象上调用该方法,或者PropertyB错误地设置为null。通过在此处创建PropertyB,我们可以隐藏一个更大的bug,以后可能会咬我们,例如,导致数据损坏的bug。

如果相反,我们让null引用冒泡了,我们正在让调用此方法的开发人员尽快知道出了点问题。错过了调用此方法的重要先决条件。

因此,实际上,我们提早抛出,因为它可以更好地分离我们的关注点。一旦发生故障,我们就会通知上游开发人员。

为什么我们“赶不上来”是另外一回事。我们真的不想迟到,我们真的想尽早了解到如何正确处理问题。在某些情况下,这将是十五层的抽象层,有时在创建时。

关键是我们想在抽象层捕获异常,这使我们能够在拥有正确处理异常所需的所有信息的时候处理异常。

评论


我认为您使用错误的上游开发人员。另外,您说它违反了单一责任原则,但实际上许多按需初始化和值缓存都是通过这种方式实现的(当然,适当的并发控制也可以代替)

–丹尼尔(Daniel Dinnyes)
2014年3月4日在11:36

在给定的示例中,如何在减法运算之前检查null,例如if(PropertyB == null)返回0;

–user1451111
18年6月14日在20:45



您能否也详细说明最后一段,特别是“抽象层”的含义。

–user1451111
18年6月14日在20:48

如果我们在做IO工作,那么捕获IO异常的抽象层就是我们要做的工作。到那时,我们掌握了决定是否重试或向用户抛出消息框或使用一组默认值创建对象所需的所有信息。

–斯蒂芬
18年6月26日在0:17

“在给定的示例中,如何在减法运算之前检查null,例如if(PropertyB == null)返回0;” uck那将告诉调用方法我要减去有效的东西。当然,这是上下文相关的,但是在大多数情况下,在此处进行错误检查将不是一个好习惯。

–斯蒂芬
18 Jun 26'在0:21



#5 楼

一旦看到值得扔掉的东西,就应该抛出,以免将对象置于无效状态。这意味着,如果传递了空指针,则应尽早检查它并在NPE有机会滴入低位之前抛出NPE。

一旦知道如何解决该问题,请立即捕获。错误(通常不会抛出该错误,否则可以只使用if-else),如果传递了无效参数,则提供该参数的层应处理后果。

评论


您写道:马上扔,...赶上...!为什么?与“提早投掷,追赶迟到”相反,这是完全相反的方法。

– shylynx
2014年3月3日13:46

@shylynx我不知道“早丢,晚抓”来自何处,但其价值值得怀疑。捕获“迟到”到底是什么意思?捕获异常(如果有的话)在哪里有意义取决于问题。唯一清楚的是您想尽早发现问题(并抛出)。

–Doval
2014年3月3日在13:56

我认为“延迟捕获”的意思是在您知道如何解决错误之前先对比捕获的做法。有时,您会看到可以捕获所有内容的函数,因此它们可以打印错误消息,然后重新引发异常。

–user122173
2014年4月4日19:10

@Hurkyl:“延迟捕获”的一个问题是,如果异常在不了解它的各层中冒出气泡,则可能无法对情况进行处理的代码很难知道事情确实是预期。举一个简单的例子,假设如果用户文档文件的解析器需要从磁盘加载CODEC,并且在读取该磁盘时发生磁盘错误,则调用解析器的代码如果认为在读取用户时发生磁盘错误,则可能无法正常工作。文件。

–超级猫
2014年6月25日15:06

#6 楼

有效的业务规则是“如果较低级别的软件无法计算值,那么...”

只能在较高级别上表示,否则较低级别的软件将尝试更改其值。行为基于其自身的正确性,只会结为结局。

#7 楼

首先,例外是​​特殊情况。在您的示例中,如果由于无法加载原始数据而没有原始数据,则无法计算任何数字。

根据我的经验,在遍历堆栈时提取异常是一个很好的实践。通常,要执行此操作的地方是每当异常跨越两层之间的边界时。

如果在数据层中收集原始数据时出错,则引发异常以通知请求数据。不要尝试在此处解决此问题。处理代码的复杂度可能很高。而且,数据层仅负责请求数据,而不负责处理在执行此操作时发生的错误。这就是“早点扔”的意思。

在您的示例中,捕获层是服务层。服务本身是一个新的层,位于数据访问层之上。因此,您想在那里捕获异常。也许您的服务具有一些故障转移基础结构,并尝试从另一个存储库请求数据。如果仍然失败,则将异常包装在服务的调用者可以理解的内容内(如果是Web服务,则可能是SOAP错误)。将原始异常设置为内部异常,以便以后的层可以准确记录发生了什么问题。

调用服务的层(例如UI)可能会捕获服务故障。这就是“迟到”的意思。如果您无法在较低的层中处理该异常,则将其重新抛出。如果最顶层不能处理该异常,请处理它!这可能包括记录或显示它。

您应该抛出异常的原因(如上文所述,将异常包装在更通用的示例中)是因为用户很可能无法理解存在错误,因为例如,指向无效内存的指针。而且他不在乎。他只关心该数字不能由服务计算得出,而这是应该向他显示的信息。

在理想的情况下,您甚至可以完全忽略try / catch代码从用户界面。取而代之的是使用一个全局异常处理程序,该处理程序能够理解较低层可能抛出的异常,将它们写入一些日志中,然后将其包装到包含有意义的(可能是本地化的)错误信息的错误对象中。这些对象可以轻松地以您希望的任何形式(消息框,通知,消息敬酒等)呈现给用户。

#8 楼

通常,尽早抛出异常是一个好习惯,因为您不希望违约的合同在不必要的情况下流经代码。例如,如果您希望某个函数参数为正整数,则应在函数调用时强制执行该约束,而不要等到该变量在代码堆栈中的其他位置使用。

赶上很晚,我无法真正置评,因为我有自己的规则,而且规则会因项目而异。我尝试做的一件事是将异常分为两组。一种仅用于内部使用,另一种仅用于外部使用。内部异常是由我自己的代码捕获和处理的,而外部异常是由要调用我的代码处理的。这基本上是稍后捕获的一种形式,但并非完全如此,因为它为我提供了必要时在内部代码中偏离规则的灵活性。