【问题标题】:How to best execute a set of methods even if an exception happens即使发生异常,如何最好地执行一组方法
【发布时间】:2009-03-24 21:30:32
【问题描述】:

在当前的 Java 项目中,我们有类似于以下示例的代码:

try {
    doSomeThing(anObject);
}
catch (SameException e) {
    // Do nothing or log, but don't abort current method.
}

try {
    doOtherThing(anObject);
}
catch (SameException e) {
    // Do nothing or log, but don't abort current method.
}

// ... some more calls to different method ...

try {
    finallyDoYetSomethingCompletelyDifferent(anObject);
}
catch (SameException e) {
    // Do nothing or log, but don't abort current method.
}

如您所见,使用完全相同的对象调用了几个不同的方法,并且对于每次调用,都会捕获和处理相同的异常(或以非常相似的方式)。异常不会重新抛出,而只能被记录然后丢弃。

每个方法周围都有一个try-catch 的唯一原因是始终执行所有方法,无论之前执行的方法是否失败。

我根本不喜欢上面的代码。它占用大量空间,非常重复(尤其是在catch-block 中完成的日志记录;此处未介绍)而且看起来很糟糕。

我可以想到一些其他的方法来编写这段代码,但也不是很喜欢它们。我想到了以下选项:

循环切换序列/for-case 范例

(见WikipediaThe Daily WTF

for (int i = 0; i <= 9; i++) {
    try {
        switch (i) {
            case 0:
                doSomeThing(anObject); break;
            case 1:
                doOtherSomeThing(anObject); break;
            // ...More cases...
            case 9:
                doYetSomethingCompletelyDifferent(anObject); break;
        }
    }
    catch (SameException e) {
        // Do nothing or log, but don't abort current method.
    }
}

这显然是糟糕的代码,非常容易出错并且看起来很业余。

反思

使用反射来获取Method 对象,以便方法调用并将它们按应该执行的顺序存储在列表中。然后遍历此列表并使用anObject 作为唯一参数调用该方法。异常在循环内部处理。

我不喜欢这种方法,因为错误(例如方法名称中的拼写错误)只会在运行时弹出,而且反射 API 有点啰嗦。

函子

像这样创建一个 Functor 类:

private class Functor
{
    void doStuff(MyObject object) throws SameException;
}

然后创建调用方法的Functor 对象列表。像这样:

List<Functor> functors = new ArrayList<Functor>();

functors.add(new Functor() {
    @Override
    public void execute(MyObject anObject) {
        doSomeThing(anObject);
    }
});

functors.add(new Functor() {
    @Override
    public void execute(MyObject anObject) {
        doOtherSomeThing(anObject);
    }
});

稍后,迭代此列表并在每个 Functor 对象上调用 execute()。 我可以用两个词来概括我对这种方法的感受:代码膨胀。


由于我不太喜欢所有四种方法,因此我想在这里讨论这个问题。你觉得最好的方法是什么?您过去是如何解决类似问题的?有没有我完全错过的更简单的解决方案?

【问题讨论】:

    标签: java exception


    【解决方案1】:

    我会提倡重构方法(或“我们当初为什么会走到这一步?”):

    考虑为什么个别方法可以在对 myObject 进行“填充”之后抛出异常,然后可以安全地忽略该异常。由于异常转义了方法,所以 myObject 必须处于未知状态。

    如果忽略异常是安全的,那么肯定它一定是错误的方式来传达每个方法中的问题。

    相反,可能是每个方法都需要在失败时进行一些日志记录。如果您不使用静态记录器,则可以将记录器传递给每个方法。

    【讨论】:

    • 不幸的是,原始异常是(我认为)Hibernate 抛出的 ConstraintViolationException 类型。这是无法改变的。我们可以在每个方法中捕获并处理异常。但这不会使代码变得更好。理想情况下,我希望所有调用都只有一个 catch 子句。
    • 但这意味着没有任何东西被写入数据库,那么为什么还要尝试继续事务呢?根据底层数据库,它已经被标记为回滚。
    • 除非您将异常理解为“对象已存在于表中,因此我们忽略插入失败”。正确的方法是首先尝试将对象加载到 Hibernates 会话缓存中,然后执行保存(或 saveOrUpdate)。我知道,因为我自己去过那里:)
    • 我一定会调查的。它目前在 Oracle 和 Derby 上运行良好。我们的用例说,如果某些信息无法进入数据库,那也没关系。我们的输入数据有时会非常不稳定。不过,后来的一些应用程序可以接受。我们也不喜欢那样。
    • 我们通常会做这个检查(“它是否已经存在?”)。我想知道为什么我们在这种情况下不这样做。也许只是一些预防措施。我将不得不询问原始开发人员(并相应地记录下来)。但可能是因为在这个用例中“损坏”的数据很好。嗯……
    【解决方案2】:

    在我看来,仿函数方法是最好的方法 - 遗憾的是 Java 没有更好的方法来表示闭包或委托。这基本上就是您真正所追求的,而在 C#(和许多其他语言)中,这将是微不足道的。

    您可以通过以下方式减少身体膨胀:

    Functor[] functors = new Functor[] {
        new Functor() { @Override public void execute(MyObject anObject) {
            doSomeThing(anObject);
        }},
        new Functor() { @Override public void execute(MyObject anObject) {
            doSomeOtherThing(anObject);
        }}
    };
    

    此处的空格折叠很可能与您使用的样式指南背道而驰,但我认为它使代码更易于实际阅读,因为您可以更轻松地看到内容。

    最好开始游说 Java 8 中的闭包;)

    【讨论】:

    • 膨胀比这更糟糕——你(和他)忘记了 throws 子句。
    • 哇,你真快! :) 我也考虑过闭包/代表。但是,正如您所说,Java 必须提供的最相似的构造是函子。也许您使用最适合阅读的代码格式是正确的,但不一定尊重我们使用的样式指南。
    • @mmyers 在原始代码中,捕获的异常是 RuntimeException。因此,在这种特殊情况下,这不是问题。 (当然,在其他情况下可能是。)
    • 风格指南应该用作“非常严厉的指南”而不是“绝对具体的规则”IMO。只是偶尔,打破准则可以让生活变得更美好。 (switch/case 可以有这个,如果每个 case 都是一个 return 语句。把每个 case/return 放在一行上。)
    • 另一个注意事项:@Override 注释在这里不是绝对必要的。如果匿名类未能实现接口,它们无论如何都将无法编译。所以这有点不那么混乱了。
    【解决方案3】:

    乍一看,我同意 PeterR:如果可以安全地轻松忽略异常,那么该方法可能根本不应该抛出异常。

    但是,如果您确定这正是您想要的,比如说,也许您正在使用坚持抛出特定异常的 3rd 方库中的方法,我会选择以下方法:

    1. 创建一个包含所有可以调用的方法的接口:

      公共接口 XyzOperations { 公共无效doSomething(对象anObject); 公共无效doOtherThing(对象anObject); ... public void finallyDoYetSomethingCompletelyDifferent(Object anObject);
    2. 为这些方法适当的方法创建一个默认实现类,可能从其他地方重构它们:

      公共类 DefaultXyzOperations 实现 XyzOperations { ... }
    3. 使用 Java Proxy 类在 XyzOperations 上创建一个动态代理,它将所有方法委托给 DefaultXyzOperations,但是,将在其 InvocationHandler 中进行集中式异常处理。以下我没有编译,但它是一个基本的大纲:

      XyzOperations xyz = (XyzOperations)Proxy.newProxyInstance( XyzOperations.class.getClassLoader(), 新类[] { XyzOperations.class }, 新的 InvocationHandler() { 公共对象调用(对象代理,方法方法,对象 [] args)抛出 Throwable { 尝试 { method.invoke(new DefaultXyzOperations(), args); } 捕捉(SameException e){ // 期望的异常处理 } } });
    4. 从那时起使用该代理实例,只需调用所需的方法

    或者,您可以使用AspectJ 或类似的AOP 解决方案为XyzOperations 的所有方法添加环绕建议,并在那里进行异常处理。

    您是否愿意引入新的依赖项,或者手动编写代理取决于您的个人喜好以及您需要此类行为的代码总量。

    【讨论】:

      【解决方案4】:

      我同意 PeterR 的观点。我很难相信你真的想在抛出异常后继续执行某些事情。如果你这样做了,那么实际上可能并没有发生什么异常情况。

      换句话说,不应将异常用于日志记录或流控制。仅当发生异常而引发异常的级别的代码无法处理时才应使用它们。

      因此,我会将日志消息内部化并删除引发的异常。

      至少,我认为您需要回过头来重新理解代码试图做什么以及正在实施的业务价值或规则。正如 PeterR 所说,试着理解“我们为什么会来到这里?”部分代码以及异常的确切含义。

      【讨论】:

        【解决方案5】:

        您正在调用的方法是否在您的控制之下?如果是这样,在这种特殊情况下,返回错误代码(或对象)可能会产生比使用异常更好的整体设计:

        handle(doSomething(anObject));
        handle(doOtherThing(anObject));
        // some more calls to different methods
        handle(finallyDoYetSomethingCompletelyDifferent(anObject));
        

        private void handle(ErrorCode errorCode) {
          // Do something about it
        }
        

        private ErrorCode doSomething(Object anObject) {
          // return ErrorCode describing the operation's outcome
        }
        

        这似乎不那么冗长,尽管不是 DRY。

        或者,您使用一些 AOP 机制来拦截对 doSomethingdoOtherThingfinallyDoYetSomethingCompletelyDifferent 的调用,并使用先处理然后丢弃异常的环绕通知。将它与 RuntimeExceptions 和基于一些很好的描述性注释的切入点结合起来,您就可以完美地捕捉到某种隐藏的横切关注点。

        我承认我喜欢 Functor 方法。

        编辑:刚刚看到您对其中一个答案的评论。在这种情况下,我可能会选择 AOP 方法。

        【讨论】:

          【解决方案6】:

          抛开重构问题不谈,AspectJ(或类似的)似乎是透明地捕获/报告这些异常的最简单方法。您应该能够进行配置,即使您添加新的方法调用,它也会在方法调用周围编织 try/catch(我怀疑,面对有人在没有完全理解的情况下修改该代码时,上面的代码块会非常脆弱其背后的基本原理)

          【讨论】:

            【解决方案7】:

            如果您采用不同的代码风格路线,我会坚持一个简单的:

            try { doSomeThing(anObject); } catch (SameException e) { Log(e); }
            try { doOtherThing(anObject); } catch (SameException e) { Log(e); }
            // ... some more calls to different method ...
            

            更新:我看不出使用像 Functor 方法这样的语法如何减少所涉及的任何代码。正如 Jon 所提到的,java 不支持进一步减少它的简单语法。如果是 c#,你可以做很多变体,围绕这样一个事实,即没有太多额外的语法来组合这样的方法,即像 actions.Add(() => doSomething(anObject)) 这样的表达式;

            【讨论】:

            • 其他人不同意,但我支持你弗雷迪。代码做它想要的,它只是美学和使事情复杂化的愿望。如果会有数百个这样的调用,那么可能是 Functor 或重构。但此刻,别管它。
            【解决方案8】:

            让我再详细说明一下仿函数方法...

            根据您的应用程序的复杂性,有时值得将部分业务逻辑移动到一个 conf 文件中,这样可以更恰当、更简洁地表达它。通过这种方式,您可以将技术细节(仿函数创建/调用、异常处理)与业务逻辑分开 - 定义要调用的方法和调用顺序。

            最简单的形式可能是这样的:

            mypackage.Action1
            mypackage.Action2
            ...
            

            其中 ActionX 是实现 Functor 类(或接口)的类。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2020-09-22
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-05-08
              相关资源
              最近更新 更多