清理多个不可关闭资源时减少嵌套

Reduce nesting when cleaning up multiple non-Closeable resources

我有一个Closeable需要在close()方法中清理多个资源。每个资源都是我无法修改的final class。 None 个包含的资源是 CloseableAutoCloseable。我还需要打电话给super.close()。所以看来我无法使用 try-with-resources 处理任何资源*。我当前的实现看起来像这样:

public void close() throws IOException {
    try {
        super.close();
    } finally {
        try {
            container.shutdown();
        } catch (final ShutdownException e) {
            throw new IOException("ShutdownException: ", e);
        } finally {
            try {
                client.closeConnection();
            } catch (final ConnectionException e) {
                throw new IOException("Handling ConnectionException: ", e);
            }
        }
    }
}

我更喜欢嵌套不那么疯狂的解决方案,但我不知道如何利用 try-with-resources 或任何其他功能来做到这一点。 Code sandwiches don't seem to help here since I'm not using the resources at all, just cleaning them up. Since the resources aren't Closeable, it's unclear how I could use the recommended solutions in Java io ugly try-finally block.

* 即使 super class 是 Closeable,我也不能在 try-with-resources 中使用 super 因为 super 只是句法糖而不是真正的 Java Object.

这对 try-with-resources 来说是一个很好的(尽管不是正统的)案例。首先,您需要创建一些接口:

interface ContainerCleanup extends AutoCloseable {
    @Override
    void close() throws ShutdownException;
}
interface ClientCleanup extends AutoCloseable {
    @Override
    void close() throws ConnectionException;
}

如果这些接口仅在当前class中使用,我建议将它们设为内部接口。但如果您在多个 class 中使用它们,它们也可以用作 public 实用程序接口。

然后在你的close()方法中你可以做:

public void close() throws IOException {
    final Closeable ioCleanup = new Closeable() {
        @Override
        public void close() throws IOException {
            YourCloseable.super.close();
        }
    };
    final ContainerCleanup containerCleanup = new ContainerCleanup() {
        @Override
        public void close() throws ShutdownException {
            container.shutdown();
        }
    };
    final ClientCleanup clientCleanup = new ClientCleanup() {
        @Override
        public void close() throws ConnectionException {
            client.closeConnection();
        }
    };
    
    // Resources are closed in the reverse order in which they are declared,
    // so reverse the order of cleanup classes.
    // For more details, see Java Langauge Specification 14.20.3 try-with-resources:
    // https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.20.3
    try (clientCleanup; containerCleanup; ioCleanup) {
        // try-with-resources only used to ensure that all resources are cleaned up.
    } catch (final ShutdownException e) {
        throw new IOException("Handling ShutdownException: ", e);
    } catch (final ConnectionException e) {
        throw new IOException("Handling ConnectionException: ", e);
    }
}

当然,使用 Java 8 个 lambda 表达式会变得更加优雅和简洁:

public void close() throws IOException {
    final Closeable ioCleanup = () -> super.close();
    final ContainerCleanup containerCleanup = () -> container.shutdown();
    final ClientCleanup clientCleanup = () -> client.closeConnection();
    
    // Resources are closed in the reverse order in which they are declared,
    // so reverse the order of cleanup classes.
    // For more details, see Java Langauge Specification 14.20.3 try-with-resources:
    // https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.20.3
    try (clientCleanup; containerCleanup; ioCleanup) {
        // try-with-resources only used to ensure that all resources are cleaned up.
    } catch (final ShutdownException e) {
        throw new IOException("Handling ShutdownException: ", e);
    } catch (final ConnectionException e) {
        throw new IOException("Handling ConnectionException: ", e);
    }
}

这消除了所有疯狂的嵌套,并且具有保存被抑制的异常的额外好处。在您的情况下,如果 client.closeConnection() 抛出,我们永远不会知道以前的方法是否抛出任何异常。所以堆栈跟踪看起来像这样:

Exception in thread "main" java.io.IOException: Handling ConnectionException: 
        at Main$YourCloseable.close(Main.java:69)
        at Main.main(Main.java:22)
Caused by: Main$ConnectionException: Failed to close connection.
        at Main$Client.closeConnection(Main.java:102)
        at Main$YourCloseable.close(Main.java:67)
        ... 1 more

通过使用 try-with-resources,Java 编译器生成代码来处理被抑制的异常,因此我们将在堆栈跟踪中看到它们,如果需要,我们甚至可以在调用代码中处理它们至:

Exception in thread "main" java.io.IOException: Failed to close super.
        at Main$SuperCloseable.close(Main.java:104)
        at Main$YourCloseable.access[=14=]1(Main.java:35)
        at Main$YourCloseable .close(Main.java:49)
        at Main$YourCloseable.close(Main.java:68)
        at Main.main(Main.java:22)
        Suppressed: Main$ShutdownException: Failed to shut down container.
                at Main$Container.shutdown(Main.java:140)
                at Main$YourCloseable.close(Main.java:55)
                at Main$YourCloseable.close(Main.java:66)
                ... 1 more
        Suppressed: Main$ConnectionException: Failed to close connection.
                at Main$Client.closeConnection(Main.java:119)
                at Main$YourCloseable.close(Main.java:61)
                at Main$YourCloseable.close(Main.java:66)
                ... 1 more

注意事项

  1. 如果清理的顺序很重要,您需要在 reverse[=] 中声明您的资源清理 classes/lambdas 50=] 订购您想要的 运行。我建议为此效果添加评论(就像我提供的那样)。

  2. 如果任何异常被抑制,该异常的catch块将执行。在这些情况下,最好更改 lambda 来处理异常:

    final Closeable containerCleanup = () -> {
        try {
            container.shutdown();
        } catch (final ShutdownException e) {
            // Handle shutdown exception
            throw new IOException("Handling shutdown exception:", e);
        }
    }
    

    处理 lambda 内部的异常确实开始添加一些嵌套,但嵌套不像原来的那样是递归的,所以它永远只有一层深。

即使有这些警告,我相信这里的自动抑制异常处理、简洁、优雅、可读性和减少嵌套(特别是如果您有 3 个或更多资源需要清理)的优点远远超过缺点。