Can my AutoCloseable.close() implementation detect a potential exception?
Asked Answered
T

2

12

When implementing an AutoCloseable to work with the Java 7 try-with-resources statement, I would like to know if there had been an exception within the try block. E.g.:

class C implements AutoCloseable {
    @Override
    public void close() {
        if (exceptionOccurred)
            something();
        else
            somethingElse();
    }
}

To illustrate this:

try (C c = new C()) {

    // This should cause a call to "something()"
    if (something)
        throw new RuntimeException();

    // This should cause a call to "somethingElse()"
    else
        ;
}

Now, from understanding how the try-with-resources statement translates to bytecode, I guess that's not possible. But is there any (reliable!) trick through instrumentation / reflection / some undocumented compiler feature that allows me to access the above RuntimeException from within AutoCloseable.close() ?

Note: I'm an API designer, and I cannot control API consumers' try-with-resources code. The implementation must thus be done at the AutoCloseable site

Tran answered 25/1, 2014 at 17:10 Comment(0)
E
10

The normal way to do this is just to explicitly make a call at the end of the try block. For example:

try (CustomTransaction transaction = ...) {
    // Do something which might throw an exception...

    transaction.commitOnClose();
}

Then in close, you'd either abort the transaction or commit it, based on whether commitOnClose() had been called or not.

It's not automatic, but it's pretty simple to achieve - and very simple to read.

Exhibitionist answered 25/1, 2014 at 17:14 Comment(6)
I see, you're right in principle. But I'm an API designer and I don't control API consumers' try-with-resources code. I should add that to the questionTran
How did you guess that this was really about transactions, btw? :-)Tran
@LukasEder: If you control the API, you can tell your API users that they need to call the extra method. It's probably the simplest option for your consumers, and there really isn't anything else you can do with the straight close() method as far as I'm aware.Exhibitionist
@LukasEder: I didn't guess it's actually about transactions in your specific case - I just gave an example where this would naturally be useful. Coincidentally, it's an example of what I've used in a lot of code myself...Exhibitionist
Nice coincidence :-) Well, if this cannot be solved within close(), I might prefer an API involving SAMs rather than abusing try-with-resources. With the advent of Java 8, lambdas are a good way to enclose transactional code. Something along the lines of tx.run(() -> { /* transactional code */ });Tran
@LukasEder: I don't actually regard it as an abuse of try-with-resources at all, but I agree that passing in "the code to execute in the transaction" will be even cleaner.Exhibitionist
F
2

I've been struggling with this for awhile. I dislike Jon Skeet's answer because a developer (i.e. me) might accidentally forget to call commitOnClose(). I want a way for the developer to be forced to call either commit() or rollback() when he leaves the block of code.

Lambda's and checked exceptions don't play nice together so a proper solution took a bit of puzzling, but eventually me and a coworker of mine came up with a piece of code that allows you to work like this:

TransactionEnforcer.DbResult<String> result = transactionEnforcer.execute(db -> {
  try {
    db.someFunctionThatThrowsACheckedException();
  } catch (TheException e) {
    return failure("fallback value");
  }
  return success(db.getAFancyValue());
});

result.ifPresent(v -> System.out.println(v));

Note how you can return values, can check if the code succeeded or not, and java's codepath return check enforces you to always be explicit about whether the code should be committed or not.

It is implemented using the code below:

package nl.knaw.huygens.timbuctoo.database;

import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;

public class TransactionEnforcer {
  private final Supplier<DbClass> dbClassFactory;

  public TransactionEnforcer(Supplier<DbClass> dbClassFactory) {
    this.dbClassFactory = dbClassFactory;
  }

  public <U> DbResult<U> execute(Function<DbClass, DbResult<U>> actions) {
    DbClass db = dbClassFactory.get();
    try {
      DbResult<U> result = actions.apply(db);
      if (result.isSuccess()) {
        db.close(true);
      } else {
        db.close(false);
      }
      return result;
    } catch (RuntimeException e) {
      db.close(false);
      throw e;
    }
  }

  public static class DbResult<T> {
    private final Optional<T> value;
    private final boolean success;

    private DbResult(T value, boolean success) {
      this.value = Optional.of(value);
      this.success = success;
    }

    public static <T> DbResult<T> success(T value) {
      return new DbResult<T>(value, true);
    }

    public static <T> DbResult<T> success() {
      return new DbResult<T>(null, true);
    }

    public static <T> DbResult<T> failure(T value) {
      return new DbResult<T>(value, false);
    }

    public static <T> DbResult<T> failure() {
      return new DbResult<T>(null, false);
    }

    public boolean isSuccess() {
      return success;
    }

    public Optional<T> getValue() {
      return value;
    }
  }
}

(I leave the DbClass as an exercise to the reader)

Fell answered 23/9, 2016 at 13:23 Comment(4)
Sure. We did this too in the end, e.g. in jOOQ's transactionResult() method, which runs a functional interface in the context of a nestable transaction. That's fine if Java 8 idioms are desired at the call site. The specific question was about using Java 7's try-with-resources, though.Tran
ok. The reason I posted this example was that I came upon this question while I had the same problem as you had (as can be read in the comments on Jon Skeets answer) You mention using Lambda's/SAMs but It took me a while to come up with a solution that worked. So for everyone else who comes to this question with the same use case, it might be nice to actually get a solution that helps them. (But if you feel this is offtopic I'll remove it again)Fell
No, not off topic at all. It's a good answer, thank you very much for sharing. Just explained why I'm not going to accept it :)Tran
Ah, yeah. I didn't expect you to :)Fell

© 2022 - 2024 — McMap. All rights reserved.