关于FeignClient的使用大全——进阶篇

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

1,FeignClient的实现原理

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

    /**
     * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation
     * declarations e.g.: {@code @ComponentScan("org.my.pkg")} instead of
     * {@code @ComponentScan(basePackages="org.my.pkg")}.
     * @return the array of 'basePackages'.
     */
    String[] value() default {};

    /**
     * Base packages to scan for annotated components.
     * <p>
     * {@link #value()} is an alias for (and mutually exclusive with) this attribute.
     * <p>
     * Use {@link #basePackageClasses()} for a type-safe alternative to String-based
     * package names.
     * @return the array of 'basePackages'.
     */
    String[] basePackages() default {};

    /**
     * Type-safe alternative to {@link #basePackages()} for specifying the packages to
     * scan for annotated components. The package of each class specified will be scanned.
     * <p>
     * Consider creating a special no-op marker class or interface in each package that
     * serves no purpose other than being referenced by this attribute.
     * @return the array of 'basePackageClasses'.
     */
    Class<?>[] basePackageClasses() default {};

    /**
     * A custom <code>@Configuration</code> for all feign clients. 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
     * @return list of default configurations
     */
    Class<?>[] defaultConfiguration() default {};

    /**
     * List of classes annotated with @FeignClient. If not empty, disables classpath
     * scanning.
     * @return list of FeignClient classes
     */
    Class<?>[] clients() default {};

}

其中@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值在该方法中将不再生效。如下:

@FeignClient(name = "categoryFeignClient",
             url = "http://127.0.0.1:10002", 
             fallback = CategoryFeignClientFallback.class,
             configuration = {FeignConfig.class})
public interface CategoryFeignClient {
    /**
     * 第三方业务接口
     * @param categoryIds 参数
     * @return 结果
     */
    @RequestMapping(method = RequestMethod.GET, value = "/v1/categorys/multi")
    String getCategorys(@RequestParam("categoryIds") String categoryIds, URI newHost);
}

2.9,其他功能的定制:

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  •  通过前面两章对Spring Cloud Ribbon和Spring Cloud Hystrix的介绍,我们已经掌...
    Chandler_珏瑜阅读 212,731评论 15 140
  • 踏着记忆的痕迹,追寻往昔的点点滴滴,从新绿到盛夏,从片片红叶到皑皑白雪,这日日夜夜的朝与夕,晨阳和落日,有...
    雯岚的海阅读 228评论 0 0
  • 匍匐 等着碾压 每一步 都走地孤独 蔓延 我看不清你的终点 就好似 你看不到我的起点 飞跃 你是坠下凡间的梯 没办...
    岚峰阅读 277评论 2 5
  • 印度那烂陀寺的「正法藏」戒贤大师,患了风病。每次发作,手足拘紧,像火烧刀刺般痛苦,想绝食求死。梦见三人,一黄金色,...
    金指尖的花园阅读 503评论 0 2
  • 孤独,只是享受一个人的所有美好 四年后,享受自己 四年前,一如昨晚,在哪个燥热的夏夜,反复辗转难眠,不同的是昨晚是...
    木堇记阅读 214评论 0 1