Springboot中的yml为单个的服务配置Feign不生效.md

一、问题

我们的服务采用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 注解属性分析

  1. name/value:定义当前客户端Client的名称,用于指定注册中心中的服务名(即调用的服务)。也可以用于日志分组等。通常推荐设置为服务注册名。
  2. contextId:Spring Bean 的上下文 ID(当存在多个同名 name 时用于区分 Bean 名称)。从 Spring Cloud 2020+ 强制唯一,推荐设置
  3. url:直接指定远程服务的 URL,会绕过服务注册中心。用于测试或特殊调用。支持 ${} 占位符
  4. path:所有方法的统一前缀路径,比如设置 /api/v1,所有方法都会加上这个前缀
  5. fallback:指定一个实现了接口的类,在远程服务调用失败时提供降级逻辑(需配合 Hystrix 或 Sentinel)
  6. fallbackFactory:类似于 fallback,但可以获取异常信息(比如:日志记录或异常判断处理)。优先级高于 fallback
  7. configuration:用于定制 Feign 客户端的配置,比如:拦截器、编码器、超时设置等,仅作用于当前接口
  8. decode404:是否将 HTTP 404 响应解码为普通响应,而不是抛异常。默认为 false
  9. 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);
相关推荐
Rain的Java大神之路4 天前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试
需要8264 天前
MySQL 索引 B+ 树与“查询为什么变慢了
java·spring boot·spring·spring cloud·tomcat
需要8264 天前
Arthas 一个命令排查线上问题
java·jvm·spring·spring cloud
苏渡苇5 天前
Spring Insight 里如何把 Span 画成瀑布时间线
后端·spring·spring cloud·springboot·监控·apm
叶总没有会5 天前
Nacos—注册中心
spring cloud·微服务
苏渡苇6 天前
Spring Insight 里如何把 Span 收成一条链路
spring boot·spring·spring cloud·系统监控·apm
叶总没有会6 天前
微服务--分布式基础
java·spring·spring cloud·微服务
孙启超7 天前
【AI开发之Rust】第 7 课:错误处理 —— panic、Result 与 `?`
人工智能·分布式·后端·爬虫·spring cloud·架构·rust
行百里er8 天前
Spring Insight 里如何把 Span 收成一条链路
spring boot·spring cloud·监控
砍材农夫8 天前
spring|spring web|拦截器、过滤器、aop
java·spring boot·spring·spring cloud