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