Повторить HTTP-запрос (Java 11 — HttpClient)

Проблема

Используя HttpClient из Java 11 (JDK, а не Apache), как я могу повторить запросы?

Допустим, я хочу повторить запрос до 10 раз, если он не вернул код состояния 200 или не выдал исключение.


Пытаться

В настоящее время я сочиняю возвращенное будущее с повторными расписаниями в цикле, и мне интересно, может ли быть лучший или более элегантный способ.

CompletableFuture<HttpResponse<Foo>> task = client.sendAsync(request, bodyHandler);

for (int i = 0; i < 10; i++) {
    task = task.thenComposeAsync(response -> response.statusCode() == 200 ?
        CompletableFuture.completedFuture(response) :
        client.sendAsync(request, bodyHandler));
}

// Do something with 'task' ...

И если мы добавим повторные попытки для исключительных случаев, я получу

CompletableFuture<HttpResponse<Foo>> task = client.sendAsync(request, bodyHandler);

for (int i = 0; i < 10; i++) {
    task = task.thenComposeAsync(response ->
            response.statusCode() == 200 ?
            CompletableFuture.completedFuture(response) :
            client.sendAsync(request, bodyHandler))
        .exceptionallyComposeAsync(e ->
            client.sendAsync(request, bodyHandler));
}

// Do something with 'task' ...

К сожалению, похоже, что нет никакого composeAsync, который срабатывает как для обычного завершения, так и для исключительного. Есть handleAsync но не compose, лямбда нужна чтобы вернуть U а не CompletionStage<U> там.


Другие фреймворки

Ради контроля качества меня также интересуют ответы, которые показывают, как добиться этого с другими фреймворками, но я их не приму.

Например, я видел библиотеку под названием Failsafe, которая могла бы предложить изящное решение этой проблемы (см. jodah.net/failsafe).


Ссылка

Для справки, вот некоторые связанные ссылки JavaDoc:


person Zabuzard    schedule 15.07.2020    source источник


Ответы (2)


Вместо этого я бы предложил сделать что-то в этом роде (при условии отсутствия менеджера безопасности):

public static int MAX_RESEND = 10;

public static void main(String[] args) {
    HttpClient client = HttpClient.newHttpClient();
    HttpResponse.BodyHandler<String> handler = HttpResponse.BodyHandlers.ofString();
    HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("http://example.com/")).build();
    var response = client.sendAsync(request, handler)
            .thenComposeAsync(r -> tryResend(client, request, handler, 1, r));
    // do something with response...
}

public static <T> CompletableFuture<HttpResponse<T>>
        tryResend(HttpClient client, HttpRequest request, BodyHandler<T> handler,
                 int count, HttpResponse<T> resp) {
    if (resp.statusCode() == 200 || count >= MAX_RESEND) {
        return CompletableFuture.completedFuture(resp);
    } else {
        return client.sendAsync(request, handler)
                .thenComposeAsync(r -> tryResend(client, request, handler, count+1, r));
    }
}

И если вы хотите обрабатывать как обычный, так и исключительный случай, вы можете сделать что-то вроде:

public static int MAX_RESEND = 5;

public static void main(String[] args) {
    HttpClient client = HttpClient.newHttpClient();
    BodyHandler<String> handler = BodyHandlers.ofString();
    HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("http://example.com/")).build();
    CompletableFuture<HttpResponse<String>> response = 
        client.sendAsync(request, handler)
            .handleAsync((r, t) -> tryResend(client, request, handler, 1, r, t))
            .thenCompose(Function.identity());
    // do something with response ...
}

public static boolean shouldRetry(HttpResponse<?> r, Throwable t, int count) {
    if (r != null && r.statusCode() == 200 || count >= MAX_RESEND) return false;
    if (t instanceof ... ) return false;
    return true;
}

public static <T> CompletableFuture<HttpResponse<T>>
        tryResend(HttpClient client, HttpRequest request,
                  BodyHandler<T> handler, int count,
                  HttpResponse<T> resp, Throwable t) {
    if (shouldRetry(resp, t, count)) {
        return client.sendAsync(request, handler)
                .handleAsync((r, x) -> tryResend(client, request, handler, count + 1, r, x))
                .thenCompose(Function.identity());
    } else if (t != null) {
        return CompletableFuture.failedFuture(t);
    } else {
        return CompletableFuture.completedFuture(resp);
    }
}
person daniel    schedule 15.07.2020
comment
Для меня это похоже на то же самое, но с использованием рекурсии вместо цикла для создания той же композиции фьючерсов, не так ли? - person Zabuzard; 15.07.2020
comment
Извините, я неправильно прочитал исходный предложенный код - на второй взгляд я думаю, что он сработает. Разница в том, что с моим предложением вы не будете заранее складывать все эти зависимые действия. Ради аргумента измените 10 на 10000, и вы поймете, что я имею в виду. - person daniel; 15.07.2020
comment
И, кстати, это не рекурсивно, так как все асинхронно: это просто планирование повторной отправки. Не связанное с этим примечание: если вы хотите обрабатывать как исключительный статус, так и статус одним и тем же методом, вы также можете использовать CF::handle. - person daniel; 15.07.2020

вы можете использовать import org.eclipse.microprofile.faulttolerance.Retry;

если вы используете микропрофилер или ищите аналог

@Повторить попытку

person Idriss Sakhi    schedule 15.07.2020