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

作者: 一曲畔上 | 来源:发表于2020-01-08 10:27 被阅读0次

    关于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,其他功能的定制:

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

    相关文章

      网友评论

        本文标题:关于FeignClient的使用大全——进阶篇

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