Feign配置原理

作者: ZhipingZhang | 来源:发表于2019-07-04 14:10 被阅读109次

背景

今天项目开发过程中,需要在给别人提供的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,否则配置会覆盖,根据加载顺序的不同会出现不同的效果,偏离配置的预期。

相关文章

网友评论

    本文标题:Feign配置原理

    本文链接:https://www.haomeiwen.com/subject/dfolhctx.html