转自: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在自己的config里定义自己的httpClient;
2.1.3,根据httpclient定义Feign的ApacheHttpClient:
@Bean @Primary public Client feignClient(HttpClient httpClient) { return new ApacheHttpClient(httpClient); }
2.1.4,定义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类中定义了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
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。