首页 > 解决方案 > Spring WebClient - 如果在 doOnError 中引发异常,则停止重试

问题描述

我有以下代码来发出将被重试最大次数的请求。此请求需要一个授权标头,我正在缓存此信息以防止此方法每次都调用该方法来检索此信息。

我想做的是:

  1. 调用 myMethod 时,我首先检索正在调用的服务的登录信息,在大多数情况下,这些信息将来自调用 getAuthorizationHeaderValue 方法时的缓存。
  2. 在 Web 客户端中,如果发送此请求的响应返回 4xx 响应,我需要在重试请求之前再次登录到我正在调用的服务。为此,我调用 tryToLoginAgain 方法再次设置标头的值。
  3. 之后,请求的重试应该可以工作了,因为已经设置了标头。
  4. 如果再次登录调用失败,我需要停止重试,因为重试请求没有用。
public <T> T myMethod(...) {
    ...

    try {
        AtomicReference<String> headerValue = new AtomicReference<>(loginService.getAuthorizationHeaderValue());

        Mono<T> monoResult = webclient.get()
                .uri(uri)
                .accept(MediaType.APPLICATION_JSON)
                .header(HttpHeaders.AUTHORIZATION, headerValue.get())
                .retrieve()
                .onStatus(HttpStatus::is4xxClientError, response -> throwHttpClientLoginException())
                .bodyToMono(type)
                .doOnError(HttpClientLoginException.class, e -> tryToLoginAgain(headerValue))
                .retryWhen(Retry.backoff(MAX_NUMBER_RETRIES, Duration.ofSeconds(5)));

        result = monoResult.block();
    } catch(Exception e) {
        throw new HttpClientException("There was an error while sending the request");
    }
    return result;
}

...

private Mono<Throwable> throwHttpClientLoginException() {
    return Mono.error(new HttpClientLoginException("Existing Authorization failed"));
}

private void tryToLoginAgain(AtomicReference<String> headerValue) {
    loginService.removeAccessTokenFromCache();
    
    headerValue.set(loginService.getAuthorizationHeaderValue());
}

我有一些单元测试并且快乐路径工作正常(第一次未经授权,尝试再次登录并再次发送请求)但是登录根本不起作用的场景不起作用。

我认为如果 tryToLoginAgain 方法抛出一个异常,该异常将被我在 myMethod 中的 catch 捕获但它永远不会到达那里,它只是再次重试请求。有什么办法可以做我想做的事吗?

标签: spring-bootspring-webfluxproject-reactorspring-webclient

解决方案


所以最后我找到了一种做我想做的事情的方法,现在代码看起来像这样:

public <T> T myMethod() {
    try {
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(getAuthorizationHeaderValue());

        final RetryBackoffSpec retrySpec = Retry.backoff(MAX_NUMBER_RETRIES, Duration.ofSeconds(5))
            .doBeforeRetry(retrySignal -> {
                //When retrying, if this was a login error, try to login again
                if (retrySignal.failure() instanceof HttpClientLoginException) {
                    tryToLoginAgain(headers);
                }
            });

        Mono<T> monoResult = Mono.defer(() ->
                getRequestFromMethod(httpMethod, uri, body, headers)
                    .retrieve()
                    .onStatus(HttpStatus::is4xxClientError, response -> throwHttpClientLoginException())
                    .bodyToMono(type)
            )
            .retryWhen(retrySpec);

        result = monoResult.block();
    } catch (Exception e) {
        String requestUri = uri != null ?
            uri.toString() :
            endpoint;

        log.error("There was an error while sending the request [{}] [{}]", httpMethod.name(), requestUri);

        throw new HttpClientException("There was an error while sending the request [" + httpMethod.name() +
            "] [" + requestUri + "]");
    }

    return result;
}

private void tryToLoginAgain(HttpHeaders httpHeaders) {
    //If there was an 4xx error, let's evict the cache to remove the existing access_token (if it exists)
    loginService.removeAccessTokenFromCache();
    //And let's try to login again
    httpHeaders.setBearerAuth(getAuthorizationHeaderValue());
}

private Mono<Throwable> throwHttpClientLoginException() {
    return Mono.error(new HttpClientLoginException("Existing Authorization failed"));
}

private WebClient.RequestHeadersSpec getRequestFromMethod(HttpMethod httpMethod, URI uri, Object body, HttpHeaders headers) {
    switch (httpMethod) {
        case GET:
            return webClient.get()
                .uri(uri)
                .headers(httpHeaders -> httpHeaders.addAll(headers))
                .accept(MediaType.APPLICATION_JSON);
        case POST:
            return body == null ?
                webClient.post()
                    .uri(uri)
                    .headers(httpHeaders -> httpHeaders.addAll(headers))
                    .accept(MediaType.APPLICATION_JSON) :
                webClient.post()
                    .uri(uri)
                    .headers(httpHeaders -> httpHeaders.addAll(headers))
                    .accept(MediaType.APPLICATION_JSON)
                    .contentType(MediaType.APPLICATION_JSON)
                    .bodyValue(body);
        case PUT:
            return body == null ?
                webClient.put()
                    .uri(uri)
                    .headers(httpHeaders -> httpHeaders.addAll(headers))
                    .accept(MediaType.APPLICATION_JSON) :
                webClient.put()
                    .uri(uri)
                    .headers(httpHeaders -> httpHeaders.addAll(headers))
                    .accept(MediaType.APPLICATION_JSON)
                    .contentType(MediaType.APPLICATION_JSON)
                    .bodyValue(body);
        case DELETE:
            return webClient.delete()
                .uri(uri)
                .headers(httpHeaders -> httpHeaders.addAll(headers))
                .accept(MediaType.APPLICATION_JSON);
        default:
            log.error("Method [{}] is not supported", httpMethod.name());
            throw new HttpClientException("Method [" + httpMethod.name() + "] is not supported");
    }
}

private String getAuthorizationHeaderValue() {
    return loginService.retrieveAccessToken();
}

通过使用Mono.defer(),我可以重试该 Mono 并确保更改将与 WebClient 一起使用的标头。重试规范将检查异常是否属于HttpClientLoginException在请求获得 4xx 状态码时抛出的异常类型,在这种情况下,它将尝试再次登录并设置下次重试的标头。如果状态码不同,它将使用相同的授权重试。

此外,如果我们再次尝试登录时出现错误,那将被 catch 捕获并且不再重试。


推荐阅读