springCloud-openFeign使用原理

springCloud-openFeign使用原理

​ 本文讲解spring-cloud环境下的openFeign的用法,探究spring-cloud是如何让openfeign开箱即用的。本文会假设读者已经熟练使用openfeign,对openFeign源码已有了解。

  • TOC
    {:toc}

1.先让项目运行起来

新建spring-cloud项目,添加如下依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

下面以获取百度贴吧首页为例。

写一个接口,其中@FeignClient注解上url属性标识请求的域名,@GetMapping标注请求methodpath@RequestParam标注请求参数。

1
2
3
4
5
6
7
8
9
//准备获取这个网页 https://tieba.baidu.com/f?kw=java

@FeignClient(name = "javaBaCLient", url = "https://tieba.baidu.com")
public interface Baidu {

@GetMapping(value = "/f")
String getMainPage(@RequestParam("kw") String kw);

}

启动类上添加注解@EnableFeignClients

​ 将Badu.class类注入到其某个类中,并调用其中的方法,可以看到spring-cloud已经自动将上面的接口生成了代理类,一个开箱即用的案例就写好了。下面开始探究下它的实现原理和默认配置。

1
2
3
4
5
6
7
8
@Autowired
private Baidu baidu;

@PostConstruct
public void test() {
String java = baidu.getMainPage("java");
System.out.println(java);
}

总结

​ 开箱即用,非常简单的就能将Demo跑起来。如果在已经存在的SpringBoot项目上添加spring-cloud-openfeign,要注意spring-cloud的版本必须匹配。这里有一个网址是spring-cloud项目的版本对应关系

https://start.spring.io/actuator/info

2.自动装配

​ 这里可能你会很好去,spring-cloud是怎么做的,他到底为我们做了什么?

首先从@EnableFeignClients注解上找到org.springframework.cloud.openfeign.FeignClientsRegistrar

查看它的registerBeanDefinitions方法,它做了两件事情。

  1. 找到@EnableFeignClients注解,将其属性defaultConfiguration对应的类注册为属性。
  2. 扫描带有@FeignClien注解的类,将其注册为FeignClientFactoryBean
1
2
3
4
5
6
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {

registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}

这里关键在FeignClientFactoryBean类,这是一个工厂类,它的getObject()方法里面是真正生成Feign接口代理类的。细节请看注释。

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
@Override
public Object getObject() throws Exception {
return getTarget();
}


<T> T getTarget() {
/*
1.获取FeignContext,相当于是ApplicationContext容器的子容器。
有点类似于Springmvc的那个子容器一样
*/
FeignContext context = this.applicationContext.getBean(FeignContext.class);

/*
2. 从容器里获取Builder,获取的build是经过配置的,下面有详细介绍此方法

*/
Feign.Builder builder = feign(context);

/*
3.这个url就是@FeignClient注解上配置的url
这里有意思的一点就是如果没有配置url只配置了name,那么他就会把name属性当成负载均衡的实例名称,这里底层要使用ribbon来做负载均衡,他就会尝试查找此name对应的实例。
如果url不为空,如url配置的'http://www.baidu.com' 那么他就会以这个值为服务器地址。

*/
if (!StringUtils.hasText(this.url)) {
if (!this.name.startsWith("http")) {
this.url = "http://" + this.name;
}
else {
this.url = this.name;
}
this.url += cleanPath();
//url为空,使用负载均衡方式创建Feign
return (T) loadBalance(builder, context,
new HardCodedTarget<>(this.type, this.name, this.url));
}
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url;
}
String url = this.url + cleanPath();

/*
4. 从容器里获取地城feign.Client,默认就是java原生的那个

*/
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
// not load balancing because we have a url,
// but ribbon is on the classpath, so unwrap
client = ((LoadBalancerFeignClient) client).getDelegate();
}
builder.client(client);
}
/*
5.获取targeter,在targeter里面对实例进行配置

*/
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(this.type, this.name, url));
}



/*
获取Feign.Builder的过程,其实就是从容器里取各种各样的东西,配置进去
熟悉Feign的同学应该能看出来,分别是logger,encoder,decoder,contract
*/
protected Feign.Builder feign(FeignContext context) {
FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
Logger logger = loggerFactory.create(this.type);

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));

//这里面也有一些配置,是根据配置文件或者注解上指定的配置类来配置的
//本文不讲这些
configureFeign(context, builder);

return builder;
}

总结

​ 通过@EnableFeignClients来引入一个ImportBeanDefinitionRegistrar。这个类里面注册了默认配置,扫描了所有的@FeignClient接口,将其注册为FactoryBean,真实使用时将会调用这个FactoryBeangetObject方法,此时才会产生代理类,并且生成代理类需要的各种组件都是从容器里获取的。

3.默认配置

根据@EnableFeignClient注解上的注释,找到了下面这个类org.springframework.cloud.openfeign.FeignClientsConfiguration,这是一个配置类,会被自动扫描到,它里面注入了以下组件。

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

//默认Decoder->SpringDecoder
@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder() {
return new OptionalDecoder(
new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
}

//默认Encoder->SpringEncoder
@Bean
@ConditionalOnMissingBean
@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
public Encoder feignEncoder() {
return new SpringEncoder(this.messageConverters);
}

//如果存在Pageable则用PageableSpringEncoder包装SpringEncoder
@Bean
@ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
@ConditionalOnMissingBean
public Encoder feignEncoderPageable() {
return new PageableSpringEncoder(new SpringEncoder(this.messageConverters));
}

//默认Contract->SpringMvcContract
//在这里spring-cloud重用了springmvc里面的注解,抛弃了openfeign原生注解,可以看下此类的源码
@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
return new SpringMvcContract(this.parameterProcessors, feignConversionService);
}


//默认重试策略为->NEVER_RETRY。因为一般要组合断路器使用,或者底层ribbon的重试,不需要Feign的重试
@Bean
@ConditionalOnMissingBean
public Retryer feignRetryer() {
return Retryer.NEVER_RETRY;
}

//默认Builder,关闭重试
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
public Feign.Builder feignBuilder(Retryer retryer) {
return Feign.builder().retryer(retryer);
}

//默认的logger,没有配置的话默认是Slf4jLogger
@Bean
@ConditionalOnMissingBean(FeignLoggerFactory.class)
public FeignLoggerFactory feignLoggerFactory() {
return new DefaultFeignLoggerFactory(this.logger);
}

//如果启用hystrix,则会使用HystrixFeign
@Configuration
@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();
}

}

总结

上面就是spring-cloudFeign的默认配置了,他们都是以bean的形式放在容器里,我们可以自己提供上面任何组件取而代之,这样我们替换其中任何组件都是非常简单的。

4.默认客户端

​ Feign默认使用的是Java原生的客户端,我们实际使用时一般要用Apache Httpclient 或者okhttp代替,spring-cloud为我们默认提供了这两种框架的自动配置,且是自动选择的。

​ 配置代码在org.springframework.cloud.openfeign.FeignAutoConfiguration里面,看名字就知道这是一个自动装配的配置类。

如果存在HttpClientfeign.ApacheHttpClient,则HttpClient就会生效

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
@Configuration
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnMissingBean(CloseableHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
protected static class HttpClientFeignConfiguration {

private final Timer connectionManagerTimer = new Timer(
"FeignApacheHttpClientConfiguration.connectionManagerTimer", true);

@Autowired(required = false)
private RegistryBuilder registryBuilder;

private CloseableHttpClient httpClient;

//1.创建HttpClientConnectionManager
@Bean
@ConditionalOnMissingBean(HttpClientConnectionManager.class)
public HttpClientConnectionManager connectionManager(
ApacheHttpClientConnectionManagerFactory connectionManagerFactory,
FeignHttpClientProperties httpClientProperties) {

//这里的连接池最大连接数200
//每路由最大连接数50
//timeToLive默认900s
final HttpClientConnectionManager connectionManager = connectionManagerFactory
.newConnectionManager(httpClientProperties.isDisableSslValidation(),
httpClientProperties.getMaxConnections(),
httpClientProperties.getMaxConnectionsPerRoute(),
httpClientProperties.getTimeToLive(),
httpClientProperties.getTimeToLiveUnit(),
this.registryBuilder);

//这里使用Timer类定时关闭连接池内的空闲连接,默认3s一次
this.connectionManagerTimer.schedule(new TimerTask() {
@Override
public void run() {
connectionManager.closeExpiredConnections();
}
}, 30000, httpClientProperties.getConnectionTimerRepeat());
return connectionManager;
}


//2. 创建HttpClient
@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;
}

//3. 创建feign的Client
@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(HttpClient httpClient) {
return new ApacheHttpClient(httpClient);
}

@PreDestroy
public void destroy() throws Exception {
this.connectionManagerTimer.cancel();
if (this.httpClient != null) {
this.httpClient.close();
}
}

}

如果存在okhttp,则会使用下面的配置,这里就不做解释了。

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
@Configuration
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
@ConditionalOnProperty("feign.okhttp.enabled")
protected static class OkHttpFeignConfiguration {

private okhttp3.OkHttpClient okHttpClient;

@Bean
@ConditionalOnMissingBean(ConnectionPool.class)
public ConnectionPool httpClientConnectionPool(
FeignHttpClientProperties httpClientProperties,
OkHttpClientConnectionPoolFactory connectionPoolFactory) {
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;
}

@PreDestroy
public void destroy() {
if (this.okHttpClient != null) {
this.okHttpClient.dispatcher().executorService().shutdown();
this.okHttpClient.connectionPool().evictAll();
}
}

@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(okhttp3.OkHttpClient client) {
return new OkHttpClient(client);
}

}

需要添加如下依赖,Apache Httpclient才会自动配置,注意要和Feign版本一致。

1
2
3
4
5
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>10.4.0</version>
</dependency>

总结

spring-cloud底层是可以根据项目存在的依赖选择使用apache httpclient 或者 okhttp来作为底层实现,而且默认配置都比较合理,几乎不用改动。

​ 需要注意的一点是,项目内只是存在Apache Httpclient的依赖是不够的,还需要添加feign-httpclient的依赖做适配包才可以。okhttp也是同理,都需要一个适配包。

5.其他配置

​ 还有一个配置类也会被自动扫描到,上面缺少的client相关的bean会从这里获取org.springframework.cloud.commons.httpclient.HttpClientConfiguration,以Apache Httpclient为例。他的源码是这样的,他的方法都带有@ConditionalOnMissingBean注解,所以优先级最低。

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
	@Configuration
@ConditionalOnProperty(name = "spring.cloud.httpclientfactories.apache.enabled", matchIfMissing = true)
@ConditionalOnClass(HttpClient.class)
static class ApacheHttpClientConfiguration {

@Bean
@ConditionalOnMissingBean
public ApacheHttpClientConnectionManagerFactory connManFactory() {
return new DefaultApacheHttpClientConnectionManagerFactory();
}

@Bean
@ConditionalOnMissingBean
public HttpClientBuilder apacheHttpClientBuilder() {
return HttpClientBuilder.create();
}

@Bean
@ConditionalOnMissingBean
public ApacheHttpClientFactory apacheHttpClientFactory(
HttpClientBuilder builder) {
return new DefaultApacheHttpClientFactory(builder);
}

}
....
....
....

控制日志等级

​ 配置loggerlevel beanFeignFactory创建时,会查找Logger.Level的实例。

1
2
3
4
5
@Bean
public feign.Logger.Level level() {
return feign.Logger.Level.FULL;
}

FeignClient接口方法上的注解解析过程

​ 代码里我们看到spring-cloud-openfeign没有使用feign提供的Contract,而是自己写了一个SpringMvcContract,所以对接口的解析已经不是原来的思路了,使用的注解也不一样。下面做简单介绍。

首先是解析类上的注解,这里会解析类上RequestMapping的value值作为url属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
if (clz.getInterfaces().length == 0) {
RequestMapping classAnnotation = findMergedAnnotation(clz,
RequestMapping.class);
if (classAnnotation != null) {
// Prepend path from class annotation if specified
if (classAnnotation.value().length > 0) {
String pathValue = emptyToNull(classAnnotation.value()[0]);
pathValue = resolve(pathValue);
if (!pathValue.startsWith("/")) {
pathValue = "/" + pathValue;
}
data.template().uri(pathValue);
}
}
}
}

然后是解析方法上的注解

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
@Override
protected void processAnnotationOnMethod(MethodMetadata data,
Annotation methodAnnotation, Method method) {
//1.方法上只能出现RequestMapping。或者getMapper postmapper这种
if (!RequestMapping.class.isInstance(methodAnnotation) && !methodAnnotation
.annotationType().isAnnotationPresent(RequestMapping.class)) {
return;
}

RequestMapping methodMapping = findMergedAnnotation(method, RequestMapping.class);
// 解析请求method
RequestMethod[] methods = methodMapping.method();
if (methods.length == 0) {
methods = new RequestMethod[] { RequestMethod.GET };
}
checkOne(method, methods, "method");
data.template().method(Request.HttpMethod.valueOf(methods[0].name()));

// 解析path,这里将url append进去,拼接在类上解析的url后面
if (methodMapping.value().length > 0) {
String pathValue = emptyToNull(methodMapping.value()[0]);
if (pathValue != null) {
pathValue = resolve(pathValue);
// Append path from @RequestMapping if value is present on method
if (!pathValue.startsWith("/") && !data.template().path().endsWith("/")) {
pathValue = "/" + pathValue;
}
data.template().uri(pathValue, true);
}
}

// produces
parseProduces(data, method, methodMapping);

// consumes
parseConsumes(data, method, methodMapping);

// headers
parseHeaders(data, method, methodMapping);

data.indexToExpander(new LinkedHashMap<Integer, Param.Expander>());
}

最后是解析参数上的注解

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
@Override
protected boolean processAnnotationsOnParameter(MethodMetadata data,
Annotation[] annotations, int paramIndex) {
boolean isHttpAnnotation = false;

AnnotatedParameterProcessor.AnnotatedParameterContext context = new SimpleAnnotatedParameterContext(
data, paramIndex);
Method method = this.processedMethods.get(data.configKey());
for (Annotation parameterAnnotation : annotations) {
AnnotatedParameterProcessor processor = this.annotatedArgumentProcessors
.get(parameterAnnotation.annotationType());
if (processor != null) {
Annotation processParameterAnnotation;
// synthesize, handling @AliasFor, while falling back to parameter name on
// missing String #value():
processParameterAnnotation = synthesizeWithMethodParameterNameAsFallbackValue(
parameterAnnotation, method, paramIndex);
isHttpAnnotation |= processor.processArgument(context,
processParameterAnnotation, method);
}
}

if (isHttpAnnotation && data.indexToExpander().get(paramIndex) == null) {
TypeDescriptor typeDescriptor = createTypeDescriptor(method, paramIndex);
if (this.conversionService.canConvert(typeDescriptor,
STRING_TYPE_DESCRIPTOR)) {
Param.Expander expander = this.convertingExpanderFactory
.getExpander(typeDescriptor);
if (expander != null) {
data.indexToExpander().put(paramIndex, expander);
}
}
}
return isHttpAnnotation;
}

​ 参数上的解析不是在此类里做的,而是委托给annotatedArgumentProcessors,来做,默认有下面四个解析器,分别可以解析@PathVariable@RequestParam@RequestHeader, @SpringQueryMap注解。这里不展开将这四个解析器的源码了。

1
2
3
4
annotatedArgumentResolvers.add(new PathVariableParameterProcessor());
annotatedArgumentResolvers.add(new RequestParamParameterProcessor());
annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());
annotatedArgumentResolvers.add(new QueryMapParameterProcessor());

总结

​ 上面就是SpringMvcContract解析接口的过程,spring-cloud-openfeign抛弃了原来feign的注解,而是复用了springmvc里面的部分注解,降低了理解成本还是挺好的。

总结

​ 以上就是spring-cloud配置openfeign的过程,读懂源码使用起来才能得心应手。从源码看出我们只要提供好底层依赖,依靠spring-cloud的默认配置就能的到一个不错效果。

​ 其实spring-cloud只使用了openFeign少部分自带功能,很多都是自己定制的如encoder decoder contract。还有一些组件是直接选择了固定的值如retry选择了no_retry,log选择了slf4j-feign等,主要是使用了Feign的思想而不是他的代码。

​ 下篇文章我们讲一讲spring-cloud-Feign负载均衡相关的源码,和spring-cloud-Feign组合断路器相关的源码。


springCloud-openFeign使用原理
https://www.huangchaoyu.com/2775568942.html
作者
hcy
发布于
2020年8月11日
更新于
2024年8月17日
许可协议