Feign配置原理

1,640 阅读3分钟

背景

今天项目开发过程中,需要在给别人提供的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,但是如果上面配置了自己的encoderdecoder,那可就要出大问题了,如果调用方没有配置自己特殊的encoderdecoder,那就会把Feign默认的FeignClientsConfiguration配置的给覆盖掉,如果调用方配置了自己的encoderdecoder,那就直接启动不了了,无论哪种情况都是不可接收的。

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,否则配置会覆盖,根据加载顺序的不同会出现不同的效果,偏离配置的预期。