文章目录
- 前言
- 源码解读
- 配置举例
- 通过@EnableFeignClients#defaultConfiguration实现全局配置
- 配置单个客户端组件
- 总结
前言
在上一篇中我们介绍了springcloud_openfeign
通过EnableFeignClients
注解扫描并注册每个@FeignClient
标识的接口对应的FeignClientFactoryBean
到spring容器中, 本节我们来了解一下这个类的具体内容。
源码解读
FeignClientFactoryBean看名字它是一个工厂bean, 负责创建某个类型的对象, 注意FactoryBean与BeanFactory是不同的, 一个是具体的bean, 一个是工厂。FactoryBean 用于创建特定类型的 bean, 而BeanFactory 用于管理和创建各种类型的 bean。
FeignClientFactoryBean
public class FeignClientFactoryBeanimplements FactoryBean<Object>, InitializingBean, ApplicationContextAware, BeanFactoryAware {//...BeanDefinition注入的一些属性们@Overridepublic Object getObject() {return getTarget();}@Overridepublic void afterPropertiesSet() {Assert.hasText(contextId, "Context id must be set");Assert.hasText(name, "Name must be set");}
}
FeignClientFactoryBean实现了bean生命周期中的三个接口, InitializingBean
用来在初始化bean之后做处理(注意初始化和实例化的区别), ApplicationContextAware
负责注入ApplicationContext
容器上下文对象, BeanFactoryAware
负责注入BeanFactory
对象。
InitializingBean的afterPropertiesSet方法用来校验contextId
不能为空, name
不能为空, 回顾上一篇的介绍
- contextId取值顺序为
contextId > serviceId(默认没有) > name > value
- name取值顺序为
serviceId>name>value
FactoryBean接口提供的getObject
方法就是真正用来创建feign接口对应的那个对象, 在之前的篇幅中了解到openFeign对接口是生成了一个代理对象。它仅仅是调用了getTarget
方法。
getTarget
<T> T getTarget() {// 从上下文中获取FeignClientFactory对象FeignClientFactory feignClientFactory = beanFactory != null ? beanFactory.getBean(FeignClientFactory.class): applicationContext.getBean(FeignClientFactory.class);// 获取Feign.Builder对象Feign.Builder builder = feign(feignClientFactory);// 当前url为空, 并且没有contextId对应的客户端配置if (!StringUtils.hasText(url) && !isUrlAvailableInConfig(contextId)) {if (LOG.isInfoEnabled()) {LOG.info("For '" + name + "' URL not provided. Will try picking an instance via load-balancing.");}if (!name.startsWith("http://") && !name.startsWith("https://")) {url = "http://" + name;}else {url = name;}// 加上pathurl += cleanPath();// 1.设置请求客户端client 2.返回接口代理对象return (T) loadBalance(builder, feignClientFactory, new HardCodedTarget<>(type, name, url));}// 配置了url属性if (StringUtils.hasText(url) && !url.startsWith("http://") && !url.startsWith("https://")) {url = "http://" + url;}// 获取contextId对应的ClientClient client = getOptional(feignClientFactory, Client.class);if (client != null) {if (client instanceof FeignBlockingLoadBalancerClient) {// not load balancing because we have a url,// but Spring Cloud LoadBalancer is on the classpath, so unwrapclient = ((FeignBlockingLoadBalancerClient) client).getDelegate();}if (client instanceof RetryableFeignBlockingLoadBalancerClient) {// not load balancing because we have a url,// but Spring Cloud LoadBalancer is on the classpath, so unwrapclient = ((RetryableFeignBlockingLoadBalancerClient) client).getDelegate();}builder.client(client);}// 使用容器中或者属性中的FeignBuilderCustomizer对`Feign.Builder`进行处理applyBuildCustomizers(feignClientFactory, builder);// 从父子容器中获取Targeter对象Targeter targeter = get(feignClientFactory, Targeter.class);// 解析TargeterHardCodedTarget<T> objectHardCodedTarget = resolveTarget(feignClientFactory, contextId, url);return targeter.target(this, builder, feignClientFactory, objectHardCodedTarget);
}
方法小结
- 从spring上下文中获取
FeignClientFactory
对象, 这个接口是feign客户端接口父子容器的关键 - 构建Feign.Builder; 这个是比较熟悉的面孔了, openFeign中的Feign.Builder对象
- 如果url为空(也就是
@FeignClient
注解里的url属性), 并且注入的属性类不存在或者url值为空
- 从当前feign客户端上下文获取targeter对象, 然后生成接口代理对象返回
- 如果url不为空, 也就是
@FeignClient
注解指定了url参数
- 设置请求客户端client, 如果自定义设置了client为
FeignBlockingLoadBalancerClient
或者FeignBlockingLoadBalancerClient
对象, 也不会生效 - 直接创建openFeign中的
HardCodedTarget
- 如果url为空, 但是注入的属性类中url不为空
- 如果需要刷新客户端, 那么创建的是
RefreshableHardCodedTarget
- 如果属性配置类中有url参数的话, 创建的是PropertyBasedTarget, 否则直接报错了
- 使用当前feign客户端和spring的上下文中的
FeignBuilderCustomizer
对Feign.Builder
进行增强 - 使用当前FactoryBean中的
FeignBuilderCustomizer
对Feign.Builder
进行增强 - 从当前feign客户端上下文获取targeter对象, 然后生成接口代理对象返回
总得来说就是根据不同的url配置, 会生成不同的Target
对象
解释起来有点绕, 举几个例子
1、无默认url配置
@FeignClient(name = "url-demo", path = "/feign")
public interface UrlDemoRemote {
}
这种情况bean默认的url参数(FeignClient.url)为空, 并且没有指定配置类属性, 那么它将使用HardCodedTarget。
此时的url为 http:// + name + path
也就是 http://url-demo/feign
, 这里的{url-demo}可以被环境中的内容替换
如果开启info及以上的日志级别, 将可以看到 o.s.c.o.FeignClientFactoryBean - For ‘url-demo’ URL not provided. Will try picking an instance via load-balancing.
2、@FeignClient注解添加url属性
@FeignClient(url = "localhost:8081", name = "url-demo", path = "/feign")
public interface UrlDemoRemote {
}
这种情况, 将会使用HardCodedTarget
对象
此时的url为 http://{localhost:localhost}/path
, 也就是http://localhost/feign
3、属性配置url
@FeignClient(contextId = "urlDemoRemote", name = "url-demo", path = "/feign")
public interface UrlDemoRemote {
}spring:cloud:openfeign:client: # feign的组件配置defaultToProperties: true # 设置配置文件中的优先级高于容中的配置defaultConfig: default # 指定默认配置是哪个, 也可以指定为下面写的urlDemoRemote; 默认指定的是defaultconfig:urlDemoRemote:url: http://localhost:8080
这种情况, 将会使用PropertyBasedTarget
对象
此时的url为 http://localhost:8080/feign
url的顺序为: @FeignClient.url > spring文件中的url > 无配置
feign方法
protected Feign.Builder feign(FeignClientFactory context) {// 从父子容器中获取FeignLoggerFactory对象FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);// 创建loggerLogger logger = loggerFactory.create(type);// @formatter:offFeign.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;
}
方法小结
- 从父子容器中获取
FeignLoggerFactory
来创建Logger
对象 - 然后就是从父子容器中获取
Encoder
,Decoder
,Contract
对象 - 构建
Feign.Builder
对象 - 添加配置到
Feign.Builder
中
这个方法主要就是构建Feign.Builder
对象
configureFeign
使用容器中配置的组件和属性配置填充Feign.Builder
protected void configureFeign(FeignClientFactory context, Feign.Builder builder) {// 配置的客户端属性FeignClientProperties properties = beanFactory != null ? beanFactory.getBean(FeignClientProperties.class): applicationContext.getBean(FeignClientProperties.class);// 从父子容器中获取FeignClientConfigurer对象FeignClientConfigurer feignClientConfigurer = getOptional(context, FeignClientConfigurer.class);// 默认是true, 继承父容器的配置setInheritParentContext(feignClientConfigurer.inheritParentConfiguration());// 处理属性对象的配置if (properties != null && inheritParentContext) {// 判断是否为默认属性, 默认是tureif (properties.isDefaultToProperties()) {// 添加容器中获取的配置configureUsingConfiguration(context, builder);// 添加配置文件中的配置; 配置的默认配置 已经当前feign接口上下文的配置(全局配置和单个接口的配置)configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()),properties.getConfig().get(contextId), builder);}else {// 添加配置文件中的配置; 配置的默认配置 已经当前feign接口上下文的配置(全局配置和单个接口的配置)configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()),properties.getConfig().get(contextId), builder);// 添加容器中获取的配置configureUsingConfiguration(context, builder);}// 1.添加请求头 2.添加query参数(@QueryMap)configureDefaultRequestElements(properties.getConfig().get(properties.getDefaultConfig()),properties.getConfig().get(contextId), builder);}else {// 添加容器中获取的配置configureUsingConfiguration(context, builder);}
}
方法小结
- 获取配置的属性对象; 前缀为
spring.cloud.openfeign.client
- 设置是否启用配置对象中的内容; inheritParentContext属性控制, 默认是true
- 如果属性对象是默认配置,
properties.isDefaultToProperties()
属性控制, 默认是true- 先从容器中获取配置信息
- 再从属性配置中获取, 它将覆盖容器中的配置
- 如果属性对象不是默认配置, 那么先从加载属性中的配置, 再加载容器中的配置
- 添加属性对象中配置的请求头和请求行参数
- 如果没有配置属性对象内容, 那么直接从上下文中获取即可
这里通过对象 FeignClientConfigurer
来开启是否可以从父容器中获取配置对象
它默认的配置如下, FeignClientConfigurer是一个接口, 默认开启父容器配置
public interface FeignClientConfigurer {// 默认开启父容器配置default boolean inheritParentConfiguration() {return true;}
}
@Bean
@ConditionalOnMissingBean(FeignClientConfigurer.class)
public FeignClientConfigurer feignClientConfigurer() {return new FeignClientConfigurer() {};
}
configureUsingConfiguration
使用bean来配置
protected void configureUsingConfiguration(FeignClientFactory context, Feign.Builder builder) {// 1.容器中获取logger的级别Logger.Level level = getInheritedAwareOptional(context, Logger.Level.class);if (level != null) {builder.logLevel(level);}// 2.重试对象Retryer retryer = getInheritedAwareOptional(context, Retryer.class);if (retryer != null) {builder.retryer(retryer);}// 3.ErrorDecoderErrorDecoder errorDecoder = getInheritedAwareOptional(context, ErrorDecoder.class);if (errorDecoder != null) {builder.errorDecoder(errorDecoder);}else {// 直接从父子容器中获取, 这里不管有没有开启父容器了FeignErrorDecoderFactory errorDecoderFactory = getOptional(context, FeignErrorDecoderFactory.class);if (errorDecoderFactory != null) {ErrorDecoder factoryErrorDecoder = errorDecoderFactory.create(type);builder.errorDecoder(factoryErrorDecoder);}}// 4.Request.OptionsRequest.Options options = getInheritedAwareOptional(context, Request.Options.class);if (options == null) {options = getOptionsByName(context, contextId);}if (options != null) {builder.options(options);readTimeoutMillis = options.readTimeoutMillis();connectTimeoutMillis = options.connectTimeoutMillis();followRedirects = options.isFollowRedirects();}// 5.请求拦截器集合Map<String, RequestInterceptor> requestInterceptors = getInheritedAwareInstances(context,RequestInterceptor.class);if (requestInterceptors != null) {List<RequestInterceptor> interceptors = new ArrayList<>(requestInterceptors.values());AnnotationAwareOrderComparator.sort(interceptors);builder.requestInterceptors(interceptors);}// 6.响应拦截器ResponseInterceptor responseInterceptor = getInheritedAwareOptional(context, ResponseInterceptor.class);if (responseInterceptor != null) {builder.responseInterceptor(responseInterceptor);}// 7.@QueryMap和@HeaderMap参数编码器QueryMapEncoder queryMapEncoder = getInheritedAwareOptional(context, QueryMapEncoder.class);if (queryMapEncoder != null) {builder.queryMapEncoder(queryMapEncoder);}// 8.是否忽略404if (dismiss404) {builder.dismiss404();}// 9.重试异常传播策略ExceptionPropagationPolicy exceptionPropagationPolicy = getInheritedAwareOptional(context,ExceptionPropagationPolicy.class);if (exceptionPropagationPolicy != null) {builder.exceptionPropagationPolicy(exceptionPropagationPolicy);}// 10.Capability(增强)Map<String, Capability> capabilities = getInheritedAwareInstances(context, Capability.class);if (capabilities != null) {capabilities.values().stream().sorted(AnnotationAwareOrderComparator.INSTANCE).forEach(builder::addCapability);}
}
方法小结
如果打开了父子容器开关(feignClientConfigurer.inheritParentConfiguration()为true), 那么这个方法中的配置对象都是从父子容器中获取的, 否则就只是从当前容器中获取, 为了方便, 以下容器就代表父子容器或者当前容器
- 从容器中获取日志等级(Logger.Level)添加到Feign.Builder
- 从容器中获取重试对象(Retryer)添加到Feign.Builder
- ErrorDecoder
- Request.Options
- 请求拦截器集合
RequestInterceptor
- 响应拦截器
ResponseInterceptor
- @QueryMap和@HeaderMap参数编码器
QueryMapEncoder
- 是否忽略404
- 重试异常传播策略
- Capability(增强)
这里就从容器中获取了十个配置项设置到了Feign.Builder
中
configureUsingProperties
configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()),
properties.getConfig().get(contextId), builder);这里通过contextId与属性配置对应
例如
spring:cloud:openfeign:client: # feign的组件配置defaultToProperties: true # 默认使用配置文件中的配置defaultConfig: defaultconfig:default: # 默认配置, 将对所有的feign客户端生效loggerLevel: HEADERSretryer: feign.Retryer.Defaultabc: # 指定contextId的客户端connectTimeout: 5000readTimeout: 5000// 这个@FeignClient标识的客户端将对应上面abc的配置
@FeignClient(contextId = "abc")
方法解释
protected void configureUsingProperties(FeignClientProperties.FeignClientConfiguration baseConfig,FeignClientProperties.FeignClientConfiguration finalConfig, Feign.Builder builder) {// 添加默认配置configureUsingProperties(baseConfig, builder);// 添加项目最终配置configureUsingProperties(finalConfig, builder);// 添加是否忽略404; 如果客户端配置(finalConfig)存在就取它, 否则取全局配置(baseConfig), 否则为空Boolean dismiss404 = finalConfig != null && finalConfig.getDismiss404() != null ? finalConfig.getDismiss404(): (baseConfig != null && baseConfig.getDismiss404() != null ? baseConfig.getDismiss404() : null);if (dismiss404 != null) {if (dismiss404) {builder.dismiss404();}}
}
-
这里也是先调用的
configureUsingProperties
方法, 只是先配置的default配置项, 再配置的指定feign接口的客户端, 也就是说指定feign客户端的配置优先级更高 -
添加是否忽略404; 如果客户端配置(finalConfig)存在就取它, 否则取全局配置(baseConfig), 否则为空
配置举例
通过@EnableFeignClients#defaultConfiguration实现全局配置
添加配置application.yml
server:port: 8080logging:level:org.springframework.cloud.openfeign: DEBUG
spring:cloud:openfeign:cache:enabled: false # 先关闭, 不然启动会报错okhttp:enabled: true
定义接口
@FeignClient(contextId = "urlDemoRemote", url = "localhost:8080", name = "url-demo", path = "/configDemo")
public interface UrlDemoRemote {// @RequestLine("POST /getPerson")
// @Headers("Content-Type:application/json")@PostMapping(value = "/getPerson", consumes = "application/json")Person getPerson(Person person);
}
注意openfeign-cloud中默认不支持openfeign的原始@RequestLine("POST")
和@Headers
注解了, 改成了@ReqeustMapping那种,
@PostMapping也是@ReqeustMapping
服务端接口
@PostMapping("/configDemo/getPerson")
public Person getPerson(@RequestBody Person person) {System.out.println("uncleqiao 收到body:" + person);person.setName("小乔同学");person.setAge(20);return person;
}
全局配置
/*** 为全局客户端配置解码器*/
public class MyGlobalDecoder implements Decoder {private final JavaTimeModule javaTimeModule = new JavaTimeModule();private final Decoder decoder = new JacksonDecoder(List.of(javaTimeModule));@Overridepublic Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {System.out.println("MyGolbalDecoder ...");return decoder.decode(response, type);}
}/*** 为全局客户端配置编码器*/
public class MyGlobalEncode implements Encoder {private JavaTimeModule javaTimeModule = new JavaTimeModule();private JacksonEncoder encoder = new JacksonEncoder(List.of(javaTimeModule));@Overridepublic void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {System.out.println("MyGlobalEncode ...");encoder.encode(object, bodyType, template);}
}
启动类
@SpringBootApplication
@EnableFeignClients(basePackages = "per.qiao.feign.starter", defaultConfiguration = {MyGlobalEncode.class, MyGlobalDecoder.class})
public class FeignStudyApplication {public static void main(String[] args) {System.setProperty("spring.profiles.active", "urldemo");SpringApplication.run(FeignStudyApplication.class, args);}
}
这里注意defaultConfiguration的配置
测试
gav: 引入springboot的测试包
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
</dependency>
测试类
@SpringBootTest
public class ConfigTest {@Autowiredprivate UrlDemoRemote urlDemoRemote;@Testvoid globlaConfigTest() {Person person = urlDemoRemote.getPerson(new Person("小乔同学", 18, 1, LocalDate.now()));System.out.println(person);}
}
结果
// 服务端打印
uncleqiao 收到body:Person(name=小乔同学, age=18, gender=1, birthday=2024-12-12)// 客户端打印
MyGlobalEncode ...
MyGolbalDecoder ...
Person(name=小乔同学, age=20, gender=1, birthday=2024-12-12)
配置单个客户端组件
@FeignClient(contextId = "urlDemoRemote", url = "localhost:8080", configuration = {MyContextDecoder.class, MyContextEncode.class}, name = "url-demo", path = "/configDemo")
public interface UrlDemoRemote {// @RequestLine("POST /getPerson")
// @Headers("Content-Type:application/json")@PostMapping(value = "/getPerson", consumes = "application/json")Person getPerson(Person person);
}
这里注意configuration的配置, 它给当前feign客户端专门配置组件用的
此时我们直接运行demo的话会报错, 原因是FeignClientFactoryBean#get方法会从父子容器中按照类型获取bean, 它会获取到多个, 然后就会抛异常, 原理如下
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));/*** 从父子容器中获取一个bean, 没有找到会抛异常*/
protected <T> T get(FeignClientFactory context, Class<T> type) {// 从contextId对应的上下文中或者spring上下文获取type对象(从父子容器中获取对象)T instance = context.getInstance(contextId, type);if (instance == null) {throw new IllegalStateException("No bean found of type " + type + " for " + contextId);}return instance;
}
// context#getInstance 这个方法获取多个直接返回null
public <T> T getInstance(String name, Class<T> type) {GenericApplicationContext context = getContext(name);try {// 容器中获取bean, 多个或者没有都会抛异常return context.getBean(type);}catch (NoSuchBeanDefinitionException e) {// ignore}return null;
}
所以@EnableFeignClients#defaultConfiguration
和@FeignClient#configuration
中不要配置相同类型的组件
总结
本节主要介绍了springcloud_openfeign如何根据@EnableFeignClients
注解和@FeignClient
以及父子容器来构建Feign.Builder对象, 并处理Target需要的url, 最后通过Feign.Builder#target构建feign接口的代理对象。
本文没有具体介绍其中父子容器的相关细节, 以及配置的一些具体内容, 这些将会在后面的文章中介绍到
@EnableFeignClients
通过@Import
注解给spring容器注入了FeignClientFactoryBean
对象, 并且它是一个实现了FactoryBean
接口的bean工厂FeignClientFactoryBean
的getObject方法创建了我们实际需要和客户端对象- Target对象根据url参数所在的位置会有不同的类型
- 如果url在
@FeignClient#url
中, 它会生成HardCodedTarget
- 如果url在配置文件中(对应一个属性类),
- 如果可刷新客户端
RefreshableHardCodedTarget
(这里不考虑这种情况) - 否则生成
PropertyBasedTarget
- 如果可刷新客户端
- 如果以上两种情况都不是, 那么url就是
@FeignClient#name
属性, 它会生成HardCodedTarget
, 这种没有配置url的就会做负载均衡, 有url的直接用url调用就行
- 父子容器中获取
FeignLoggerFactory
,Feign.Builder
,Encoder
,Decoder
,Contract
对象 - 可以通过
FeignClientConfigurer
对象控制是否让属性文件中的配置内容生效
- 如果属性配置生效的话, 可以通过
defaultToProperties
为true, 设置属性配置优先级高于容器中配置的优先级, 为false则顺序相反
- 如果没有开启属性配置生效, 那么只从容器中获取组件设置给
Feign.Builder
- 可以通过容器中的
FeignBuilderCustomizer
对象对Feign.Builder
做最终调整 - 属性文件可配置的组件有以下10个
-
- Logger.Level: 日志级别
-
- 请求客户端的链接超时(connectTimeout), 读取超时(readTimeout), 跟随服务端重定向(followRedirects)
-
- Retryer: 重试对象
-
- ErrorDecoder 异常编码
-
- RequestInterceptor: 追加请求拦截器, 注意这里是追加
-
- ResponseInterceptor: 响应拦截器, 注意是追加
-
- Encoder: 编码器
-
- Decoder: 解码器
-
- Contract: 约定解析器(约定并解析注解、参数的对象)
-
- 重试异常传播策略
-
- Capability增强策略
-
- QueryMapEncoder: @QueryMap和@HeaderMap参数编码器