Feign Client 配置优先级

Last Modified: 2023/01/17

概述

微服务之间的调用,一般我们会使用到 OpenFeign,借助于 OpenFeign,不仅可以实现声明的方式调用其他服务,更重要的是可以针对不同的服务使用不同的配置,feign 可配置项目有很多,我们可以只配置我们需要配置的配置项目,其他的使用公共的默认配置。那么 feign 是如何做到配置隔离的同时又支持默认配置公用呢?本文将会讲述 feign 的配置层级。

注意:以下会混用“子 context” 和子容器,他们表达相同的含义。容器可以看成是一堆 bean 实例的集合。

配置层级

OpenFeign 实现配置隔离是通过 NamedContextFactory 实现的,可以将 NamedContextFactory 实例看成是包含多个命名子容器的数据结构,看下面的示意图:

创建子容器的代码位于 NamedContextFactory#createContext:

if (this.configurations.containsKey(name)) {
    for (Class<?> configuration : this.configurations.get(name).getConfiguration()) {
        // 层级1
        context.register(configuration);
    }
}
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
    // 注意:这里检查配置名称是否以 'default.'开头,如果是,则注入到当前子 context 中
    if (entry.getKey().startsWith("default.")) {
        for (Class<?> configuration : entry.getValue().getConfiguration()) {
            // 层级2
            context.register(configuration);
        }
    }
}
// 层级3
// 针对 feign,ths.defaultConfigType 是 FeignClientsConfiguration.class
context.register(PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType);

可见每个子 context(子容器) 中 beans 有三个注册的地方,从而分为三个层级(层级1、层级2和层级3),如果不考虑 conditional bean,那么高层级中的 bean 会覆盖低层级中的同名 bean。 仅看上面的代码可能不容易理解,现在假如我们的工程中有个微服务需要需要通过 feign 调用 “file-service” 微服务,为了使 feign client 生效,我们需要通过 @EnableFeignClients 注解启用 feign。

// MyFileServiceConfig 里面定义的 beans 对应上面的层级1
@FeignClient(contextId = "remoteFileService", value = "file-service", configuration = MyFileServiceConfig.class)
public interface RemoteFileService {}

// 启用 feign,这里的 MyDefaultClientConfig 里面定义的 beans 对应上面的层级2
@EnableFeignClients(defaultConfiguration = MyDefaultClientConfig.class, basePackages = "com.ry")
@SpringBootApplication
public class RuoYiSystemApplication {
	// ...
}

// 下面的代码片段来自 FeignAutoConfiguration,这里的 FeignClientsConfiguration 里面定义的 bean 对应上面的层级3
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
    @Bean
	public FeignContext() {
		super(FeignClientsConfiguration.class, "feign", "feign.client.name");
	}
	// ...
}

上面提到“如果不考虑 conditional bean,那么高层级中的 bean 会覆盖低层级中的同名 bean”,假如 MyFileServiceConfig、MyDefaultClientConfig 和 FeignClientsConfiguration 都定义了 “feignDecoder” bean,那么 FeignClientsConfiguration 中定义的 feignDecoder 将会取胜。优先级从低到高排列如下:

  • 层级1中定义的 bean,即 MyFileServiceConfig 类中定义的 bean;
  • 层级2中定义的 bean,即 MyDefaultClientConfig 类中定义的 bean;
  • 层级3中定义的 bean,也就是 FeignClientsConfiguration 类中定义的 bean。

然而事实并非如此,FeignClientsConfiguration 中定义的大部分 bean 都有 “@ConditionalOnMissingBean” 注解,FeignClientsConfiguration 中虽然定义了 “feignDecoder” bean,但是 bean 被标注了 @ConditionalOnMissingBean,因此 bean 的优先级没有层级2 中的 feignDecoder bean 优先级高。实际的优先级从低到高排列如下:

  • 层级3中定义的 bean,也就是 FeignClientsConfiguration 类中定义的 bean;
  • 层级1中定义的 bean,即 MyFileServiceConfig 类中定义的 bean;
  • 层级2中定义的 bean,即 MyDefaultClientConfig 类中定义的 bean。

但我们仔细想下,这个优先级顺序显然不合理,MyDefaultClientConfig 是针对所有 clients 的默认配置,我们显然希望某个特定 client 上的的配置(如这里的 MyFileServiceConfig 中定义的 bean) 的优先级更高,从而覆盖默认配置。办法有两个:

  • 方法1,我们不定义 MyDefaultClientConfig 这个默认配置;
  • 方法2,我们依然定义 MyDefaultClientConfig,模仿 FeignClientsConfiguration,将 bean 都标注上 @ConditionalOnMissingBean 注解。

通过方法2,最终优先级从低到高排列如下:

  • 层级3中定义的 bean,也就是 FeignClientsConfiguration 类中定义的 bean;
  • 层级2中定义的 bean,即 MyDefaultClientConfig 类中定义的 bean;
  • 层级1中定义的 bean,即 MyFileServiceConfig 类中定义的 bean。

有一点需要说明下,为什么说 MyDefaultClientConfig 对应 NamedContextFactory 中的层级2 呢?

package com.ry;
// 启用 feign,这里的 MyDefaultClientConfig 里面定义的 beans 对应上面的层级2
@EnableFeignClients(defaultConfiguration = MyDefaultClientConfig.class, basePackages = "com.ry")
@SpringBootApplication
public class RuoYiSystemApplication {
	// ...
}

注意观察上面的 MyDefaultClientConfig.class 是写在 @EnableFeignClients 注解中的。@EnableFeignClients 注解上面标有 @Import(FeignClientsRegistrar.class),最终 FeignClientsRegistrar 中的 registerDefaultConfiguration(见下面的代码片段) 会将 MyDefaultClientConfig 作为 NamedContextFactory 实例的默认配置。

默认配置中定义的 bean 会注入到每个子 context 中,通过将默认配置注入到每个子容器中,实现了默认配置公用,每个 feign client 也可以有自己的特殊配置,例如 remoteFileService 的特殊配置为 MyFileServiceConfig,该配置里面定义的 bean 只会注入到 remoteFileService 这个子容器中,实现了配置的隔离。实际上每个 @FeignClient 标记的 interface 都会创建一个子容器。

private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    Map<String, Object> defaultAttrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true);

    if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
        String name;
        if (metadata.hasEnclosingClass()) {
            // 这个配置的名称是以 'default.' 开头的
            name = "default." + metadata.getEnclosingClassName();
        }
        else {
            // 这个配置的名称是以 'default.' 开头的
            name = "default." + metadata.getClassName();
        }
        registerClientConfiguration(registry, name, defaultAttrs.get("defaultConfiguration"));
    }
}

通过配置文件配置 OpenFeign

上面讨论了代码配置 OpenFeign 的方式以及配置的优先级,实际上我们还可以通过配置文件来配置 OpenFeign,如果加上配置文件这种配置方式,那么优先级又是怎样的呢?在 FeignClientFactoryBean#configureFeign 中已经揭秘了配置优先级:

protected void configureFeign(FeignContext context, Feign.Builder builder) {
  FeignClientProperties p = beanFactory != null ? beanFactory.getBean(FeignClientProperties.class)
          : applicationContext.getBean(FeignClientProperties.class);
  
  FeignClientConfigurer feignClientConfigurer = getOptional(context, FeignClientConfigurer.class);
  setInheritParentContext(feignClientConfigurer.inheritParentConfiguration());
  
  // 如果不配置,默认情况下 inheritParentContext 为 true
  if (p != null && inheritParentContext) {
    // 如果不配置,isDefaultToProperties 默认为 true,因此默认情况下,配置文件配置优先
    if (p.isDefaultToProperties()) { // 配置文件配置优先
        configureUsingConfiguration(context, builder);
        configureUsingProperties(p.getConfig().get(p.getDefaultConfig()), builder);
        configureUsingProperties(p.getConfig().get(contextId), builder);
    }
    else { // 代码配置优先
        configureUsingProperties(p.getConfig().get(p.getDefaultConfig()), builder);
        configureUsingProperties(p.getConfig().get(contextId), builder);
        configureUsingConfiguration(context, builder);
    }
  }
  else {
    configureUsingConfiguration(context, builder);
  }
}

通过上面的代码可以看出,可以通过 defaultToProperties 配置可以决定‘代码方式配置’优先还是‘配置文件方法配置’优先,如果 defaultToProperties 为 true,那么配置文件中配置方式优先级比代码配置优先级高,否则代码配置优先级高

现在假设 MyFileServiceConfig 中配置了超时时间如下:

public class MyFileServiceConfig {
  @Bean
  public Request.Options req() {
      return new Request.Options(10000, 3000, true);
  }
}

与此同时,配置文件中也配置了超时时间:

feign:
  client:
    config:
      default:
        connectTimeout: 10000
        readTimeout: 4000

根据上面的说明,如果 defaultToProperties 为 true,那么配置文件中的配置将会覆盖代码中的配置,因此 readTimeout 的值是 4000 而不是 3000。仔细想想,可以看到这里又有一个违反直觉的地方,MyFileServiceConfig 是针对 remoteFileService 配置的,但是配置文件中的配置是针对所有 feign client 的默认配置,结果这个默认配置居然将针对 remoteFileService 而特别配置的配置覆盖了,this is somewhat ridiculous

如果我们想单独针对 remoteFileService 配置超时时间,可以使用配置文件配置的方式:

feign:
  client:
    config:
      remoteFileService:
        connectTimeout: 10000
        readTimeout: 6000
有问题吗?点此反馈!

温馨提示:反馈需要登录