前言
在 上篇 介绍了 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 等组件的合作,感兴趣的敌人能够自行下载源码学习理解。
感激各位看官的点赞、珍藏和评论,咱们下周见!文章继续更新,微信搜一搜「mghio」关注这个“Java 搬运工 & 一生学习者”,一起牛逼~。