前言
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如何动态更新配置
- 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;
}
- DefaultClientConfigImpl.loadProperties(String restClientName)
public void loadProperties(String restClientName) {
this.enableDynamicProperties = true; // 允许动态配置
this.setClientName(restClientName);
this.loadDefaultValues();// 加载配置
...
}
- DefaultClientConfigImpl.loadDefaultValues
public void loadDefaultValues() {
// ... 其他配置
this.putDefaultIntegerProperty(CommonClientConfigKey.ReadTimeout, this.getDefaultReadTimeout());
}
- 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);
}
- 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);
}
}
- 如何触发回调
通过发送EnvironmentChangeEvent事件,案例:
applicationContext.publishEvent(
new EnvironmentChangeEvent(Collections.singleton("ribbon.trade-service.ReadTimeout")));
- 为什么发送这个事件可以触发回调
答案还是在自动配置类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;
}
}
}
}