转自:https://www.jianshu.com/p/50fd582b739f

关于FeignClient的基本使用,我在上一篇文章关于FeignClient的使用大全——使用篇已经介绍过了,大家可以先浏览一遍。
这一篇文章仍然是关于FeignClient,不过是进阶篇,我来讲讲如何定制自己期望的FeignClient。

1,FeignClient的实现原理

我们知道,想要开启FeignClient,首先要素就是添加@EnableFeignClients注解。其主要功能是初始化FeignClient的配置和动态执行client的请求。
我们看看EnableFeignClients的源代码,其核心是

其中@Import(FeignClientsRegistrar.class)是用来初始化FeignClient配置的。我们接着看其代码,找到核心实现代码

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        registerDefaultConfiguration(metadata, registry);
        registerFeignClients(metadata, registry);
    }

其中,registerDefaultConfiguration(metadata, registry)是用来加载@EnableFeignClients中的defaultConfiguration和@FeignClient中的configuration配置文件。代码实现代码比较简单,不再细说。
registerFeignClients(metadata, registry)是用来加载@EnableFeignClients中的其他配和@FeignClient中的其他配置。这是该文章要说的重点。
我们找到下面的代码

    private void registerFeignClient(BeanDefinitionRegistry registry,
            AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        String className = annotationMetadata.getClassName();
        BeanDefinitionBuilder definition = BeanDefinitionBuilder
                .genericBeanDefinition(FeignClientFactoryBean.class);
        validate(attributes);
        definition.addPropertyValue("url", getUrl(attributes));
        definition.addPropertyValue("path", getPath(attributes));
        String name = getName(attributes);
        definition.addPropertyValue("name", name);
        String contextId = getContextId(attributes);
        definition.addPropertyValue("contextId", contextId);
        definition.addPropertyValue("type", className);
        definition.addPropertyValue("decode404", attributes.get("decode404"));
        definition.addPropertyValue("fallback", attributes.get("fallback"));
        definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

        String alias = contextId + "FeignClient";
        AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();

        boolean primary = (Boolean) attributes.get("primary"); // has a default, won't be
                                                                // null

        beanDefinition.setPrimary(primary);

        String qualifier = getQualifier(attributes);
        if (StringUtils.hasText(qualifier)) {
            alias = qualifier;
        }

        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
                new String[] { alias });
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    }

从其中可以看到,该初始化是对FeignClientFactoryBean的初始化,接着我们进入FeignClientFactoryBean的代码中

    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;
    }

该段代码就是动态实现FeignClient的基本逻辑,从这里可以看到,它实现了下面几个组件:Feign.Builder、logger、encoder、decoder和contract。
我们先继续看configureFeign(context, builder)的代码

    protected void configureFeign(FeignContext context, Feign.Builder builder) {
        FeignClientProperties properties = this.applicationContext
                .getBean(FeignClientProperties.class);
        if (properties != null) {
            if (properties.isDefaultToProperties()) {
                configureUsingConfiguration(context, builder);
                configureUsingProperties(
                        properties.getConfig().get(properties.getDefaultConfig()),
                        builder);
                configureUsingProperties(properties.getConfig().get(this.contextId),
                        builder);
            }
            else {
                configureUsingProperties(
                        properties.getConfig().get(properties.getDefaultConfig()),
                        builder);
                configureUsingProperties(properties.getConfig().get(this.contextId),
                        builder);
                configureUsingConfiguration(context, builder);
            }
        }
        else {
            configureUsingConfiguration(context, builder);
        }
    }

其中configureUsingConfiguration(...)是使用我们定义的属性去更新Feign.Builder;configureUsingProperties是用我们定义的default属性去更新Feign.Builder。
继续看configureUsingConfiguration(...)

    protected void configureUsingConfiguration(FeignContext context,
            Feign.Builder builder) {
        Logger.Level level = getOptional(context, Logger.Level.class);
        if (level != null) {
            builder.logLevel(level);
        }
        Retryer retryer = getOptional(context, Retryer.class);
        if (retryer != null) {
            builder.retryer(retryer);
        }
        ErrorDecoder errorDecoder = getOptional(context, ErrorDecoder.class);
        if (errorDecoder != null) {
            builder.errorDecoder(errorDecoder);
        }
        Request.Options options = getOptional(context, Request.Options.class);
        if (options != null) {
            builder.options(options);
        }
        Map<String, RequestInterceptor> requestInterceptors = context
                .getInstances(this.contextId, RequestInterceptor.class);
        if (requestInterceptors != null) {
            builder.requestInterceptors(requestInterceptors.values());
        }
        QueryMapEncoder queryMapEncoder = getOptional(context, QueryMapEncoder.class);
        if (queryMapEncoder != null) {
            builder.queryMapEncoder(queryMapEncoder);
        }
        if (this.decode404) {
            builder.decode404();
        }
    }

虽然使用了3次属性初始化,其实3次大体逻辑是一样的,只是所使用的context不一样而已。相关context的优先级顺序遵循如下规则:
当没定义FeignClientProperties对应的bean时,从全局context查找对属性;
当定义了FeignClientProperties对应的bean时:
如果defaultToProperties=true
先从全局context查找对应属性并且初始化;再从default的context中查找对应属性并且初始化;最后从当前配置的context中查找属性并且初始化。
也就是配置文件优先级顺序是:appConfig < defaultConfig < clientConfig。
如果defaultToProperties=false
先从default的context中查找对应属性并且初始化;在从当前配置的context中查找属性并且初始化;最后从全局context查找对应属性并且初始化。
也就是配置文件优先级顺序是:defaultConfig < clientConfig < appConfig 。
这段代码的逻辑是从对应的context中分别查找logLevel、retryer、errorDecoder、options、requestInterceptors、queryMapEncoder、decode404等组件,然后重新初始化Feign.Builder,从而达到定制FeignClient的目的。

2,FeignClient的功能定制

通过前面的分析,那我们想要定制自己需要的FeignClient就轻而易举了。我们以一下情况来举例说明:

2.1,使用Apache的Httpclient替换Ribbon/loadbalance配置:

有时候,我们的Feignclient没有启用注册中心,那我们就要启用FeignClient的url属性来标明被调用方。此时,启用Httpclient的连接池方式可能会比Ribbon的客户端loadbalance方式更好,那么,我们可以按照如下方式定制我们的FeignClient:

2.1.1,引入jar包

        <!-- apache httpclient -->
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-httpclient</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpmime</artifactId>
        </dependency>

相关版本号可自行根据自己的配置来定。

2.1.2,定义Apache的httpclient的bean

方案一,可以直接引入HttpClientFeignConfiguration;


 
引入HttpClientFeignConfiguration

方案二,可以参照HttpClientFeignConfiguration在自己的config里定义自己的httpClient;

2.1.3,根据httpclient定义Feign的ApacheHttpClient:

    @Bean
    @Primary
    public Client feignClient(HttpClient httpClient) {
        return new ApacheHttpClient(httpClient);
    }

2.1.4,定义Feign.Builder

 
Feign.Builder定义

其实,这个定义不是必须的,但是,我们为了避免其他的client对其影响,这样做可以确保正确。

2.2,支持文件上传配置:

httpclient默认启用的encoder是SpringEncoder,是不支持文件上传的,为了支持文件上传,我们需要如下定制:

2.2.1,引入jar包

        <!-- 解决Feign的 application/x-www-form-urlencoded和multipart/form-data类型 -->
        <dependency>
            <groupId>io.github.openfeign.form</groupId>
            <artifactId>feign-form</artifactId>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign.form</groupId>
            <artifactId>feign-form-spring</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
        </dependency>

相关版本号根据自己的环境自行定义。

2.2.2,定义SpringFormEncoder和Feign.Builder

    @Bean
    @Primary
    public Encoder multipartFormEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
        return new SpringFormEncoder(new SpringEncoder(messageConverters));
    }

    @Bean
    @Scope("prototype")
    public Feign.Builder feignBuilder(Encoder encoder) {
        return Feign.builder().encoder(encoder);
    }

注意这里,SpringEncoder其实也支持文件上传,但是仅仅支持单个MultipartFile的文件上传,不支持MultipartFile[]或者其他类型的多文件上传,因此需要再用SpringFormEncoder封装一层

2.3,支持Hystrix配置:

2.3.1,引入FeignClientsConfiguration

 
FeignClientsConfiguration

因为在FeignClientsConfiguration类中定义了Feign.Builder

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
    protected static class HystrixFeignConfiguration {

        @Bean
        @Scope("prototype")
        @ConditionalOnMissingBean
        @ConditionalOnProperty(name = "feign.hystrix.enabled")
        public Feign.Builder feignHystrixBuilder() {
            return HystrixFeign.builder();
        }

    }

2.3.2,HystrixFeign.builder加载

配置feign.hystrix.enabled=true

2.4,用业务定义的log日志系统替换FeignClient默认日志系统:

2.4.1,实现业务日志系统代理Feignclient日志系统类

    final class FeignLog extends Logger {
        private Log log;
        
        public FeignLog(Class<?> clazz) {
            log = LogFactory.getLog(clazz);
        }
        
        @Override
        protected void log(String configKey, String format, Object... args) {
            if (log.isDebugEnabled()) {
                log.debug(String.format(methodTag(configKey) + format, args));
            }
        }
    }

2.4.2,定义日志系统bean

    @Bean
    @Primary
    public Logger logger() {
        return new FeignLog(this.getClass());
    }
    @Bean
    @Scope("prototype")
    public Feign.Builder feignBuilder(Logger logger) {
        return Feign.builder().logger(logger);
    }

2.5,定义FeignClient的request的重试机制:

2.5.1,定义重试bean

    @Bean
    @Primary
    public Retryer feignRetryer() {
        return Retryer.NEVER_RETRY;
    }

2.5.1,初始化Feign.builder

    @Bean
    @Scope("prototype")
    public Feign.Builder feignBuilder(Retryer retryer) {
        return Feign.builder().retryer(retryer);
    }

2.6,启用response的压缩功能:

2.6.1,开启response的压缩属性

feign:
  compression: 
    response: 
      enabled: true
      useGzipDecoder: true

2.6.2,定义DefaultGzipDecoder的bean

    @Bean
    @Primary
    @ConditionalOnProperty("feign.compression.response.useGzipDecoder")
    public Decoder responseGzipDecoder(ObjectFactory<HttpMessageConverters> messageConverters) {
        return new OptionalDecoder(new ResponseEntityDecoder(
                new DefaultGzipDecoder(new SpringDecoder(messageConverters))));
    }

由于该bean是有条件的,所以,无需强制加载到Feign.builder,让其自动加载即可。

2.7,自定义UserAgent:

使用Apache Httpclient的FeignClient的请求,默认会添加UserAgent:Apache-HttpClientxxxxxxx,如果我们需要自定义UserAgent,可有下面多种方法:
方法1,使用系统属性http.agent:

System.setProperty("http.agent", "MyUserAgent");

方法2,通用设置方式:

    @Bean
    public RequestInterceptor uaRequestInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                template.header("User-Agent", "MyUserAgent");
            }
        };
    }

2,8,动态请求地址的host

有时候,我们可能会需要动态更改请求地址的host,也就是@FeignClient中的url的值在我调用的是才确定。此时我们就可以利用他的一个高级用法实现这个功能:
在定义的接口的方法中,添加一个URI类型的参数即可,该值就是新的host。此时@FeignClient中的url值在该方法中将不再生效。如下:

2.9,其他功能的定制:

关于其他功能的定制,这里就不再赘述,大家可以参照上述实现原理。如果还是不明白可以留言。



作者:一曲畔上
链接:https://www.jianshu.com/p/50fd582b739f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted on 2020-09-22 03:46  Sharpest  阅读(6368)  评论(0编辑  收藏  举报