Search code examples
javaexceptiondesign-patternsjava-7try-with-resources

Reduce nesting when cleaning up multiple non-Closeable resources


I have a Closeable that needs to clean up multiple resources in the close() method. Each resource is a final class that I cannot modify. None of the included resources are Closeable or AutoCloseable. I also need to call super.close(). So it appears that I cannot handle any of the resources* using try-with-resources. My current implementation looks something like this:

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);
            }
        }
    }
}

I'd prefer a solution with less crazy nesting but I can't figure out how to take advantage of try-with-resources or any other features to do that. 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.

* Even though the super class is Closeable, I cannot use super in a try-with-resources because super is just syntactic sugar and not a real Java Object.


Solution

  • This a good (albeit unorthodox) case for try-with-resources. First, you'll need to create some interfaces:

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

    If these interfaces are only used in the current class, I'd recommend making them inner interfaces. But they also work as public utility interfaces if you use them in multiple classes.

    Then in your close() method you can do:

    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);
        }
    }
    

    Of course this becomes even more elegant and concise with 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,
        // 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);
        }
    }
    

    This removes all the crazy nesting and it has the added benefit of saving the suppressed exceptions. In your case, if client.closeConnection() throws, we'll never know if the previous methods threw any exceptions. So the stacktrace will look something like this:

    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
    

    By using try-with-resources, the Java compiler generates code to handle the suppressed exceptions, so we'll see them in the stacktrace and we can even handle them in the calling code if we want to:

    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
    

    Caveats

    1. If the order of clean up matters, you need to declare your resource cleanup classes/lambdas in the reverse order that you want them run. I recommend adding a comment to that effect (like the one I provided).

    2. If any exceptions are suppressed, the catch block for that exception will not execute. In those cases, it's probably better change the lambdas to handle the exception:

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

      Handling the exceptions inside the lambda does start to add some nesting, but the nesting isn't recursive like the original so it'll only ever be one level deep.

    Even with those caveats, I believe the pros greatly outweigh the cons here with the automatic suppressed exception handling, conciseness, elegance, readability, and reduced nesting (especially if you have 3 or more resources to clean up).