清理多个不可关闭的资源时减少嵌套 注意事项

问题描述

我有一个 Closeable 需要在 close() 方法中清理多个资源。每个资源都是一个我无法修改final 类。包含的资源都不是 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 在这里似乎没有帮助,因为我根本没有使用资源,只是清理它们。由于资源不是 Closeable,因此我不清楚如何使用 Java io ugly try-finally block 中推荐的解决方案。

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

解决方法

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

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

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

然后在您的 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 lambdas 中变得更加优雅和简洁:

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,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$001(Main.java:35)
        at Main$YourCloseable $1.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$2.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$3.close(Main.java:61)
                at Main$YourCloseable.close(Main.java:66)
                ... 1 more

注意事项

  1. 如果清理顺序很重要,您需要以相反的顺序声明您的资源清理类/lambdas,以便它们运行。我建议为此添加评论(就像我提供的那样)。

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

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

    处理 lambda 内部的异常确实开始添加一些嵌套,但嵌套不像原始嵌套那样递归,因此它只会有一层深。

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