盘点 Cloud Feign 初始化配置流程

这是我参与更文挑战的第5天,活动详情查看: 更文挑战

总文档 :文章目录

Github : github.com/black-ant

一 . 前言

文章目的 :

  • Feign 主要流程源码分析
  • Feign 的要点分析
  • Feign 的设计思路及扩散思考

二 . 源码梳理

以一个最基础的 OpenFeign 的案例为例 , 我们在使用时通常会有如下操作 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码// Step 1: 开启 Feign 客户端
@EnableFeignClients
@SpringBootApplication
public class ComGangCloudTemplateOrderApplication {..........}


// Step 2 : 准备 FeignClient 对象
@Component
@FeignClient("product-server")
public interface ProductFeignClient {
@GetMapping("/template/get")
CloudTemplateEntity get(@RequestParam("desc") String desc);
}

// Step 3 : 调用 FeignClient 对象
@Autowired
private ProductFeignClient productFeignClient;
productFeignClient.get("order-server")

光看这三个步骤 , 大概可以得出几个问题 :

  1. EnableFeignClients 的作用 ?
  2. @FeignClient 的扫描

2.1 通过 @EnableFeignClients 开启 FeignClients

主要的配置集中在 FeignClientsRegistrar 中 , 主要包括以下内容 :

首先看一下 FeignClientsRegistrar 的调用流程:

  • Step 1 : Bean 加载时调用 registerBeanDefinitions 完成 BeanDefinition 的注册
  • Step 2 : registerDefaultConfiguration 注册 config
  • Step 3 : 扫描 FeignClient , 并且进行注册
  • Step 4 : 注册扫描的所有的 FeignClient

Step 1 : Bean 加载时调用 registerBeanDefinitions 完成 BeanDefinition 的注册

1
2
3
4
5
6
7
8
java复制代码public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// 其中涉及到2个主要的步骤 :
// 1.注册Configuration
registerDefaultConfiguration(metadata, registry);
// 2. 注册 Feign Client
registerFeignClients(metadata, registry);
}

Step 2 : registerDefaultConfiguration 注册 config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码// 1. 获取注解上面的属性 -> PS:001
Map<String, Object> defaultAttrs =
metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true);

// 2. 注册 BeanDefinitionBuilder (PS : 这一步骤实际上构建了一个 BeanDefinitionBuilder)
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
Object configuration) {

// BeanDefinitionBuilder是使用构建器模式构建BeanDefinitions
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientSpecification.class);
// name : default.com.gang.cloud.template.demo.ComGangCloudTemplateOrderApplication
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
// default.com.gang.cloud.template.demo.ComGangCloudTemplateOrderApplication.FeignClientSpecification
registry.registerBeanDefinition(
name + "." + FeignClientSpecification.class.getSimpleName(),builder.getBeanDefinition()
);

}

PS:001 defaultAttrs 参数

image.png

Step 3 : 扫描 FeignClient

整体大纲就是 :

  1. 从 EnableFeignClients 获取属性 attrs
  2. 通过属性 attrs 获取 基础扫描路径 basePackages
  3. 扫描 basePackages 下面的所有标注 @Component 的类
  4. 获取其中的 @FeignClient
  5. 对注解标注的类进行 registry
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码// 注解类的使用主要是 FeignClientsRegistrar
C05- FeignClientsRegistrar(AnnotationMetadata metadata,BeanDefinitionRegistry registry)
M5_01- registerFeignClients : FeignClient 核心方式
- 准备对象 ClassPathScanningCandidateComponentProvider
- 获取标注了 EnableFeignClients 的 Map 集合
- 构建一个 AnnotationTypeFilter
?- 为什么不和上面一样使用 getAnnotationAttributes 直接获取相关类 -> PS:M5_01_01
- getBasePackages 获取需要扫描的路径集合 Set<String>->LV001
FOR- 循环路径集合 : LV001
- findCandidateComponents 获取对应的 Set<BeanDefinition>->LV002
?- 注意 , 这里会通过 findCandidateComponents 查找路径下标注了 Component 的类型 -> PS:M5_01_02
FOR- 循环 BeanDefinition : LV002
- 获取 FeignClient 的属性 Map<String, Object> ->LV003
- registerClientConfiguration 注册配置信息
- registerFeignClient 注册当前 FeignClient
M5_02- registerFeignClient(BeanDefinitionRegistry registry,AnnotationMetadata annotationMetadata, Map<String, Object> attributes)
?- 核心作用就是构建一个 BeanDefined , 其中又主要可以分为4步 -> PS:M5_02_01
1- BeanDefinitionBuilder.genericBeanDefinition 构建一个 BeanDefinitionBuilder
2- definition.getBeanDefinition() 构建一个 AbstractBeanDefinition
3- new BeanDefinitionHolder 构建一个 BeanDefinitionHolder
4- BeanDefinitionReaderUtils.registerBeanDefinition 注入 Bean

以上是主流程 ,但是我们还是可以看一下其中的一点小细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码// PS : 其中省略了部分代码 , 想看完整版的可以看源码
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// 准备 scan 对象用于扫描
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);

// 这一句很重要 , 这是个用于 Scan 的排除筛选器 ,他会屏蔽调无用的 Bean
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);

if (clients == null || clients.length == 0) {
// 添加排除筛选器 -> PS:0002
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = getBasePackages(metadata);
} else {
// 省略 ,同样是为了进行特殊的匹配
}

for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
// 获取注解属性 -> PS:0003
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
// 注册 FeignClient
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}

// PS:0002 排除筛选器的使用
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
for (TypeFilter tf : this.includeFilters) {
//..........
}
}

Step 3 : 扫描 FeignClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();

// 准备 BeanDefinitionBuilder -> org.springframework.cloud.openfeign.FeignClientFactoryBean
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
// 校验属性是否合法
validate(attributes);

// 省略所有的 addPropertyValue 操作 ,此操作为 definition 添加属性

String alias = contextId + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);

// 由于有一个默认 BeanDefinition , 此处通过 primary 进行覆盖
boolean primary = (Boolean) attributes.get("primary");
beanDefinition.setPrimary(primary);

BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
// 向给定的bean工厂注册给定的bean定义
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}

PS : 这个对象在创建Bean过程中会被调用 ,我们后面再说!!!

2.2 FeignAutoConfiguration 的配置

Feign 其实是支持 OKHttp 方式调用的 ,该方法在 FeignAutoConfiguration 中进行配置 , 该配置类中提供了2个连接框架 : HttpClientFeignConfiguration / OkHttpFeignConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
java复制代码// HttpClientFeignConfiguration 的配置项
@Bean
@ConditionalOnMissingBean(HttpClientConnectionManager.class)
public HttpClientConnectionManager connectionManager(ApacheHttpClientConnectionManagerFactory connectionManagerFactory,FeignHttpClientProperties httpClientProperties) {
final HttpClientConnectionManager connectionManager = connectionManagerFactory
.newConnectionManager(httpClientProperties.isDisableSslValidation(),
// 最大连接数
httpClientProperties.getMaxConnections(),
httpClientProperties.getMaxConnectionsPerRoute(),
// 连接存活时间和单位
httpClientProperties.getTimeToLive(),
httpClientProperties.getTimeToLiveUnit(),
this.registryBuilder);

this.connectionManagerTimer.schedule(new TimerTask() {
@Override
public void run() {
connectionManager.closeExpiredConnections();
}
}, 30000, httpClientProperties.getConnectionTimerRepeat());

return connectionManager;
}

@Bean
public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory,
HttpClientConnectionManager httpClientConnectionManager,
FeignHttpClientProperties httpClientProperties) {

RequestConfig defaultRequestConfig = RequestConfig.custom()
.setConnectTimeout(httpClientProperties.getConnectionTimeout())
.setRedirectsEnabled(httpClientProperties.isFollowRedirects())
.build();

this.httpClient = httpClientFactory.createBuilder()
.setConnectionManager(httpClientConnectionManager)
.setDefaultRequestConfig(defaultRequestConfig).build();

return this.httpClient;
}


// OkHttpFeignConfiguration 的配置项
@Bean
@ConditionalOnMissingBean(ConnectionPool.class)
public ConnectionPool httpClientConnectionPool(
FeignHttpClientProperties httpClientProperties,
OkHttpClientConnectionPoolFactory connectionPoolFactory) {
// OKHttp 连接池属性
Integer maxTotalConnections = httpClientProperties.getMaxConnections();
Long timeToLive = httpClientProperties.getTimeToLive();
TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
}

@Bean
public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory,
ConnectionPool connectionPool,
FeignHttpClientProperties httpClientProperties) {

Boolean followRedirects = httpClientProperties.isFollowRedirects();
Integer connectTimeout = httpClientProperties.getConnectionTimeout();
Boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
this.okHttpClient = httpClientFactory.createBuilder(disableSslValidation)
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
.followRedirects(followRedirects).connectionPool(connectionPool)
.build();

return this.okHttpClient;
}

那么问题来了 : 如何切换到 OKHttp 呢?

PS : 网上有一种方法 ,通过配置 okhttp3.OkHttpClient 的方法 , 但是经过个人测试 , 可能由于版本不同会出现问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
java复制代码// 如果细看源码 , 会发现存在2个 OkHttpFeignConfiguration 
//一个在 FeignAutoConfiguration 内部 , 另外一个存在于 org.springframework.cloud.openfeign.clientconfig 包下

// 疑点 : 点开FeignAutoConfiguration$OkHttpFeignConfiguration 上面有多个 Conditional

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
@ConditionalOnProperty("feign.okhttp.enabled")
protected static class OkHttpFeignConfiguration {
//............
}

// [Pro] : 通过 OnClassCondition 测试的时候就会发现 , 由于 ILoadBalancer 存在 , 则不会加载该类下的配置

而在 org.springframework.cloud.openfeign.clientconfig 包下的配置中 , 则有如下要求
@ConditionalOnMissingBean(okhttp3.OkHttpClient.class)

// 所以 , 已知的那种配置实际上会导致2个配置项都不会走



//!!! 那么应该怎么配置 ?



// Step 1 :添加 eign-okhttp 相关配置
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>


// Step 2 : Application 上导入配置文件
@Import(value = {OkHttpFeignConfiguration.class})


// Step 3 : config 中注册 Client
@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureAfter(value = {FeignAutoConfiguration.class, OkHttpFeignConfiguration.class})
public class FeignOkHttpConfig {

@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(okhttp3.OkHttpClient client) {
// 这里还有相关配置 ,暂时省略 , 写流程的时候补上
return new OkHttpLoadBalancingClient(....);
}
}

总结

Feign 的配置篇基本上就说完了 , 后面会说明一个 FeignBean 的创建和一个 Invoke 流程

附录

PS:0003 attributes 中包含哪些属性?

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码C- FeignClient
M- String value() default ""
M- String serviceId() default ""
M- String contextId() default "" : 如果存在,它将用作bean名而不是名称,但不会用作服务id。
M- String name() default ""
M- String qualifier() default ""
M- String url() default ""
M- boolean decode404() default false
M- Class<?>[] configuration() : 为虚客户端定制的配置类。
M- Class<?> fallback()
M- Class<?> fallbackFactory()
M- String path() default ""
M- boolean primary() default true

常见的 FeignClient 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// 覆盖默认配置
@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)
link : https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#spring-cloud-feign-overriding-defaults

// 获取回退的原因
@FeignClient(name = "test", url = "http://localhost:${server.port}/", fallback = Fallback.class)

// 获取触发回退的原因
@FeignClient(name = "testClientWithFactory", url = "http://localhost:${server.port}/",fallbackFactory = TestFallbackFactory.class)

// 配置主 Bean
// PS : 当使用 Feign 和 Spring Cloud CircuitBreaker 回退时,在同一类型的 ApplicationContext 中有多个 bean
@FeignClient(name = "hello", primary = false)

Application 配置篇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
java复制代码feign:
client:
config:
feignName:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
errorDecoder: com.example.SimpleErrorDecoder
retryer: com.example.SimpleRetryer
defaultQueryParameters:
query: queryValue
defaultRequestHeaders:
header: headerValue
requestInterceptors:
- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
decode404: false
encoder: com.example.SimpleEncoder
decoder: com.example.SimpleDecoder
contract: com.example.SimpleContract
capabilities:
- com.example.FooCapability
- com.example.BarCapability
metrics.enabled: false


// 已经配套的 Bean 类 ,Debug get/set 就可以看到使用的节点
@ConfigurationProperties(prefix = "feign.httpclient")
public class FeignHttpClientProperties {

// 禁用SSL验证的默认值
public static final boolean DEFAULT_DISABLE_SSL_VALIDATION = false;

// 最大od连接数的缺省值
public static final int DEFAULT_MAX_CONNECTIONS = 200;

// 每条路由的最大连接数的缺省值
public static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 50;

// 存活的时间的默认值
public static final long DEFAULT_TIME_TO_LIVE = 900L;

// 默认存活时间单位.
public static final TimeUnit DEFAULT_TIME_TO_LIVE_UNIT = TimeUnit.SECONDS;

// 是否重定向
public static final boolean DEFAULT_FOLLOW_REDIRECTS = true;

// 连接超时的默认值
public static final int DEFAULT_CONNECTION_TIMEOUT = 2000;

//连接计时器重复的默认值
public static final int DEFAULT_CONNECTION_TIMER_REPEAT = 3000;

private boolean disableSslValidation = DEFAULT_DISABLE_SSL_VALIDATION;

private int maxConnections = DEFAULT_MAX_CONNECTIONS;

private int maxConnectionsPerRoute = DEFAULT_MAX_CONNECTIONS_PER_ROUTE;

private long timeToLive = DEFAULT_TIME_TO_LIVE;

private TimeUnit timeToLiveUnit = DEFAULT_TIME_TO_LIVE_UNIT;

private boolean followRedirects = DEFAULT_FOLLOW_REDIRECTS;

private int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT;

private int connectionTimerRepeat = DEFAULT_CONNECTION_TIMER_REPEAT;

}

@ConfigurationProperties("feign.client")
public class FeignClientProperties {

private boolean defaultToProperties = true;
private String defaultConfig = "default";
private Map<String, FeignClientConfiguration> config = new HashMap<>();

}

public static class FeignClientConfiguration {

private Logger.Level loggerLevel;

private Integer connectTimeout;

private Integer readTimeout;

private Class<Retryer> retryer;

private Class<ErrorDecoder> errorDecoder;

private List<Class<RequestInterceptor>> requestInterceptors;

private Boolean decode404;

private Class<Decoder> decoder;

private Class<Encoder> encoder;

private Class<Contract> contract;

private ExceptionPropagationPolicy exceptionPropagationPolicy;

//........

}

// Step 1 : Config 配置的类
FeignClientConfiguration
FeignHttpClientProperties
FeignClientProperties


// Step 2 : 配置的位置
C- FeignClientFactoryBean
M- configureUsingProperties
M- configureUsingConfiguration

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%