背景
feign可以配置重試策略及超時時間,但是無法根據業務場景動態的設置。可能會引起接口冪等,無效重試資源耗費,大數據量耗時操作報超時異常等問題。所以需要更細粒度的重試策略及超時時間配置。
自定義重試策略
框架會使用容器中Retryer
和Request.Options
類型的配置Bean構造對應的feignClient Bean, 后續使用的時候可以直接通過@Autowired
注入即可發起調用;
若要進行更加靈活的控制feign,也可以手動構造FeignClient,通過構造時設置Retryer
和Request.Options
可以達到 feign class 級別控制粒度;
引入全局配置Bean
由於構造FeignClient需要依賴一些Bean,所以先構造全局配置Bean;
@Slf4j
@Configuration
public class FeignAutoConfiguration {
public static final int CONNECT_TIME_OUT_MILLIS = 5000;
public static final int READ_TIME_OUT_MILLIS = 12000;
@Autowired(required = false)
private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();
@Bean
public Encoder encoder(ObjectFactory<HttpMessageConverters> messageConverters) {
Encoder encoder = new SpringEncoder(messageConverters);
return encoder;
}
@Bean
public Decoder decoder(ObjectFactory<HttpMessageConverters> messageConverters) {
Decoder decoder = new SpringDecoder(messageConverters);
return decoder;
}
@Bean
public Contract feignContract(@Qualifier("mvcConversionService") ConversionService feignConversionService) {
return new SpringMvcContract(this.parameterProcessors, feignConversionService);
}
//全局超時配置
@Bean
public Request.Options options() {
return new Request.Options(CONNECT_TIME_OUT_MILLIS, READ_TIME_OUT_MILLIS);
}
//全局重試策略
@Bean
public Retryer feignRetryer() {
return Retryer.NEVER_RETRY;
}
}
手動構造FeignClient
根據上述的配置類,構造自定義FeignClient;
此配置需要在調用方服務中定義,直接復制該配置類,根據需要模仿customRoleClient()
方法實現
// 引入全局配置
@Import(value = {FeignAutoConfiguration.class})
@Configuration
public class CustomFeignClientConfiguration {
@Qualifier("feignClient")
@Autowired
private Client client;
@Autowired
private Encoder encoder;
@Autowired
private Decoder decoder;
@Autowired
private Contract contract;
@Autowired
private Request.Options options;
@Autowired
private Retryer retryer;
/**
* 自定義RoleClient; 【后續擴展自定義Feign的模仿本方法配置即可】
*
* @return
*/
@Bean
public RoleClient customRoleClient() {
//自定義超時時間,connectTimeout 5s ; readTimeout 10s;
Request.Options options = new Request.Options(5, TimeUnit.SECONDS, 10, TimeUnit.SECONDS, true);
//重試2次
Retryer.Default retryer = new Retryer.Default(100, SECONDS.toMillis(1), 2);
return getCustomFeignClient(RoleClient.class, options, retryer);
}
/**
* 手動構建feignClient工具方法
*
* @param clazz
* @param options
* @param retryer
* @param <T>
* @return
*/
private <T> T getCustomFeignClient(Class<T> clazz, Request.Options options, Retryer retryer) {
//只需要對其中的超時和重試配置自定義,其他的還需要使用全局配置
//通過反射獲取@FeignClient注解
FeignClient annotation = clazz.getAnnotation(FeignClient.class);
return Feign.builder()
.client(client)
.options(options == null ? this.options : options)
.retryer(retryer == null ? this.retryer : retryer)
.contract(contract)
.encoder(encoder)
.decoder(decoder)
.target(clazz, "http://" + annotation.value());
}
}
使用自定義FeignClient
由於框架會根據全局配置構造一個FeignClientBean, 上述步驟又手動構造了一個Bean,容器中存在兩個相同類型RoleClient
的Bean。
使用@Autowired
注入需要添加@Qualifier("customRoleClient")
標識唯一Bean 。
可以使用@Resource
注解,優先根據beanName注入。
// 注入
@Resource
private RoleClient roleClient;
@Resource
private RoleClient customRoleClient;
public void checkRoleDataAuth(String roleId){
// 使用時直接替換feignClient即可
// ResultBody resultBody = roleClient.checkRoleDataAuth(roleId);
ResultBody resultBody = customRoleClient.checkRoleDataAuth(roleId);
if (!resultBody.isSuccess()){
throw new BaseException(resultBody.getCode(),resultBody.getMessage());
}
}
自定義超時時間
在處理大數據量、大文件以、統計等耗時任務時需要自定義超時時間,防止出現feign調用超時異常。
feignClient粒度的自定義超時
根據上文的描述,可以自定義FeignClientBean,從而將超時時間控制在client Bean粒度。
方法粒度的自定義超時
feign方法調用邏輯
feign.SynchronousMethodHandler#invoke
方法源碼
//feign方法調用實現
@Override
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
//獲取當前方法的Request.Options超時配置
Options options = findOptions(argv);
Retryer retryer = this.retryer.clone();
while (true) {
try {
//方法調用
return executeAndDecode(template, options);
} catch (RetryableException e) {
//重試
try {
retryer.continueOrPropagate(e);
} catch (RetryableException th) {
Throwable cause = th.getCause();
if (propagationPolicy == UNWRAP && cause != null) {
throw cause;
} else {
throw th;
}
}
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}
//從feignClient方法參數列表中找到Request.Options實例對象
Options findOptions(Object[] argv) {
// 如果方法沒有參數,使用client配置
if (argv == null || argv.length == 0) {
return this.options;
}
//查找並使用參數列表的Request.Options,若不存在則使用client配置
return Stream.of(argv)
.filter(Options.class::isInstance)
.map(Options.class::cast)
.findFirst()
.orElse(this.options);
}
方法定義
基於以上的代碼分析,可以在feign方法簽名中參數列表增加一個Request.Options
參數,在調用的時候動態構建Request.Options
對象傳入;
@FeignClient(value = UserConstants.SERVER_NAME)
public interface RoleClient {
@GetMapping(value = "/openfeign/role/checkRoleDataAuth")
ResultBody checkRoleDataAuth(@RequestParam("roleId") String roleId, Request.Options options);
}
方法調用
//自定義超時時間,connectTimeout 5s ; readTimeout 60s;
Request.Options options = new Request.Options(5, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true);
ResultBody resultBody = roleClient.checkRoleDataAuth(roleId, options);
//傳入null,使用client中的超時配置
ResultBody resultBody = roleClient.checkRoleDataAuth(roleId, null);