關於FeignClient的使用大全——進階篇


轉自: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
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM