Contents

Feign 重试

1. 简介

通过 REST 端点调用外部服务是一种常见的活动,像Feign 这样的库非常简单。但是,在此类通话期间,很多事情都可能出错。其中许多问题是随机的或暂时的。

在本教程中,我们将学习如何重试失败的调用并创建更具弹性的 REST 客户端。

2. Feign 客户端设置

首先,让我们创建一个简单的 Feign 客户端构建器,稍后我们将通过重试功能对其进行增强。我们将使用OkHttpClient 作为 HTTP 客户端。此外,我们将使用GsonEncoderGsonDecoder对请求和响应进行编码和解码。最后,我们需要指定目标的 URI 和响应类型:

public class ResilientFeignClientBuilder {
    public static <T> T createClient(Class<T> type, String uri) {
        return Feign.builder()
          .client(new OkHttpClient())
          .encoder(new GsonEncoder())
          .decoder(new GsonDecoder())
          .target(type, uri);
    }
}

或者,如果我们使用 Spring,我们可以让它使用可用的 bean 自动连接 Feign 客户端。

3. 假Retryer

幸运的是,重试能力是在 Feign 中烘焙的,只需要配置它们。我们可以通过向客户端构建器提供Retryer接口的实现来做到这一点。

它最重要的方法continueOrPropagate接受 RetryableException作为参数并且不返回任何内容。执行时,它要么抛出异常,要么成功退出(通常在休眠后)。**如果没有抛出异常,Feign 会继续重试调用。**如果抛出异常,它将被传播并有效地完成调用并出现错误。

3.1. 简单的实现

让我们编写一个非常简单的 Retryer 实现,它总是会在等待一秒钟后重试调用:

public class NaiveRetryer implements feign.Retryer {
    @Override
    public void continueOrPropagate(RetryableException e) {
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
            throw e;
        }
    }
}

因为Retryer实现了Cloneable接口,所以我们还需要重写clone方法。

@Override
public Retryer clone() {
    return new NaiveRetryer();
}

最后,我们需要将我们的实现添加到客户端构建器中:

public static <T> T createClient(Class<T> type, String uri) {
    return Feign.builder()
      // ...
      .retryer(new NaiveRetryer())    
      // ...
}

或者,如果我们使用 Spring,我们可以使用注解@Component 来注解NaiveRetryer ,或者在配置类中定义一个bean ,让 Spring 完成剩下的工作:

@Bean
public Retryer retryer() {
    return new NaiveRetryer();
}

3.2. 默认实现

Feign 提供了Retryer接口的合理默认实现。它只会重试给定的次数,从某个时间间隔开始,然后随着每次重试将其增加到提供的最大值。 让我们定义它的起始间隔为 100 毫秒,最大间隔为 3 秒,最大尝试次数为 5:

public static <T> T createClient(Class<T> type, String uri) {
    return Feign.builder()
// ...
      .retryer(new Retryer.Default(100L, TimeUnit.SECONDS.toMillis(3L), 5))    
// ...
}

3.3. 不重试

如果我们不希望 Feign 重试任何调用,我们可以为客户端构建器提供Retryer.NEVER_RETRY实现。它每次都会简单地传播异常。

4. 创建可重试异常

在上一节中,我们学会了控制重试调用的频率。现在让我们看看如何控制何时重试调用以及何时简单地抛出异常。

4.1. ErrorDecoderRetryableException

当我们收到错误的响应时,Feign 将其传递给ErrorDecoder接口的一个实例,该接口决定如何处理它。最重要的是,解码器可以将异常映射到 RetryableException 的实例,使Retryer能够重试调用。ErrorDecoder的默认实现仅在响应包含“Retry-After”标头时创建RetryableException实例。**最常见的是,我们可以在 503 Service Unavailable 响应中找到它。

这是很好的默认行为,但有时我们需要更加灵活。例如,我们可能正在与一个外部服务通信,该服务不时随机响应 500 Internal Server Error,而我们无权修复它。我们能做的就是重试调用,因为我们知道它下次可能会起作用。为此,我们需要编写一个自定义的ErrorDecoder实现。

4.2. 创建自定义错误解码器

我们只需要在自定义解码器中实现一种方法:decode。它接受两个参数,一个String方法键和一个Response对象。它返回一个异常,它应该是RetryableException的实例还是其他依赖于实现的异常。

我们的decode方法将简单地检查响应的状态码是否高于或等于 500。如果是这种情况,它将创建RetryableException。如果没有,它将返回使用FeignException类的errorStatus工厂函数创建的基本FeignException

public class Custom5xxErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {
        FeignException exception = feign.FeignException.errorStatus(methodKey, response);
        int status = response.status();
        if (status >= 500) {
            return new RetryableException(
              response.status(),
              exception.getMessage(),
              response.request().httpMethod(),
              exception,
              null,
              response.request());
        }
        return exception;
    }
}

请注意,在这种情况下,我们创建并返回异常,而不是抛出它。

最后,我们需要在客户端构建器中插入我们的解码器:

public static <T> T createClient(Class<T> type, String uri) {
    return Feign.builder()
      // ...
      .errorDecoder(new Custom5xxErrorDecoder())
      // ...
}