概述

通过REST端点调用外部服务是常见的操作,Feign库使得这一过程变得简单易行。然而,在这些调用过程中可能会遇到各种问题,许多问题是随机或暂时的。本教程将介绍如何处理失败的调用,创建更健壮的REST客户端。

1. 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,可以让它自动将Feign客户端与可用的bean绑定。

2. Feign的重试器

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

其最重要的方法continueOrPropagate接受一个RetryableException参数并返回无值。在执行时,它要么抛出异常,要么成功退出(通常是在休眠后)。如果它不抛出异常,Feign将继续尝试调用。如果抛出异常,它将被传播,并最终以错误结束调用。

2.1. 简单实现

让我们编写一个非常简单的重试器实现,每次失败后都会等待一秒钟再尝试:

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,可以将NaiveRetryer注解为@Component注解,或者在配置类中定义一个bean,让Spring完成其余工作:

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

2.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))    
// ...
}

2.3. 不进行重试

如果我们不想让Feign对任何调用进行重试,可以在客户端构建器中提供Retryer.NEVER_RETRY实现。它每次都会直接传播异常。

3. 创建可重试的异常

在上一节中,我们学习了如何控制重试频率。现在来看看如何决定何时重试调用,何时抛出异常。

3.1. ErrorDecoderRetryableException

当收到错误响应时,Feign会将其传递给一个ErrorDecoder接口的实例,由该实例决定如何处理。最重要的是,解码器可以将异常映射为RetryableException实例,从而让Retryer重试调用。默认的ErrorDecoder实现只有在响应包含"Retry-After"头时才会创建RetryableExeception实例。通常,我们会在503服务不可用的响应中找到它。

这是合理的默认行为,但有时我们需要更具灵活性。例如,我们可能与一个外部服务通信,它偶尔会随机返回500内部服务器错误,而我们无法修复。在这种情况下,我们可以重试调用,因为我们知道下次可能会正常工作。为了实现这一点,我们需要编写自定义的ErrorDecoder实现。

3.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())
      // ...
}

4. 总结

在这篇文章中,我们学习了如何控制Feign库的重试逻辑。我们探讨了Retryer接口及其如何用于操纵重试的时间和次数。然后我们创建了一个自定义的ErrorDecoder来控制哪些响应值得重试。

如往常一样,所有代码示例均可在GitHub上找到。