一、问题
我们的服务采用OpenFeign作为服务间的调用组件,业务服务调用存储服务某个接口时,报错 timeout executing
我们使用的是全局默认配置,没有为每个client 配置单独的configuration ,查看了一下我们的配置如下:
yml
# feign 相关配置
feign:
sentinel:
enabled: true
okhttp:
enabled: true
httpclient:
enabled: false
client:
config:
default:
connectTimeout: 120000
readTimeout: 120000
该接口执行时间大约5分钟,所以需要给该服务单独配置重新配置修改如下所示:
yml
feign:
sentinel:
enabled: true
okhttp:
enabled: true
httpclient:
enabled: false
client:
config:
# 大文件解压操作有些耗时设置成10分钟
micro-storage-biz:
connectTimeout: 120000
readTimeout: 600000
default:
connectTimeout: 120000
readTimeout: 120000
这样修改之后并没有生效,我的 FeignClient
定义如下:
java
@FeignClient(contextId = "remoteFileService", value = "micro-storage-biz")
public interface RemoteFileService {
二、解决方案
通过试错之后,修改了生效的配置
yml
feign:
sentinel:
enabled: true
okhttp:
enabled: true
httpclient:
enabled: false
client:
config:
# 大文件解压操作有些耗时设置成10分钟
remoteFileService:
connectTimeout: 120000
readTimeout: 600000
default:
connectTimeout: 120000
readTimeout: 120000
这个配置和上述配置的区别在于,将定义的 FeignClient
中的value 换成了 contextId。
先说结论,如果你的 FeignClient
contextId 的值不为空,yml中必须配置值contextId 对应的值,如果只是配置了name 或者value ,在yml中配置对应的值即可
三、FeignClient 注解属性分析
- name/value:定义当前客户端Client的名称,用于指定注册中心中的服务名(即调用的服务)。也可以用于日志分组等。通常推荐设置为服务注册名。
- contextId:Spring Bean 的上下文 ID(当存在多个同名
name
时用于区分 Bean 名称)。从 Spring Cloud 2020+ 强制唯一,推荐设置 - url:直接指定远程服务的 URL,会绕过服务注册中心。用于测试或特殊调用。支持
${}
占位符 - path:所有方法的统一前缀路径,比如设置
/api/v1
,所有方法都会加上这个前缀 - fallback:指定一个实现了接口的类,在远程服务调用失败时提供降级逻辑(需配合 Hystrix 或 Sentinel)
- fallbackFactory:类似于
fallback
,但可以获取异常信息(比如:日志记录或异常判断处理)。优先级高于fallback
- configuration:用于定制 Feign 客户端的配置,比如:拦截器、编码器、超时设置等,仅作用于当前接口
- decode404:是否将 HTTP 404 响应解码为普通响应,而不是抛异常。默认为 false
- primary:是否设置为
@Primary
Bean,默认 true。如果存在多个相同类型的 Bean,可以设置为 false
三从源码的角度解析
如果没有设置 ContextId
, 会将 配置的Name/Value 的值设置成ContextId
在 FeignClientsRegistrar 中的 registerBeanDefinitions方法下的 registerFeignClients方法下的registerFeignClient方法中的 getContextId
java
private String getContextId(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
String contextId = (String) attributes.get("contextId");
if (!StringUtils.hasText(contextId)) {
return getName(attributes);
}
contextId = resolve(beanFactory, contextId);
return getName(contextId);
}
java
/* for testing */ String getName(Map<String, Object> attributes) {
return getName(null, attributes);
}
String getName(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
String name = (String) attributes.get("serviceId");
if (!StringUtils.hasText(name)) {
name = (String) attributes.get("name");
}
if (!StringUtils.hasText(name)) {
name = (String) attributes.get("value");
}
name = resolve(beanFactory, name);
return getName(name);
}
然后在构造这个代理对象时,找到对应的配置 // 注册FeignClient代理对象为Spring 容器的bean
java
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
Class clazz = ClassUtils.resolveClassName(className, null);
ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
? (ConfigurableBeanFactory) registry : null;
// 获取当前的的 contextId
String contextId = getContextId(beanFactory, attributes);
String name = getName(attributes);
FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
factoryBean.setBeanFactory(beanFactory);
factoryBean.setName(name);
factoryBean.setContextId(contextId);
factoryBean.setType(clazz);
factoryBean.setRefreshableClient(isClientRefreshEnabled());
// 构造代理对象
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
factoryBean.setUrl(getUrl(beanFactory, attributes));
factoryBean.setPath(getPath(beanFactory, attributes));
factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));
Object fallback = attributes.get("fallback");
if (fallback != null) {
factoryBean.setFallback(fallback instanceof Class ? (Class<?>) fallback
: ClassUtils.resolveClassName(fallback.toString(), null));
}
Object fallbackFactory = attributes.get("fallbackFactory");
if (fallbackFactory != null) {
factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class<?>) fallbackFactory
: ClassUtils.resolveClassName(fallbackFactory.toString(), null));
}
返回代理的bean
return factoryBean.getObject();
});
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
definition.setLazyInit(true);
validate(attributes);
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);
// has a default, won't be null
boolean primary = (Boolean) attributes.get("primary");
beanDefinition.setPrimary(primary);
String[] qualifiers = getQualifiers(attributes);
if (ObjectUtils.isEmpty(qualifiers)) {
qualifiers = new String[] { contextId + "FeignClient" };
}
// 将这个代理对象注册到 Spring的容器中
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
// 如果开启动态更新的配置,将这个bean注册成动态刷新的bean
registerOptionsBeanDefinition(registry, contextId);
}
下面是如何获取代理类的源码:
java
<T> T getTarget() {
// 获取Feign的上下文
FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
: applicationContext.getBean(FeignContext.class);
// 获取代理对象
Feign.Builder builder = feign(context);
if (!StringUtils.hasText(url)) {
if (url != null && LOG.isWarnEnabled()) {
LOG.warn("The provided URL is empty. Will try picking an instance via load-balancing.");
}
else if (LOG.isDebugEnabled()) {
LOG.debug("URL not provided. Will use LoadBalancer.");
}
if (!name.startsWith("http")) {
url = "http://" + name;
}
else {
url = name;
}
url += cleanPath();
return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url));
}
if (StringUtils.hasText(url) && !url.startsWith("http")) {
url = "http://" + url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
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();
}
if (client instanceof RetryableFeignBlockingLoadBalancerClient) {
// not load balancing because we have a url,
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
client = ((RetryableFeignBlockingLoadBalancerClient) client).getDelegate();
}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
}
获取具体的构造代理的对象:
java
protected Feign.Builder feign(FeignContext context) {
FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
Logger logger = loggerFactory.create(type);
// @formatter:off
Feign.Builder builder = get(context, Feign.Builder.class)
// required values
.logger(logger)
.encoder(get(context, Encoder.class))
.decoder(get(context, Decoder.class))
.contract(get(context, Contract.class));
// @formatter:on
// 获取对应的配置
configureFeign(context, builder);
//
applyBuildCustomizers(context, builder);
return builder;
}
设置对应的配置:
java
/protected void configureFeign(FeignContext context, Feign.Builder builder) {
// 拿到所有的配置
FeignClientProperties properties = beanFactory != null ? beanFactory.getBean(FeignClientProperties.class)
: applicationContext.getBean(FeignClientProperties.class);
FeignClientConfigurer feignClientConfigurer = getOptional(context, FeignClientConfigurer.class);
setInheritParentContext(feignClientConfigurer.inheritParentConfiguration());
if (properties != null && inheritParentContext) {
// 为当前的bean 工厂配置 readTimeoutMillis connectTimeoutMillis followRedirects等
if (properties.isDefaultToProperties()) {
configureUsingConfiguration(context, builder);
configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
configureUsingProperties(properties.getConfig().get(contextId), builder);
}
else {
configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
configureUsingProperties(properties.getConfig().get(contextId), builder);
configureUsingConfiguration(context, builder);
}
}
else {
configureUsingConfiguration(context, builder);