背景
今天项目开发过程中,需要在给别人提供的feign的spi中添加一些功能,让调用方通过我提供的Feign调用的时候能额外加一个请求头,配置代码如下:
@Configuration
@ConditionalOnProperty(value = "api.audit.log.enabled", matchIfMissing = true)
public class DemoFeignClientConfig {
@Value("${spring.application.name:UNKNOWN}")
private String appName;
@Bean
public RequestInterceptor headerInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header(HEADER_APP_NAME, appName);
}
};
}
}
同时在提供的jar包的resources/META-INF/spring.factories
下配置,这个不懂的可以搜索spring.factories机制
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.platform.config.DemoFeignClientConfig
此时当调用方依赖了这个spi包后,上面的配置就会生效,发出请求的时候会过这个RequestInterceptor
,在请求头上加入应用名,但此时有个问题,那就是这个RequestInterceptor
是全局的,也就是说不止是调我这个spi时会被加上这个请求头,通过feign调用其他服务时也会被加上这个请求头,RequestInterceptor
当被全局加载后破坏性还不是很大,因为在Feign的源码中可以看到RequestInterceptor
时会加载多个的(下面代码),所以顶多就是多了个header,但是如果上面配置了自己的encoder
和decoder
,那可就要出大问题了,如果调用方没有配置自己特殊的encoder
和decoder
,那就会把Feign默认的FeignClientsConfiguration
配置的给覆盖掉,如果调用方配置了自己的encoder
和decoder
,那就直接启动不了了,无论哪种情况都是不可接收的。
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
ApplicationContextAware {
......
protected void configureUsingConfiguration(FeignContext context, Feign.Builder builder) {
ErrorDecoder errorDecoder = getOptional(context, ErrorDecoder.class);
if (errorDecoder != null) {
builder.errorDecoder(errorDecoder);
}
Map<String, RequestInterceptor> requestInterceptors = context.getInstances(
this.name, RequestInterceptor.class);
if (requestInterceptors != null) {
builder.requestInterceptors(requestInterceptors.values());
}
}
隔离FeignClient的配置
要想解决上面的问题,不让我提供的feign配置污染调用方的项目,此时需要进到FeignClient注解中,可以看到如下配置项:
/**
* A custom <code>@Configuration</code> for the feign client. Can contain override
* <code>@Bean</code> definition for the pieces that make up the client, for instance
* {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.
*
* @see FeignClientsConfiguration for the defaults
*/
Class<?>[] configuration() default {};
从注释可以看出,configuration
是用来定制FeignClient的配置的,我们需要将代码修改如下:
@ConditionalOnProperty(value = "api.audit.log.enabled", matchIfMissing = true)
public class DemoFeignClientConfig {
@Value("${spring.application.name:UNKNOWN}")
private String appName;
@Bean
public RequestInterceptor headerInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header(HEADER_APP_NAME, appName);
}
};
}
}
去掉jar包的resources/META-INF/spring.factories
下的配置,提供的Api注解改成如下:
@FeignClient(value = "demo-spi",configuration = DemoFeignClientConfig.class)
上面首先是防止DemoFeignClientConfig
被调用方自动扫到,然后在FeignClient注解上指定了配置类,这样就能让DemoFeignClientConfig
里配置的bean只在demo-spi
中生效了,通过测试,验证了结果正确,大功告成。
但是为什么呢?DemoFeignClientConfig
里面还是有@Bean
的存在,理论上来说RequestInterceptor
还是会被注册到springContext里,那么为什么这个bean没有被其他FeignClient找到呢?
FeignContext
如何隔离bean,奥秘就在FeignContext.class
这个类,首先看获取FeignClient的源码中有这样一段:
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
ApplicationContextAware {
/**
* @param <T> the target type of the Feign client
* @return a {@link Feign} client created with the specified data and the context information
*/
<T> T getTarget() {
FeignContext context = applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
我们可以看到在build的时候从容器中拿到了一个FeignContext的实例,再看feign(FeignContext context)
方法:
protected Feign.Builder feign(FeignContext context) {
FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
Logger logger = loggerFactory.create(this.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);
return builder;
}
大家可以跟进去get(context, Encoder.class)
,可以看到构造builder时,是从这个FeignContext中获取对应的ecoder
实例的,具体代码就不贴了,那么FeignContext
到底是什么呢?
从FeignContext
的源码中可以看到,它是继承自NamedContextFactory
的一个类,这个类主要的两个属性如下:
private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
private ApplicationContext parent;
contexts是一个map,key在FeignContext
中是feign的name,value是一个AnnotationConfigApplicationContext,从protected AnnotationConfigApplicationContext createContext(String name)
的源码中可以看出,每个context会去解析配置在FeignClient中的configuration
类,将类中定义的@bean
注册到当前的AnnotationConfigApplicationContext
里,同时将容器的context设置为自己的父context:
protected AnnotationConfigApplicationContext createContext(String name) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
if (this.configurations.containsKey(name)) {
for (Class<?> configuration : this.configurations.get(name)
.getConfiguration()) {
//注册配置类中的bean
context.register(configuration);
}
}
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
if (entry.getKey().startsWith("default.")) {
for (Class<?> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
context.register(PropertyPlaceholderAutoConfiguration.class,
this.defaultConfigType);
context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
this.propertySourceName,
Collections.<String, Object> singletonMap(this.propertyName, name)));
if (this.parent != null) {
//设置自己的父context为容器的context
context.setParent(this.parent);
}
context.setDisplayName(generateDisplayName(name));
context.refresh();
return context;
}
然后在生成FeignClient的时候,获取作用在该Client上的组件时,调用如下方法:
public <T> T getInstance(String name, Class<T> type) {
//获取该Feign对应的context
AnnotationConfigApplicationContext context = getContext(name);
if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
type).length > 0) {
//从自己的context中获取对应的组件,会依次往上从父context中寻找
return context.getBean(type);
}
return null;
}
至此,就搞清了Feign是如何隔离开不同FeignClient的配置。
一些小问题
由于FeignContext是已feign.name隔离的,所以当有不同的Api,但是有相同的Feign.name时,需要全部都配上一样的configuration
,否则配置会覆盖,根据加载顺序的不同会出现不同的效果,偏离配置的预期。
网友评论