【问题标题】:Spring WebClient - Stop retrying if an exception is thrown in the doOnErrorSpring WebClient - 如果在 doOnError 中抛出异常,则停止重试
【发布时间】:2021-12-18 02:06:10
【问题描述】:

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

我想做的是:

  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-boot spring-webflux project-reactor spring-webclient


    【解决方案1】:

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

    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捕获,并且不再重试。

    【讨论】:

      猜你喜欢
      • 2020-09-02
      • 1970-01-01
      • 1970-01-01
      • 2010-10-09
      • 2021-09-03
      • 1970-01-01
      • 1970-01-01
      • 2020-02-13
      • 1970-01-01
      相关资源
      最近更新 更多