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);
相关推荐
StackNoOverflow1 小时前
SpringCloud 声明式服务调用 —— Feign 全面解析(入门 + 原理 + 优化)
后端·spring·spring cloud
身如柳絮随风扬2 天前
Dubbo 与 Spring Cloud 终极对比:RPC 框架 vs 微服务生态
spring cloud·rpc·dubbo
一个有温度的技术博主2 天前
Spring Cloud 入门与实战:从架构拆分到核心组件详解
spring·spring cloud·架构
uNke DEPH2 天前
SpringCloud Gateway 集成 Sentinel 详解 及实现动态监听Nacos规则配置实时更新流控规则
spring cloud·gateway·sentinel
慕容卡卡2 天前
你所不知道的RAG那些事
java·开发语言·人工智能·spring boot·spring cloud
dLYG DUMS2 天前
Spring Cloud Data Flow 简介
后端·spring·spring cloud
Ken_11152 天前
SpringCloud系列(61)--Nacos之服务配置中心的介绍与使用
spring cloud
Ken_11153 天前
SpringCloud系列(62)--Nacos之命名空间、分组和DataID三者之间的关系
spring cloud
Ken_11153 天前
SpringCloud系列(63)--Nacos读取不同配置之DataID配置方案
spring cloud
Devin~Y3 天前
从Spring Boot到Spring AI:音视频AIGC内容社区Java大厂面试三轮连环问(含Kafka/Redis/安全/可观测性答案)
java·spring boot·redis·spring cloud·kafka·spring security·resilience4j