Feign源码阅读(三)FeignClient定制化配置

5,021 阅读7分钟

前言

  FeignClient定制化配置主要分注解形式和配置文件形式。对于个别属性(如超时时间)支持编码方式配置,且支持方法级别的定制化配置。对于Ribbon的几个核心抽象的配置,还是沿用了SpringClientFactory来获取。

一、注解形式

案例

@Test
public void test01() {
    Retryer instance01 = feignContext.getInstance("trade-service", Retryer.class);
    System.out.println(instance01.getClass());// 默认 class feign.Retryer$1
    Retryer instance02 = feignContext.getInstance("override-trade-service", Retryer.class);
    System.out.println(instance02.getClass());// 自定义 class com.yy.springcloud.trade.feignclient.SpecificationTest$FeignSpecificationConfiguration$1
}

@FeignClient(name = "trade-service", contextId = "override-trade-service", configuration = FeignSpecificationConfiguration.class)
interface SpecificationStockClient extends StockClient {
}
static class FeignSpecificationConfiguration {
    @Bean
    public Retryer retryer() {
        return new Retryer() {
            @Override
            public void continueOrPropagate(RetryableException e) {
                System.out.println("My Retryer");
            }
            @Override
            public Retryer clone() {
                return null;
            }
        };
    }
}

原理

FeignContext继承了NamedContextFactory,与Ribbon的SpringClientFactory一样,具体逻辑就在NamedContextFactory中已经讲过。

public class FeignAutoConfiguration {

	@Autowired(required = false)
	private List<FeignClientSpecification> configurations = new ArrayList<>();
	@Bean
	public FeignContext feignContext() {
		FeignContext context = new FeignContext();
		context.setConfigurations(this.configurations);
		return context;
	}
}
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
	public FeignContext() {
		super(FeignClientsConfiguration.class, "feign", "feign.client.name");
	}
}
protected AnnotationConfigApplicationContext createContext(String name) {
	AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
	// 注册name对应的所有特殊配置到子容器
	if (this.configurations.containsKey(name)) {
		for (Class<?> configuration : this.configurations.get(name)
				.getConfiguration()) {
			context.register(configuration);
		}
	}
	// 之后再注册默认的配置到子容器,所以自定义特殊配置的优先级高
	context.register(PropertyPlaceholderAutoConfiguration.class,
			this.defaultConfigType);
	context.refresh();
	return context;
}

二、配置文件形式

案例

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {Application.class}, 
value = {"feign.client.config.override-trade-service.retryer=feign.Retryer.Default"
        , "feign.client.config.override-trade-service.connectTimeout=2001"
        , "feign.client.config.override-trade-service.readTimeout=2002"})
public class SpecificationTest {
    @Autowired
    private StockClient stockClient;
    @Test
    public void test() {
        System.out.println(stockClient.getStock(1L));
    }
     @FeignClient(name = "trade-service", contextId = "override-trade-service", configuration = FeignSpecificationConfiguration.class)
    interface SpecificationStockClient extends StockClient {
    }
}

FeignClientProperties

管理feign.client.config.{contextId或服务名}开头的配置

@ConfigurationProperties("feign.client")
public class FeignClientProperties {
	private Map<String, FeignClientConfiguration> config = new HashMap<>();
}

以配置超时时间为例:feign.client.config.override-trade-service.connectTimeout=2001

配置文件配置在FeignClientFactoryBean的getObject阶段,FeignClientFactoryBean#configureFeign调用configureUsingProperties方法向Feign.Builder注入属性。

protected void configureUsingProperties(
			FeignClientProperties.FeignClientConfiguration config,
			Feign.Builder builder) {
	// ... 省略其他配置项
	if (config.getConnectTimeout() != null && config.getReadTimeout() != null) {
		builder.options(new Request.Options(config.getConnectTimeout(),
				config.getReadTimeout()));
	}
}

三、编码方式

对于超时时间的配置,还有一个特殊配置方式,就是通过编码方式,比如

@FeignClient(name = "trade-service", contextId = "override-trade-service", configuration = FeignSpecificationConfiguration.class)
interface SpecificationStockClient extends StockClient {
    @RequestMapping(value = "/getStockByMpId/{mpId}", method = RequestMethod.GET)
    ApiResponse<Stock> getStockWithSpecialTimeOut(@PathVariable("mpId") Long mpId, Request.Options options);
}
@Test
public void test02() {
    client.getStockWithSpecialTimeOut(2L, new Request.Options(3001, 3002));
}

这里可以通过编码的方式传入,做到不同方法的超时时间个性化定制,这个原理是进入 动态代理之后的feign.SynchronousMethodHandler#invoke方法里,通过findOptions方法反射获取方法参数列表里第一个Options类型对象。

Options findOptions(Object[] argv) { // 入参是方法参数列表
    if (argv == null || argv.length == 0) {
      return this.options;
    }
    return Stream.of(argv)
        .filter(Options.class::isInstance)
        .map(Options.class::cast)
        .findFirst()
        .orElse(this.options);
}

关于超时时间的配置,通过打断点方式验证,最后http请求的入口:feign.Client.Default#convertAndSend

HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
      final URL url = new URL(request.url());
      final HttpURLConnection connection = this.getConnection(url);
      connection.setConnectTimeout(options.connectTimeoutMillis());
      connection.setReadTimeout(options.readTimeoutMillis());
      // ...
}

feign默认的超时时间,取的就是RibbonClientConfiguration#ribbonClientConfig配置的,都是1秒

@Bean
@ConditionalOnMissingBean
public IClientConfig ribbonClientConfig() {
    DefaultClientConfigImpl config = new DefaultClientConfigImpl();
    config.loadProperties(this.name);
    config.set(CommonClientConfigKey.ConnectTimeout, 1000);
    config.set(CommonClientConfigKey.ReadTimeout, 1000);
    config.set(CommonClientConfigKey.GZipPayload, true);
    return config;
}

四、优先级

  Feign的配置优先级和Ribbon有所不同

  • Ribbon注解优先(@RibbonClient),其次是配置文件。
  • Feign的优先级是可以配置的(feign.client.defaultToProperties = true),默认是配置文件优先,其次是注解(@FeignClient)。当然了,对于超时时间的配置,编码的优先级高于注解和文件配置,因为他不是在bean装载的阶段去配置的,是在运行时通过反射的方式最终决定了超时时间。

FeignClientFactoryBean#configureFeign

  这个方法之前讲FeignClient动态代理的时候跳过了,这个方法主要是往Builder里注入一些配置属性。

protected void configureFeign(FeignContext context, Feign.Builder builder) {
	// 读取feign.client开头的properties
	FeignClientProperties properties = this.applicationContext
			.getBean(FeignClientProperties.class);
	// 设置inheritParentContext属性为true
	FeignClientConfigurer feignClientConfigurer = getOptional(context,
			FeignClientConfigurer.class);
	setInheritParentContext(feignClientConfigurer.inheritParentConfiguration());

	if (properties != null && inheritParentContext) {
		if (properties.isDefaultToProperties()) {
			// 默认feign.client.defaultToProperties = true 走这个分支
			// 1. 先取子容器的配置类里的配置
			configureUsingConfiguration(context, builder);
			// 2. 用配置文件feign.client.defaultConfig开头的配置文件配置覆盖
			configureUsingProperties(
					properties.getConfig().get(properties.getDefaultConfig()),
					builder);
			// 3. 最后用feign.client.config.{contextId}开头的配置文件覆盖
			configureUsingProperties(properties.getConfig().get(this.contextId),
					builder);
		}
		else {
           // 1. 先用配置文件feign.client.defaultConfig开头的配置文件
			configureUsingProperties(
					properties.getConfig().get(properties.getDefaultConfig()),
					builder);
           // 2. 用feign.client.config.{contextId}开头的配置文件覆盖
			configureUsingProperties(properties.getConfig().get(this.contextId),
					builder);
            // 3. 最后取子容器的配置类里的配置覆盖
			configureUsingConfiguration(context, builder);
		}
	}
	else {
		configureUsingConfiguration(context, builder);
	}
}

五、Ribbon的服务配置定制化在Feign中的使用

1、Ribbon服务定制化配置-Ribbon的抽象实现

  在第一章里,我们注意到FeignContext的默认配置里,并没有涉及到IRule、ServerList等原来Ribbon的几个核心抽象,其实这些配置还是通过SpringClientFactory(Ribbon命名容器工厂)获取的,并不在FeignContext中。自动配置DefaultFeignLoadBalancedConfiguration创建LoadBalancerFeignClient依赖了SpringClientFactory,那必然会使用SpringClientFactory提供的IRule等抽象实现,这将在解析FeignClient动态代理执行流程时会看到。

@Configuration(proxyBeanMethods = false)
class DefaultFeignLoadBalancedConfiguration {
	@Bean
	@ConditionalOnMissingBean
	public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
			SpringClientFactory clientFactory) {
		return new LoadBalancerFeignClient(new Client.Default(null, null), cachingFactory,
				clientFactory);
	}
}

  也就是说仍然可以使用@RibbonClient注解和Ribbon配置文件的形式,来设置IRule、ServerList等实现实例。

@FeignClient(name = "trade-service")
public interface StockClient {
    @RequestMapping(value = "/getStockByMpId/{mpId}", method = RequestMethod.GET)
    ApiResponse<Stock> getStock(@PathVariable("mpId") Long mpId);
}

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {Application.class,SpecificationTest.RibbonClientTestConfiguration.class})
public class SpecificationTest {
  @Autowired
  private StockClient stockClient;
  @Test
  public void test() {
      System.out.println(stockClient.getStock(1L));
  }

  @RibbonClient(name = "trade-service", configuration = SpecificationConfiguration.class)
  static class RibbonClientTestConfiguration {}
  
  static class SpecificationConfiguration {
      @Bean
      public IRule iRule(){
          return new RandomRule();
      }
  }
}

2、Ribbon服务定制化配置-其他

  如果不使用Feign配置超时时间,可以使用Ribbon配置,如ribbon.ReadTimeout服务名.ribbon.ReadTimeout。因为Feign可以沿用Ribbon的DefaultClientConfigImpl。

代码入口在:LoadBalancerFeignClient#getClientConfig,这将在解析FeignClient动态代理执行流程时会看到。

IClientConfig getClientConfig(Request.Options options, String clientName) {
      IClientConfig requestConfig;
      // 如果Options取的是FeignRibbonClientAutoConfiguration#feignRequestOptions自动注入的Options,则IClientConfig取Ribbon的实现
      //(这意思是没有用Feign自己的机制配置options,就走ribbon的DefaultClientConfigImpl)
      if (options == DEFAULT_OPTIONS) {
          requestConfig = this.clientFactory.getClientConfig(clientName);
      }
      else {
      // 否则取Ribbon默认配置,并用当前options的超时时间覆盖
          requestConfig = new FeignOptionsClientConfig(options);
      }
      return requestConfig;
  }
static class FeignOptionsClientConfig extends DefaultClientConfigImpl {
      FeignOptionsClientConfig(Request.Options options) {
          setProperty(CommonClientConfigKey.ConnectTimeout,
                  options.connectTimeoutMillis());
          setProperty(CommonClientConfigKey.ReadTimeout, options.readTimeoutMillis());
      }
      @Override
      public void loadProperties(String clientName) {
      }

      @Override
      public void loadDefaultValues() {
      }
}

由于用Feign的配置方式配置超时时间是在bean装配阶段(FeignClientFactoryBean.getObject),所以无法动态更新超时时间(例如与Apollo集成后,无法动态更新超时时间)。所以如果要动态配置,最好不要用Feign自己的机制设置option,而是通过Ribbon的DefaultClientConfigImpl来设置。(spring-cloud-openfeign的v3.0.3版本,新增了feign.client.refresh-enabled: true,支持利用RefreshScope动态刷新options)

DefaultClientConfigImpl

之前讲Ribbon的时候没有仔细讲DefaultClientConfigImpl,因为使用Spring的RestTemplate集成Ribbon,它的超时时间不是由DefaultClientConfigImpl设置的(也就是说通过ribbon.ReadTimeout不能调整超时时间),是通过RestTemplate的请求工厂配置的。

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    RestTemplate template = new RestTemplate();
    SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
    factory.setReadTimeout(2000);
    factory.setConnectTimeout(2000);
    template.setRequestFactory(factory);
    return template;
}

DefaultClientConfigImpl如何获取Spring配置

netflix本身有一套配置系统---Archaius,spring-cloud-starter-netflix-archaius提供了Spring与Archaius集成的自动配置。

自动配置类ArchaiusAutoConfiguration

public class ArchaiusAutoConfiguration {

	@Bean
	public static ConfigurableEnvironmentConfiguration configurableEnvironmentConfiguration(ConfigurableEnvironment env, ApplicationContext context) {
    	// 获取其他外部配置,如zk、jdbc、etcd...
		Map<String, AbstractConfiguration> abstractConfigurationMap = context.getBeansOfType(AbstractConfiguration.class);
		List<AbstractConfiguration> externalConfigurations = new ArrayList<>(abstractConfigurationMap.values());
        // 把Spring的ConfigurableEnvironment封装为org.springframework.cloud.netflix.archaius.ConfigurableEnvironmentConfiguration
		ConfigurableEnvironmentConfiguration envConfig = new ConfigurableEnvironmentConfiguration(env);
        // 配置Archaius
		configureArchaius(envConfig, env, externalConfigurations);
		return envConfig;
	}
    
    protected static void configureArchaius(
          ConfigurableEnvironmentConfiguration envConfig, ConfigurableEnvironment env,
          List<AbstractConfiguration> externalConfigurations) {
          ConcurrentCompositeConfiguration config = new ConcurrentCompositeConfiguration();
		 // 外部配置externalConfigurations加入ConcurrentCompositeConfiguration
         // ...
         // 把封装着Spring的Environment的envConfig加入ConcurrentCompositeConfiguration
         // ... 其他配置加入ConcurrentCompositeConfiguration
         
         // 调用ConfigurationManager.install(config);
          addArchaiusConfiguration(config);
	}
    
    private static void addArchaiusConfiguration(
			ConcurrentCompositeConfiguration config) {
		if (ConfigurationManager.isConfigurationInstalled()) {
			// 忽略
		} else {
        	// 走这里,ConfigurationManager负责管理Archaius配置,不细讲
			ConfigurationManager.install(config);
		}
	}

DefaultClientConfigImpl如何动态更新配置

  1. SpringClientFactory子容器默认配置类RibbonClientConfiguration注入DefaultClientConfigImpl时,执行了config.loadProperties(this.name)
public class RibbonClientConfiguration {
  public static final int DEFAULT_CONNECT_TIMEOUT = 1000;
  public static final int DEFAULT_READ_TIMEOUT = 1000;
  public static final boolean DEFAULT_GZIP_PAYLOAD = true;
  // 这是个@Value注解,name最终为trade-service
  @RibbonClientName
  private String name = "client";
  @Bean
  @ConditionalOnMissingBean
  public IClientConfig ribbonClientConfig() {
      DefaultClientConfigImpl config = new DefaultClientConfigImpl();
      // 可以加载动态配置,取Spring的Environment里的配置
      config.loadProperties(this.name);
      // 设置默认值
      config.set(CommonClientConfigKey.ConnectTimeout, 1000);
      config.set(CommonClientConfigKey.ReadTimeout, 1000);
      config.set(CommonClientConfigKey.GZipPayload, true);
      return config;
  }
  1. DefaultClientConfigImpl.loadProperties(String restClientName)
public void loadProperties(String restClientName) {
        this.enableDynamicProperties = true; // 允许动态配置
        this.setClientName(restClientName);
        this.loadDefaultValues();// 加载配置
        ...
}
  1. DefaultClientConfigImpl.loadDefaultValues
public void loadDefaultValues() {
		// ... 其他配置
        this.putDefaultIntegerProperty(CommonClientConfigKey.ReadTimeout, this.getDefaultReadTimeout());
}
  1. DefaultClientConfigImpl.putDefaultIntegerProperty
 protected void putDefaultIntegerProperty(IClientConfigKey propName, Integer defaultValue) {
 		// 从ConfigurationManager获取配置
        Integer value = ConfigurationManager.getConfigInstance().getInteger(this.getDefaultPropName(propName), defaultValue);
        // 设置配置信息到DefaultClientConfigImpl的内存map,并设置动态配置的更新回调
        this.setPropertyInternal((IClientConfigKey)propName, value);
    }
  1. DefaultClientConfigImpl.setPropertyInternal
protected void setPropertyInternal(final String propName, Object value) {
    String stringValue = value == null ? "" : String.valueOf(value);
    // 放入内存map
    this.properties.put(propName, stringValue);
    if (this.enableDynamicProperties) {
        String configKey = this.getConfigKey(propName);
        final DynamicStringProperty prop = DynamicPropertyFactory.getInstance().getStringProperty(configKey, (String)null);
        // 设置动态配置更新时的回调函数
        Runnable callback = new Runnable() {
            public void run() {
                String value = prop.get();
                // 更新内存map
                if (value != null) {
                    DefaultClientConfigImpl.this.properties.put(propName, value);
                } else {
                    DefaultClientConfigImpl.this.properties.remove(propName);
                }

            }
        };
        prop.addCallback(callback);
        // 动态配置对象 放入内存map
        // private final Map<String, DynamicStringProperty> dynamicProperties;
        this.dynamicProperties.put(propName, prop);
    }
}
  1. 如何触发回调

通过发送EnvironmentChangeEvent事件,案例:

applicationContext.publishEvent(
new EnvironmentChangeEvent(Collections.singleton("ribbon.trade-service.ReadTimeout")));
  1. 为什么发送这个事件可以触发回调

答案还是在自动配置类ArchaiusAutoConfiguration,注入了ApplicationListener实现类,负责处理配置更新,最后会通知到对应的每个注册的callback函数。

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnProperty(value = "archaius.propagate.environmentChangedEvent",
			matchIfMissing = true)
	@ConditionalOnClass(EnvironmentChangeEvent.class)
	protected static class PropagateEventsConfiguration
			implements ApplicationListener<EnvironmentChangeEvent> {

		@Autowired
		private Environment env;

		@Override
		public void onApplicationEvent(EnvironmentChangeEvent event) {
			AbstractConfiguration manager = ConfigurationManager.getConfigInstance();
			for (String key : event.getKeys()) {
				for (ConfigurationListener listener : manager
						.getConfigurationListeners()) {
					Object source = event.getSource();
					int type = AbstractConfiguration.EVENT_SET_PROPERTY;
					String value = this.env.getProperty(key);
					boolean beforeUpdate = false;
					listener.configurationChanged(new ConfigurationEvent(source, type,
							key, value, beforeUpdate));
				}
			}
		}

	}

最后用个单元测试测一下,动态更新超时时间。

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {Application.class}, value = {"trade-service.ribbon.ReadTimeout=3000"})
public class DefaultClientConfigImplTest {

    @Autowired
    private SpringClientFactory springClientFactory;
    @Autowired
    private ConfigurableEnvironment environment;
    @Autowired
    private ApplicationContext applicationContext;
    @Autowired
    private StockClient stockClient;

    private final IClientConfigKey<Integer> configKey = CommonClientConfigKey.ReadTimeout;
    private final String serviceName = "trade-service";
    private final String namespace = "ribbon";

    @Test
    public void testDefaultClientConfigImpl() throws InterruptedException {
        // 获取IClientConfig
        IClientConfig iClientConfig = springClientFactory.getInstance(serviceName, IClientConfig.class);
        int cnt = 0;
        while (true) {
            // 打印当前值
            System.out.println(configKey.key() + ":" + iClientConfig.get(configKey));
            // 请求
            stockClient.getStock(2L);
            TimeUnit.SECONDS.sleep(1);
            cnt++;
            if (cnt % 2 == 0) {
                // 修改配置
                changeConfig();
            }
        }
    }

    private void changeConfig() {
        String propertyKey = String.join(".", serviceName, namespace, configKey.key());
        MutablePropertySources propertySources = environment.getPropertySources();
        for (PropertySource<?> propertySource : propertySources) {
            String property = (String) propertySource.getProperty(propertyKey);
            if (property != null) {
                MapPropertySource source = (MapPropertySource) propertySource;
                // 修改配置
                source.getSource().put(propertyKey, String.valueOf(Integer.parseInt(property) + 1));
                // 发布EnvironmentChangeEvent事件
                applicationContext.publishEvent(new EnvironmentChangeEvent(Collections.singleton(propertyKey)));
                break;
            }
        }
    }
}