Spring Cloud 整合 Feign 的原理
前言
在 上篇 介绍了 Feign 的核心实现原理,在文末也提到了会再介绍其和 Spring Cloud 的整合原理,Spring 具有很强的扩展性,会把一些常用的解决方案通过 starter 的方式开放给开发者使用,在引入官方提供的 starter 后通常只需要添加一些注解即可使用相关功能(通常是 @EnableXXX)。下面就一起来看看 Spring Cloud 到底是如何整合 Feign 的。
整合原理浅析
在 Spring 中一切都是围绕 Bean 来展开的工作,而所有的 Bean 都是基于 BeanDefinition 来生成的,可以说 BeanDefinition 是整个 Spring 帝国的基石,这个整合的关键也就是要如何生成 Feign 对应的 BeanDefinition。
要分析其整合原理,我们首先要从哪里入手呢?如果你看过 上篇 的话,在介绍结合 Spring Cloud 使用方式的例子时,第二步就是要在项目的 XXXApplication 上加添加 @EnableFeignClients 注解,我们可以从这里作为切入点,一步步深入分析其实现原理(通常相当一部分的 starter 一般都是在启动类中添加了开启相关功能的注解)。

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

从注解的源码可以发现,该注解除了定义几个参数(basePackages、defaultConfiguration、clients 等)外,还通过 @Import 引入了 FeignClientsRegistrar 类,一般 @Import 注解有如下功能(具体功能可见 官方 Java Doc):
- 声明一个 Bean
- 导入 @Configuration 注解的配置类
- 导入 ImportSelector 的实现类
- 导入 ImportBeanDefinitionRegistrar 的实现类(这里使用这个功能)
到这里不难看出,整合实现的主要流程就在 FeignClientsRegistrar 类中了,让我们继续深入到类 FeignClientsRegistrar 的源码,

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

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

整个过程主要分为如下两个步骤:
- 给 @EnableFeignClients 的全局默认配置(注解的 defaultConfiguration 属性)创建 BeanDefinition 对象并注入到容器中(对应上图中的第 ① 步)
- 给标有了 @FeignClient 的类创建 BeanDefinition 对象并注入到容器中(对应上图中的第 ② 步)
下面分别深入方法源码实现来看其具体实现原理,首先来看看第一步的方法 registerDefaultConfiguration(AnnotationMetadata, BeanDefinitionRegistry),源码如下:

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

可以看到,全局默认配置的 BeanClazz 都是 FeignClientSpecification,然后这里将全局默认配置 configuration 设置为 BeanDefinition 构造器的输入参数,然后当调用构造器实例化时将这个参数传进去。到这里就已经把 @EnableFeignClients 的全局默认配置(注解的 defaultConfiguration 属性)创建出 BeanDefinition 对象并注入到容器中了,第一步到此完成,整体还是比较简单的。
下面再来看看第二步 给标有了 @FeignClient 的类创建 BeanDefinition 对象并注入到容器中 是如何实现的。深入第二步的方法 registerFeignClients(AnnotationMetadata, BeanDefinitionRegistry) 实现中,由于方法实现代码较多,使用截图会比较分散,所以用贴出源代码并在相关位置添加必要注释的方式进行:
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 对象,继续深入该方法,源码如下:

方法实现比较长,最终目标是构造出 BeanDefinition 对象,然后通过 BeanDefinitionReaderUtils.registerBeanDefinition(BeanDefinitionHolder, BeanDefinitionRegistry) 注入到容器中。其中关键的一步是从 @FeignClient 注解中获取信息并设置到 BeanDefinitionBuilder 中,BeanDefinitionBuilder 中注册的类是 FeignClientFactoryBean,这个类的功能正如它的名字一样是用来创建出 FeignClient 的 Bean 的,然后 Spring 会根据 FeignClientFactoryBean 生成对象并注入到容器中。
需要明确的一点是,实际上这里最终注入到容器当中的是 FeignClientFactoryBean 这个类,Spring 会在类初始化的时候会根据这个类来生成实例对象,就是调用 FeignClientFactoryBean.getObject() 方法,这个生成的对象就是我们实际使用的代理对象。下面再进入到类 FeignClientFactoryBean 的 getObject() 这个⽅法,源码如下:

可以看到这个方法是直接调用的类中的另一个方法 getTarget() 的,在继续跟进该方法,由于该方法实现代码较多,使用截图会比较分散,所以用贴出源代码并在相关位置添加必要注释的方式进行:
/**
* @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 可以直接注入使用了。同时还可以发现上面的代码分支最终都会走到如下代码:
Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, target);
点进去深入 targeter.target 的源码,可以看到实际上这里创建的就是一个代理对象,也就是说在容器启动的时候,会为每个 @FeignClient 创建了一个代理对象。至此,Spring Cloud 和 Feign 整合原理的核心实现介绍完毕。
总结
本文主要介绍了 Spring Cloud 整合 Feign 的原理。通过上文介绍,你已经知道 Srpring 会我们的标注的 @FeignClient 的接口创建了一个代理对象,那么有了这个代理对象我们就可以做增强处理(e.g. 前置增强、后置增强),那么你知道是如何实现的吗?感兴趣的朋友可以再翻翻源码寻找答案(温馨提示:增强逻辑在 InvocationHandler 中)。还有 Feign 与 Ribbon 和 Hystrix 等组件的协作,感兴趣的朋友可以自行下载源码学习了解。
Spring Cloud 整合 Feign 的原理的更多相关文章
- spring cloud 使用feign 遇到问题
spring cloud 使用feign 项目的搭建 在这里就不写了,本文主要讲解在使用过程中遇到的问题以及解决办法 1:示例 @RequestMapping(value = "/gener ...
- spring cloud(四) feign
spring cloud 使用feign进行服务间调用 1. 新建boot工程 pom引入依赖 <dependency> <groupId>org.springframewor ...
- spring cloud关于feign client的调用对象列表参数、设置header参数、多环境动态参数试配
spring cloud关于feign client的调用 1.有些场景接口参数需要传对象列表参数 2.有些场景接口设置设置权限等约定header参数 3.有些场景虽然用的是feign调用,但并不会走 ...
- Spring cloud系列教程第十篇- Spring cloud整合Eureka总结篇
Spring cloud系列教程第十篇- Spring cloud整合Eureka总结篇 本文主要内容: 1:spring cloud整合Eureka总结 本文是由凯哥(凯哥Java:kagejava ...
- Spring Cloud实战 | 第九篇:Spring Cloud整合Spring Security OAuth2认证服务器统一认证自定义异常处理
本文完整代码下载点击 一. 前言 相信了解过我或者看过我之前的系列文章应该多少知道点我写这些文章包括创建 有来商城youlai-mall 这个项目的目的,想给那些真的想提升自己或者迷茫的人(包括自己- ...
- Spring Cloud 整合 nacos 实现动态配置中心
上一篇文章讲解了Spring Cloud 整合 nacos 实现服务注册与发现,nacos除了有服务注册与发现的功能,还有提供动态配置服务的功能.本文主要讲解Spring Cloud 整合nacos实 ...
- 深入理解 Spring Cloud 核心组件与底层原理
一.Spring Cloud核心组件:Eureka Netflix Eureka Eureka详解 1.服务提供者 2.服务消费者 3.服务注册中心 二.Spring Cloud核心组件:Ribbon ...
- Spring Cloud中Feign如何统一设置验证token
代码地址:https://github.com/hbbliyong/springcloud.git 原理是通过每个微服务请求之前都从认证服务获取认证之后的token,然后将token放入到请求头中带过 ...
- spring cloud中feign的使用
我们在进行微服务项目的开发的时候,经常会遇到一个问题,比如A服务是一个针对用户的服务,里面有用户的增删改查的接口和方法,而现在我有一个针对产品的服务B服务中有一个查找用户的需求,这个时候我们可以在B服 ...
随机推荐
- 啥?SynchronousQueue和钟点房一个道理
今天这篇文章,我们继续讲架构师大刘的故事. 大刘有段时间经常会给一些程序员讲课.这一方面是由于团队培训的需要,一方面也是大刘自身想搞搞凡尔赛,嘚瑟一下自身的实力. 大刘讲课是允许公司任何一个人进去听的 ...
- 快速熟悉windows操作
快捷键 win + E : 打开我的电脑 Ctrl+Shift+Esc:打开资源管理器 Alt +F4 :关闭当前窗口 Win + R:打开命令窗口 DOS 命令 打开CMD 的方式 Win+R:输入 ...
- zimbra安装ssl证书
zimbra在后台安装证书签发机构签发证书出现时候出现错误:{RemoteManager: mail.domain.com->zimbra@mail.domain.com:22} com.zim ...
- [刷题] 17 Letter Combinations of a Phone Number
要求 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合 1 不对应任何字母 示例 输入:"23" 输出:["ad", "ae&q ...
- 在/etc/profile下配置java的环境变量
在/etc/profile下配置java的环境变量 原创 Java 作者:xiaoyan5686670 时间:2016-01-18 14:30:28 6152 0 以root用户编辑:#vi /e ...
- shell脚本就是由Shell命令组成的执行文件,将一些命令整合到一个文件中,进行处理业务逻辑,脚本不用编译即可运行。它通过解释器解释运行,所以速度相对来说比较慢。
shell脚本?在说什么是shell脚本之前,先说说什么是shell. shell是外壳的意思,就是操作系统的外壳.我们可以通过shell命令来操作和控制操作系统,比如Linux中的Shell命令就包 ...
- shell基础之case应用
在server0上穿件一个名为/root/script.sh的脚本,让其提供给下列的特性 1.当运行/root/script.sh all,输出为none 2.当运行/root/script.s ...
- 并发王者课-青铜7:顺藤摸瓜-如何从synchronized中的锁认识Monitor
在前面的文章中,我们已经体验过synchronized的用法,并对锁的概念和原理做了简单的介绍.然而,你可能已经察觉到,有一个概念似乎总是和synchronized.锁这两个概念如影相随,很多人也比较 ...
- 第一天:python学习-基础-计算机简史
第一天:计算机简史 1.元始的计算方式 : 1.1:数手指头,结绳记事.符号记事.算筹(祖冲之-圆周率).算盘:计算效率较低. 1.2:15世纪航海活动各天文需求 计算比较繁重,出现计算尺工具,奥特雷 ...
- 桥接PyTorch和TVM
桥接PyTorch和TVM 人工智能最引人入胜的一些应用是自然语言处理.像BERT或GPT-2之类的模型及其变体,可以获住足够多的文本信息. 这些模型属于称为Transformers的神经网络类体系结 ...