Spring Cloud 整合 Feign 的原理

cover.jpeg

前言

上篇 介绍了 Feign 的核心实现原理,在文末也提到了会再介绍其和 Spring Cloud 的整合原理,Spring 具有很强的扩展性,会把一些常用的解决方案通过 starter 的方式开放给开发者使用,在引入官方提供的 starter 后通常只需要添加一些注解即可使用相关功能(通常是 @EnableXXX)。下面就一起来看看 Spring Cloud 到底是如何整合 Feign 的。

整合原理浅析

Spring 中一切都是围绕 Bean 来展开的工作,而所有的 Bean 都是基于 BeanDefinition 来生成的,可以说 BeanDefinition 是整个 Spring 帝国的基石,这个整合的关键也就是要如何生成 Feign 对应的 BeanDefinition

要分析其整合原理,我们首先要从哪里入手呢?如果你看过 上篇 的话,在介绍结合 Spring Cloud 使用方式的例子时,第二步就是要在项目的 XXXApplication 上加添加 @EnableFeignClients 注解,我们可以从这里作为切入点,一步步深入分析其实现原理(通常相当一部分的 starter 一般都是在启动类中添加了开启相关功能的注解)。

feign-1.png

进入 @EnableFeignClients 注解中,其源码如下:

feign-2.png

从注解的源码可以发现,该注解除了定义几个参数(basePackagesdefaultConfigurationclients 等)外,还通过 @Import 引入了 FeignClientsRegistrar 类,一般 @Import 注解有如下功能(具体功能可见 官方 Java Doc):

  • 声明一个 Bean
  • 导入 @Configuration 注解的配置类
  • 导入 ImportSelector 的实现类
  • 导入 ImportBeanDefinitionRegistrar 的实现类(这里使用这个功能

到这里不难看出,整合实现的主要流程就在 FeignClientsRegistrar 类中了,让我们继续深入到类 FeignClientsRegistrar 的源码,

feign-3.png

通过源码可知 FeignClientsRegistrar 实现 ImportBeanDefinitionRegistrar 接口,该接口从名字也不难看出其主要功能就是将所需要初始化的 BeanDefinition 注入到容器中,接口定义两个方法功能都是用来注入给定的 BeanDefinition 的,一个可自定义 beanName(通过实现 BeanNameGenerator 接口自定义生成 beanName 的逻辑),另一个使用默认的规则生成 beanName(类名首字母小写格式)。接口源码如下所示:

feign-4.png

Spring 有一些了解的朋友们都知道,Spring 会在容器启动的过程中根据 BeanDefinition 的属性信息完成对类的初始化,并注入到容器中。所以这里 FeignClientsRegistrar 的终极目标就是将生成的代理类注入到 Spring 容器中。
虽然 FeignClientsRegistrar 这个类的源码看起来比较多,但是从其终结目标来看,我们主要是看如何生成 BeanDefinition 的,通过源码可以发现其实现了 ImportBeanDefinitionRegistrar 接口,并且重写了 registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry) 方法,在这个方法里完成了一些 BeanDefinition 的生成和注册工作。源码如下:

feign-5.png

整个过程主要分为如下两个步骤:

  1. @EnableFeignClients 的全局默认配置(注解的 defaultConfiguration 属性)创建 BeanDefinition 对象并注入到容器中(对应上图中的第 ① 步)
  2. 给标有了 @FeignClient 的类创建 BeanDefinition 对象并注入到容器中(对应上图中的第 ② 步)

下面分别深入方法源码实现来看其具体实现原理,首先来看看第一步的方法 registerDefaultConfiguration(AnnotationMetadata, BeanDefinitionRegistry),源码如下:

feign-6.png

可以看到这里只是获取一下注解 @EnableFeignClients 的默认配置属性 defaultConfiguration 的值,最终的功能实现交给了 registerClientConfiguration(BeanDefinitionRegistry, Object, Object) 方法来完成,继续跟进深入该方法,其源码如下:

feign-7.png

可以看到,全局默认配置的 BeanClazz 都是 FeignClientSpecification,然后这里将全局默认配置 configuration 设置为 BeanDefinition 构造器的输入参数,然后当调用构造器实例化时将这个参数传进去。到这里就已经把 @EnableFeignClients 的全局默认配置(注解的 defaultConfiguration 属性)创建出 BeanDefinition 对象并注入到容器中了,第一步到此完成,整体还是比较简单的。

下面再来看看第二步 给标有了 @FeignClient 的类创建 BeanDefinition 对象并注入到容器中 是如何实现的。深入第二步的方法 registerFeignClients(AnnotationMetadata, BeanDefinitionRegistry) 实现中,由于方法实现代码较多,使用截图会比较分散,所以用贴出源代码并在相关位置添加必要注释的方式进行:

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
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 最终获取到有 @FeignClient 注解类的集合
LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
// 获取 @EnableFeignClients 注解的属性 map
Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
// 获取 @EnableFeignClients 注解的 clients 属性
final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
// 如果 @EnableFeignClients 注解未指定 clients 属性则扫描添加(扫描过滤条件为:标注有 @FeignClient 的类)
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
Set<String> basePackages = getBasePackages(metadata);
for (String basePackage : basePackages) {
candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
}
}
else {
// 如果 @EnableFeignClients 注解已指定 clients 属性,则直接添加,不再扫描(从这里可以看出,为了加快容器启动速度,建议都指定 clients 属性)
for (Class<?> clazz : clients) {
candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
}
}

// 遍历最终获取到的 @FeignClient 注解类的集合
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// verify annotated class is an interface
// 验证带注释的类必须是接口,不是接口则直接抛出异常(大家可以想一想为什么只能是接口?)
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");
// 获取 @FeignClient 注解的属性值
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
// 获取 clientName 的值,也就是在构造器的参数值(具体获取逻辑可以参见 getClientName(Map<String, Object>) 方法
String name = getClientName(attributes);
// 同上文第一步最后调用的方法,注入 @FeignClient 注解的配置对象到容器中
registerClientConfiguration(registry, name, attributes.get("configuration"));
// 注入 @FeignClient 对象,该对象可以在其它类中通过 `@Autowired` 直接引入(e.g. XXXService)
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}

通过源码可以看到最后是通过方法 registerFeignClient(BeanDefinitionRegistry, AnnotationMetadata, Map<String, Object>) 注入的 @FeignClient 对象,继续深入该方法,源码如下:

feign-8.png

方法实现比较长,最终目标是构造出 BeanDefinition 对象,然后通过 BeanDefinitionReaderUtils.registerBeanDefinition(BeanDefinitionHolder, BeanDefinitionRegistry) 注入到容器中。其中关键的一步是从 @FeignClient 注解中获取信息并设置到 BeanDefinitionBuilder 中,BeanDefinitionBuilder 中注册的类是 FeignClientFactoryBean,这个类的功能正如它的名字一样是用来创建出 FeignClientBean 的,然后 Spring 会根据 FeignClientFactoryBean 生成对象并注入到容器中。

需要明确的一点是,实际上这里最终注入到容器当中的是 FeignClientFactoryBean 这个类,Spring 会在类初始化的时候会根据这个类来生成实例对象,就是调用 FeignClientFactoryBean.getObject() 方法,这个生成的对象就是我们实际使用的代理对象。下面再进入到类 FeignClientFactoryBeangetObject() 这个⽅法,源码如下:

feign-9.png

可以看到这个方法是直接调用的类中的另一个方法 getTarget() 的,在继续跟进该方法,由于该方法实现代码较多,使用截图会比较分散,所以用贴出源代码并在相关位置添加必要注释的方式进行:

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
/**
* @param <T> the target type of the Feign client
* @return a {@link Feign} client created with the specified data and the context
* information
*/
<T> T getTarget() {
// 从 Spring 容器中获取 FeignContext Bean
FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
: applicationContext.getBean(FeignContext.class);
// 根据获取到的 FeignContext 构建出 Feign.Builder
Feign.Builder builder = feign(context);

// 注解 @FeignClient 未指定 url 属性
if (!StringUtils.hasText(url)) {
// url 属性是固定访问某一个实例地址,如果未指定协议则拼接 http 请求协议
if (!name.startsWith("http")) {
url = "http://" + name;
}
else {
url = name;
}
// 格式化 url
url += cleanPath();
// 生成代理和我们之前的代理一样,注解 @FeignClient 未指定 url 属性则返回一个带有负载均衡功能的客户端对象
return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url));
}
// 注解 @FeignClient 已指定 url 属性
if (StringUtils.hasText(url) && !url.startsWith("http")) {
url = "http://" + url;
}
String url = this.url + cleanPath();
// 获取一个 client
Client client = getOptional(context, 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 unwrap
// 这里没有负载是因为我们有指定了 url
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
}
builder.client(client);
}
// 生成代理和我们之前的代理一样,最后被注入到 Spring 容器中
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
}

通过源码得知 FeignClientFactoryBean 继承了 FactoryBean,其方法 FactoryBean.getObject 返回的就是 Feign 的代理对象,最后这个代理对象被注入到 Spring 容器中,我们就可以通过 @Autowired 直接注入使用了。同时还可以发现上面的代码分支最终都会走到如下代码:

1
2
Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, target);

点进去深入 targeter.target 的源码,可以看到实际上这里创建的就是一个代理对象,也就是说在容器启动的时候,会为每个 @FeignClient 创建了一个代理对象。至此,Spring CloudFeign 整合原理的核心实现介绍完毕。

总结

本文主要介绍了 Spring Cloud 整合 Feign 的原理。通过上文介绍,你已经知道 Spring 会给标注了 @FeignClient 的接口创建了一个代理对象,那么有了这个代理对象我们就可以做增强处理(e.g. 前置增强、后置增强),那么你知道是如何实现的吗?感兴趣的朋友可以再翻翻源码寻找答案(温馨提示:增强逻辑在 InvocationHandler 中)。还有 FeignRibbonHystrix 等组件的协作,感兴趣的朋友可以自行下载源码学习了解。

-------------本文结束感谢您的阅读-------------
mghio wechat
微信公众号「mghio」
赏作者☕️