У меня вопрос, но начну с введение в то как я до него дошёл.
Допустим, у нас есть какой-то коллбэк, в качестве примера возьмём FutureCallback из всячески уважаемой мной библиотеки HttpCore.
Допустим также, что мы угорели по асинхронности и поэтому после того как коллбэк завершил свою работу, мы хотим сообщить об этом куда следует. Например, чтобы там новый запрос запустили или ещё чего.
Наивная имплементация будет выглядеть примерно так:
public class CompletingFutureCallback<T> implements FutureCallback {
private final FutureCallback<? super T> callback;
private final Runnable completionCallback;
public CompletionFutureCallback(
final FutureCallback<? super T> callback,
final Runnable completionCallback)
{
this.callback = callback;
this.completionCallback = completionCallback;
}
@Override
public void cancelled() {
callback.cancelled();
completionCallback.run();
}
@Override
public void completed(final T result) {
callback.completed(result);
completionCallback.run();
}
@Override
public void failed(final Exception e) {
callback.failed(e);
completionCallback.run();
}
}
Зоркий глаз сразу скажет: А что если коллбэк был написан говнокодером и он кинет unchecked exception в ответ на вызов cancelled
, completed
или failed
? Тогда completionCallback
вызван не будет. Ладно, переделаем на finally
. Далее уже рассматриваем рефакторинг одной функции, благо все они однотипные:
@Override
public void cancelled() {
try {
callback.cancelled();
} finally {
completionCallback.run();
}
}
Зоркий глаз возразит ещё раз: А что если оба коллбэка написаны одним и тем же говнокодером и completionCallback.run()
так же кинет unchecked exception? Да, после того как unchecked exception кинул callback.cancelled()
. Даже если мы где-то снаружи ловим все Throwable, то информация о первом эксепшене будет безвозвратно потеряна.
В этот момент перфекционист вырывает клок волос из головы и призывает в помощь try-with-resources:
class Completer implements AutoCloseable {
private final Runnable completionCallback;
public Completer(final Runnable completionCallback) {
this.completionCallback = completionCallback;
}
@Override
public void close() {
completionCallback.run();
}
}
public class CompletingFutureCallback<T> implements FutureCallback<T> {
…
@Override
public void cancelled() {
try (Completer completer = new Completer(completionCallback)) {
callback.cancelled();
}
}
…
}
Вот теперь всё предельно корректно:
Если callback
кинет исключение, то completer.close()
так же будет вызван, и если он также кинет исключение, то это второе исключение будет добавлено к первому в список suppressed и при печати стек-трейса его будет видно и можно будет поанализировать.
Ну а если callback
отработал как следует, то completer.close()
так же будет вызван, а если исключение, if any, будет также проброшено наружу.