Как правильно обрабатывать исключения из ListenableFuture guava?

У меня есть библиотека, в которой я предоставил два метода, синхронизацию и асинхронность для нашего клиента. Они могут называть тот метод, который, по их мнению, подходит для их целей.

  • executeSynchronous() - ждет, пока у меня не будет результата, вернет результат.
  • executeAsynchronous() - немедленно возвращает Будущее, которое может быть обработано после выполнения других действий, если это необходимо.

Они передадут объект DataKey, в котором есть идентификатор пользователя. И мы выясним, какую машину вызывать на основе идентификатора пользователя. Таким образом, мы сделаем http-обращение к URL-адресу с использованием AsyncRestTemplate, а затем отправим ответ на их вопрос о том, успешно ли он или нет.

Ниже мой интерфейс:

public interface Client {
    // for synchronous
    public DataResponse executeSync(final DataKey key);

    // for asynchronous
    public Future<DataResponse> executeAsync(final DataKey key);
}

И ниже моя реализация:

public class DataClient implements IClient {

    // does this have to be final?
    private final AsyncRestTemplate restTemplate = new AsyncRestTemplate();

    @Override
    public DataResponse executeSync(final DataKey keys) {
        Future<DataResponse> responseFuture = executeAsync(keys);
        DataResponse response = null;
        try {
            response = responseFuture.get(keys.getTimeout(), TimeUnit.Milliseconds);
        } catch (CancellationException e) {
            // what to do here?
        }  catch (InterruptedException e) {
            // is this right way to deal with InterruptedException?
            throw new RuntimeException("Interrupted", e);
        } catch (ExecutionException e) {
            // what do you mean by ExecutionException? And how should we deal with this?
            DataLogging.logErrors(e.getCause(), DataErrorEnum.ERROR_CLIENT, keys);
            response = new DataResponse(null, DataErrorEnum.ERROR_CLIENT, DataStatusEnum.ERROR);
        } catch (TimeoutException e) {
            DataLogging.logErrors(e.getCause(), DataErrorEnum.TIMEOUT_ON_CLIENT, keys);
            response = new DataResponse(null, DataErrorEnum.TIMEOUT_ON_CLIENT, DataStatusEnum.ERROR);       
        }

        return response;
    }

    @Override
    public Future<DataResponse> executeAsync(final DataKey keys) {
        final SettableFuture<DataResponse> responseFuture = SettableFuture.create();
        restTemplate.exchange(createURL(keys), HttpMethod.GET, keys.getEntity(), String.class).addCallback(
                new ListenableFutureCallback<ResponseEntity<String>>() {
                    @Override
                    public void onSuccess(ResponseEntity<String> result) {
                        responseFuture.set(new DataResponse(result.getBody(), DataErrorEnum.OK,
                                DataStatusEnum.SUCCESS));
                    }

                    @Override
                    public void onFailure(Throwable ex) {
                        DataLogging.logErrors(ex, DataErrorEnum.ERROR_SERVER, keys);
                        responseFuture.set(new DataResponse(null, DataErrorEnum.ERROR_CLIENT,
                                DataStatusEnum.ERROR));
                    }
                });

        return responseFuture;

    }
}

Теперь мой вопрос:

  • Как правильно обрабатывать исключения в блоке catch executeSync? Есть ли разница между CancellationException и TimeoutException? Также, что мы должны делать с ExecutionException в целом?
  • Должен ли мой DataKey быть окончательным в моем интерфейсе? Если я удаляю конечную переменную в моей реализации executeAsync, тогда я получаю ошибку компиляции как Cannot refer to a non-final variable keys inside an inner class defined in a different method.
  • Это правильный способ использования ListenableFutureCallback в моем методе executeAsync? Или есть лучший способ использовать это?

Любые входы/предложения также приветствуются в моем дизайне для реализации синхронизации и асинхронности.

Ответ 1

Я предполагаю, что вы используете Spring 4 (AsyncRestTemplate). В этом случае ListenableFuture, который вы получаете, на самом деле не является Guava ListenableFuture, но он клонируется в Spring. В любом случае вы должны иметь дело с исключениями так же, как и с исключениями из стандартного Future.

Ответы на ваши вопросы:

// does this have to be final? private final AsyncRestTemplate
restTemplate = new AsyncRestTemplate();

Это не так (в данном случае), но это хорошая практика, поскольку, как правило, он делает объект менее изменчивым, упрощающим рассуждения о его поведении.

catch (CancellationException e) {
    // what to do here?
}

CancellationException будет выведено, если задача отменена (либо через Future # cancel, либо ExecutorService # shutdownNow). Это не может произойти в вашем случае, поскольку только у вас есть ссылки на Future и (неявно через приватную AsyncRestTemplate) ExecutorService, используемую для выполнения запросов. Итак,

throw new AssertionError("executeAsync task couldn't be cancelled", e);

Есть ли разница между CancellationException и TimeoutException?

В Future # get call вы указали время ожидания. TimeoutException будет выведено, если результат по-прежнему недоступен после keys.getTimeout() миллисекунд.

catch (InterruptedException e) {
   // is this right way to deal with InterruptedException?
   throw new RuntimeException("Interrupted", e);
}

В этом случае нет. InterruptedException будет выбрано, когда поток клиента будет прерван. Вы не владеете этим потоком, поэтому вам следует распространять InterruptedException (т.е. Объявлять executeSync(DataKey keys) throws InterruptedException). Если по какой-либо причине вы не можете изменить подпись метода, то, по крайней мере, восстановить прерванный флаг (Thread.currentThread().interrupt()), прежде чем бросать RuntimeException.

catch (ExecutionException e) {
   // what do you mean by ExecutionException? And how should we deal with this?
   DataLogging.logErrors(e.getCause(), DataErrorEnum.ERROR_CLIENT, keys);
   response = new DataResponse(null, DataErrorEnum.ERROR_CLIENT, DataStatusEnum.ERROR);
}

ExecutionException означает, что код, представленный ExecutorService как Callable/Runnable, вызывал исключение во время выполнения. В вашем случае ExecutionException никогда не будет выбрано, потому что вы возвращаете SettableFuture со значением, установленным как в обратных вызовах onSuccess, так и onFailure, поэтому вы можете бросить AssertionError в блоке catch. Нет общей стратегии ответа на ExecutionException.

Должен ли мой DataKey быть окончательным в моем интерфейсе?

Он должен быть окончательным в реализации executeAsync, потому что вы ссылаетесь на него с анонимного класса (обратный вызов onFailure);

Является ли это правильным способом использования ListenableFutureCallback в моем методе executeAsync? Или есть лучший способ использовать это?

Не вижу в этом ничего плохого.

Некоторые советы:

  • Рассмотрите возможность создания пула потоков для асинхронного клиента.

По умолчанию AsyncRestTemplate использует SimpleAsyncTaskExecutor, который создает новый поток для каждого запроса. Это может быть неприемлемо для всех ваших клиентов. Обратите внимание, что если вы следуете этому совету, ответ на CancellationException должен быть другим, поскольку клиент теперь может иметь ссылку на ExecutorService: бросать RuntimeException должно быть хорошо.

  1. Описать в (java) пуле потоков doc, используемом по умолчанию!

  2. Я бы разделил версии синхронизации и асинхронизации.

  3. Я думаю, что использование sync RestTemplate и реализация async-версии с помощью версии синхронизации упростило бы реализацию.

  4. Рассмотрите возможность возврата более гибкого ListenableFuture вместо простого будущего (используя SettableListenableFuture вместо SettableFuture).