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);
相关推荐
微扬嘴角6 小时前
springcloud篇5-微服务保护(Sentinel)
spring cloud·微服务·sentinel
java亮小白199711 小时前
Spring Cloud 快速通关之Sentinel
java·spring cloud·sentinel
MrSYJ2 天前
AuthenticationEntryPoint认证入口
java·spring cloud·架构
银迢迢2 天前
SpringCloud微服务技术自用笔记
java·spring cloud·微服务·gateway·sentinel
弈芯2 天前
SpringCloud微服务拆分最佳实践
spring cloud
麦兜*3 天前
【Prometheus】 + Grafana构建【Redis】智能监控告警体系
java·spring boot·redis·spring·spring cloud·grafana·prometheus
sniper_fandc4 天前
Spring Cloud系列—SkyWalking告警和飞书接入
spring cloud·skywalking
abigalexy4 天前
深入图解Spring Cloud底层设计
spring·spring cloud
楠有枝6 天前
普通用户使用docker命令
spring cloud·docker·eureka
孤狼程序员6 天前
【Spring Cloud 微服务】2.守护神网关Gateway
spring cloud·微服务·gateway