feign调用如何配置单个接口的超时时间
通过配置文件配置超时时间
feign:
hystrix:
enabled: true
client:
config:
default:
readTimeout: 2000
connectTimeout: 2000
xxx-service:
readTimeout: 2000
connectTimeout: 2000
一般常用全局默认的超时和根据服务名设置的超时,但是缺点是一个FeignClient往往有多个方法,而可能某个方法需要较长的超时时间,这种配置的方式就无法满足需求。但是可以通过增加一个FeignClient且只有1个需要单独配置超时方法的接口,加上配置@FeignClient注解的contextId。这样就可以为单个接口配置超时时间。显然这种方式不够优雅。
通过feign动态代理解析参数实现
feign.ReflectiveFeign.FeignInvocationHandler#invoke --> feign.SynchronousMethodHandler
FeignInvocationHandler#invoke方法,通过Method的反射,调用方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("equals".equals(method.getName())) {
try {
Object otherHandler =
args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
} catch (IllegalArgumentException e) {
return false;
}
} else if ("hashCode".equals(method.getName())) {
return hashCode();
} else if ("toString".equals(method.getName())) {
return toString();
}
return dispatch.get(method).invoke(args);
}
处理器 feign.SynchronousMethodHandler关于超时时间配置参数的处理
Options findOptions(Object[] argv) {
if (argv == null || argv.length == 0) {
return this.options;
}
return Stream.of(argv)
.filter(Options.class::isInstance)
.map(Options.class::cast)
.findFirst()
.orElse(this.options);
}
因此可知使用方式为
@GetMapping("/hello")
String sayHello(Request.Options options);
通过自定义注解
启动时
配置文件 --> FeignClientProperties --> FeignClientConfiguration --> FeignClientFactoryBean#configureUsingProperties --> feign.Request.Options#Options
根据FeignClientFactoryBean#configureUsingProperties的引用可知,default的配置优先级是低于根据contextId的(也就是服务名),核心代码如下:
//一般会走这个分支的代码
configureUsingConfiguration(context, builder);
configureUsingProperties(
properties.getConfig().get(properties.getDefaultConfig()),
builder);
configureUsingProperties(properties.getConfig().get(this.contextId),
builder);
超时时间是从config中构建的Options
if (config.getConnectTimeout() != null && config.getReadTimeout() != null) {
builder.options(new Request.Options(config.getConnectTimeout(),
config.getReadTimeout()));
}
看不出来启动时内部那里可以扩展,修改单个接口的超时时间配置。因为框架设计超时的配置粒度是服务,并不是服务的接口
运行时
自编写一个demo,并进行debug
定位到发送请求的代码feign.Client.Default#convertAndSend,关于超时时间的代码如下:
final HttpURLConnection connection = this.getConnection(url);
connection.setConnectTimeout(options.connectTimeoutMillis());
connection.setReadTimeout(options.readTimeoutMillis());
由于项目中刚好接入了sleuth,可以看到多了一个TraceFeignBlockingLoadBalancerClient。其他的则是Feign自身实现的Client。整个栈可以看出Client的嵌套调用,明显就是使用了责任链模式。一个想法就是,我们可以自定义自己的Client,加入调用链中,干涉远程调用时Option的超时时间设置。
如何把自己的Client加入到调用链中?可以参考sleuth的实现
TraceFeignBlockingLoadBalancerClient --> TraceFeignObjectWrapper#wrap -->TraceFeignContext#getInstance --> FeignClientFactoryBean#getOptional -->FeignClientFactoryBean#loadBalance --> FeignContext --> FeignContextBeanPostProcessor
通过wrapper,包装是Client的bean
if (bean instanceof Client && !(bean instanceof TracingFeignClient)) {
if (ribbonPresent && bean instanceof LoadBalancerFeignClient
&& !(bean instanceof TraceLoadBalancerFeignClient)) {
return instrumentedFeignRibbonClient(bean);
}
if (ribbonPresent && bean instanceof TraceLoadBalancerFeignClient) {
return bean;
}
if (loadBalancerPresent && bean instanceof FeignBlockingLoadBalancerClient
&& !(bean instanceof TraceFeignBlockingLoadBalancerClient)) {
return instrumentedFeignLoadBalancerClient(bean);
}
if (ribbonPresent && bean instanceof TraceFeignBlockingLoadBalancerClient) {
return bean;
}
return new LazyTracingFeignClient(this.beanFactory, (Client) bean);
}
而源头又指向了FeignClientFactoryBean,方法FeignClientFactoryBean#loadBalance通过contextId创建出我们的FeignClient
<T> T getTarget() {
FeignContext context = this.applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
if (!StringUtils.hasText(this.url)) {
if (!this.name.startsWith("http")) {
this.url = "http://" + this.name;
}
else {
this.url = this.name;
}
this.url += cleanPath();
return (T) loadBalance(builder, context,
new HardCodedTarget<>(this.type, this.name, this.url));
}
...
在debug中显示FeignContext的运行时类型是TraceFeignContext,是在FeignContextBeanPostProcessor处理FeignContext的bean时进行替换的
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof FeignContext && !(bean instanceof TraceFeignContext)) {
return new TraceFeignContext(traceFeignObjectWrapper(), (FeignContext) bean);
}
return bean;
}
答案已经显而易见了,我们可以学习sleuth,在处理FeignContext时,再进行一次wrapper,通过wrapper植入我们的Client的逻辑。下面就是实际代码环节。
实现注解配置超时时间
提供一个FeignOptions注解,可以注解在FeignClient的方法上,配置超时时间
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface FeignOptions {
long connectTimeout();
TimeUnit connectTimeoutUnit() default TimeUnit.MILLISECONDS;
long readTimeout();
TimeUnit readTimeoutUnit() default TimeUnit.MILLISECONDS;
}
后续就是根据sleuth依葫芦画瓢,提供一个BeanPostProcessor
@Order(100)
@Component
public class FeignOptionsBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {
private BeanFactory beanFactory;
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof FeignContext && !(bean instanceof OptionsFeignContext)) {
return new OptionsFeignContext(feignOptionsObjectWrapper(), (FeignContext) bean);
}
return bean;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
private FeignOptionsObjectWrapper feignOptionsObjectWrapper() {
return new FeignOptionsObjectWrapper(this.beanFactory);
}
}
提供一个自定义的FeignContext
public class OptionsFeignContext extends FeignContext {
private final FeignOptionsObjectWrapper feignOptionsObjectWrapper;
private final FeignContext delegate;
public OptionsFeignContext(FeignOptionsObjectWrapper traceFeignObjectWrapper,
FeignContext delegate) {
this.feignOptionsObjectWrapper = traceFeignObjectWrapper;
this.delegate = delegate;
}
@Override
@SuppressWarnings("unchecked")
public <T> T getInstance(String name, Class<T> type) {
T object = this.delegate.getInstance(name, type);
if (object != null) {
return (T) this.feignOptionsObjectWrapper.wrap(object);
}
return null;
}
@Override
@SuppressWarnings("unchecked")
public <T> Map<String, T> getInstances(String name, Class<T> type) {
Map<String, T> instances = this.delegate.getInstances(name, type);
if (instances == null) {
return null;
}
Map<String, T> convertedInstances = new HashMap<>();
for (Map.Entry<String, T> entry : instances.entrySet()) {
convertedInstances.put(entry.getKey(),
(T) this.feignOptionsObjectWrapper.wrap(entry.getValue()));
}
return convertedInstances;
}
}
提供一个wrapper
public class FeignOptionsObjectWrapper {
private final BeanFactory beanFactory;
public FeignOptionsObjectWrapper(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
public Object wrap(Object bean) {
if (bean instanceof Client) {
return new CustomizeOptionsFeignClient((Client) bean);
}
return bean;
}
}
提供一个自定义的FeignClient
public class CustomizeOptionsFeignClient implements Client {
private Client delegate;
private static Map<Method, Request.Options> cacheFeignOptionsMap = new ConcurrentHashMap<>();
public CustomizeOptionsFeignClient(Client delegate) {
this.delegate = delegate;
}
@Override
public Response execute(Request request, Request.Options options) throws IOException {
Method method = request.requestTemplate().methodMetadata().method();
if (method.isAnnotationPresent(FeignOptions.class)) {
Request.Options customizeOptions = cacheFeignOptionsMap.computeIfAbsent(method, m -> {
FeignOptions feignOptions = method.getAnnotation(FeignOptions.class);
return new Request.Options(feignOptions.connectTimeout(),
feignOptions.connectTimeoutUnit(),
feignOptions.readTimeout(),
feignOptions.readTimeoutUnit(),
options.isFollowRedirects());
});
return this.delegate.execute(request, customizeOptions);
}else {
return this.delegate.execute(request, options);
}
}
}
使用
@FeignOptions(connectTimeout = 3000, readTimeout = 3000)
@GetMapping("/hello")
String sayHello();
BUG!!!
使用上述的方式会产生一个BUG,具体产生原因为:
DefaultFeignLoadBalancerConfiguration#feignClient
//默认获得的是FeignBlockingLoadBalancerClient
@Bean
@ConditionalOnMissingBean
public Client feignClient(BlockingLoadBalancerClient loadBalancerClient) {
return new FeignBlockingLoadBalancerClient(new Client.Default(null, null),
loadBalancerClient);
}
FeignClientFactoryBean#getTarget
<T> T getTarget() {
FeignContext context = this.applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
if (!StringUtils.hasText(this.url)) {
if (!this.name.startsWith("http")) {
this.url = "http://" + this.name;
}
else {
this.url = this.name;
}
this.url += cleanPath();
return (T) loadBalance(builder, context,
new HardCodedTarget<>(this.type, this.name, this.url));
}
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url;
}
//当指定具体url时,不走loadbalance
//根据上述代码进行包装之后,类型变成了CustomizeOptionsFeignClient
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
// not load balancing because we have a url,
// but ribbon is on the classpath, so unwrap
client = ((LoadBalancerFeignClient) client).getDelegate();
}
//问题在于这里,指定url之后,会解包FeignBlockingLoadBalancerClient
//但是此时类型为CustomizeOptionsFeignClient,无法进入到if逻辑解包。
//所以执行的时候feign调用还是会去查找注册中心,写死url的一般不会注册到注册中心,所以会调用失败
if (client instanceof FeignBlockingLoadBalancerClient) {
// not load balancing because we have a url,
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(this.type, this.name, url));
}
最终代码集合
//注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface FeignOptions {
long connectTimeout();
TimeUnit connectTimeoutUnit() default TimeUnit.MILLISECONDS;
long readTimeout();
TimeUnit readTimeoutUnit() default TimeUnit.MILLISECONDS;
}
//自定义Client逻辑
public class CustomizeOptionsFeignClient implements Client {
private Client delegate;
private static Map<Method, Request.Options> cacheFeignOptionsMap = new ConcurrentHashMap<>();
public CustomizeOptionsFeignClient(Client delegate) {
this.delegate = delegate;
}
@Override
public Response execute(Request request, Request.Options options) throws IOException {
Method method = request.requestTemplate().methodMetadata().method();
if (method.isAnnotationPresent(FeignOptions.class)) {
Request.Options customizeOptions = cacheFeignOptionsMap.computeIfAbsent(method, m -> {
FeignOptions feignOptions = method.getAnnotation(FeignOptions.class);
return new Request.Options(feignOptions.connectTimeout(),
feignOptions.connectTimeoutUnit(),
feignOptions.readTimeout(),
feignOptions.readTimeoutUnit(),
options.isFollowRedirects());
});
return this.delegate.execute(request, customizeOptions);
}else {
return this.delegate.execute(request, options);
}
}
}
//注册Bean
@Configuration
public class FeignOptionsConfig {
@Bean
public Client feignClient(BlockingLoadBalancerClient loadBalancerClient) {
return new FeignBlockingLoadBalancerClient(new CustomizeOptionsFeignClient(new Client.Default(null, null)),
loadBalancerClient);
}
}
扩展总结
由于FeignClient是通过FeignClientFactoryBean生成的,所以BeanPostProcessor无法控制FeignClient。而只能在框架内部寻找扩展点。既然是扩展点,那一定是交给Spring管理的才能进行扩展。FeignClientFactoryBean聚合了ApplicationContext,内部也通过容器去getBean。所有通过getBean的,就都是可以扩展的地方。这也是为什么sleuth框架选择了扩展点为FeignContext,具体代码参考FeignClientFactoryBean#getTarget
<T> T getTarget() {
FeignContext context = this.applicationContext.getBean(FeignContext.class);
...
}
后面发现,不仅仅有这种方式,超时配置可以在很多地方配置,只是配置的优先级不一样。比如,通过hystrix也可以实现超时时间的配置。