Feign源码解析系列-注册套路
感谢不知名朋友的打赏,感谢你的支持!
开始
在追寻Feign源码的过程中发现了一些套路,既然是套路,就可以举一反三,所以值得关注。
这篇会详细解析Feign Client配置和初始化的方式,这些方式大多依赖Spring的游戏规则,在和Spring相关的各个组件中都可以看到类似的玩法,都是可以举一反三。所以熟悉这些套路大有益处。
内容
在上一篇中,我们提到了注解FeignClient引入了FeignClientsRegistrar,它继承ImportBeanDefinitionRegistrar。
在Spring中,使用ImportBeanDefinitionRegistrar动态组装注册BeanDefinition,就是套路之一,像FeignClientsRegistrar一样的类还有很多,比如:org.springframework.cloud.netflix.ribbon.RibbonClientConfigurationRegistrar,org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesRegistrar
FeignClientsRegistrar实现ImportBeanDefinitionRegistrar的registerBeanDefinitions方法:
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}
从入口代码调用的两个方法看,从方法名上也可以看出来,要做的事可以分为两个:
1,注册@EnableFeignClients中定义defaultConfiguration属性下的类,包装成FeignClientSpecification,注册到Spring容器。
private void registerDefaultConfiguration(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
Map<String, Object> defaultAttrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
String name;
// 注解只用的类进行判断是否为内部类或者方法内的本地类
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
}
else {
name = "default." + metadata.getClassName();
}
registerClientConfiguration(registry, name,
defaultAttrs.get("defaultConfiguration"));
}
}
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
Object configuration) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
registry.registerBeanDefinition(
name + "." + FeignClientSpecification.class.getSimpleName(),
builder.getBeanDefinition());
}
registerClientConfiguration方法中,使用FeignClientSpecification生成BeanDefinitionBuilder,放入构造函数的两个参数,然后构造了bean注册的名称。
这里的名称是类似这样的:default.xxx.TestApplication.FeignClientSpecification。
假如你还记得在@FeignClient中有一个属性:configuration,这个属性是表示各个FeignClient自定义的配置类,后面也会通过调用registerClientConfiguration方法来注册成FeignClientSpecification到容器。
所以,这里可以完全理解在@EnableFeignClients中配置的是做为兜底的配置,在各个@FeignClient配置的就是自定义的情况。
2,对于每个@FeignClient进行解析,并将他们注册到spring容器。
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
Set<String> basePackages;
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = getBasePackages(metadata);
}
else {
final Set<String> clientClasses = new HashSet<>();
basePackages = new HashSet<>();
for (Class<?> clazz : clients) {
basePackages.add(ClassUtils.getPackageName(clazz));
clientClasses.add(clazz.getCanonicalName());
}
AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
@Override
protected boolean match(ClassMetadata metadata) {
String cleaned = metadata.getClassName().replaceAll("\\$", ".");
return clientClasses.contains(cleaned);
}
};
scanner.addIncludeFilter(
new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
}
for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
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");
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
registerClientConfiguration(registry, name, attributes.get(“configuration"));
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
我们知道@FeignClient的扫描路径在@EnableFeignClients上是可以通过basePackages,basePackageClasses,client这三个参数进行配置的。所以在扫描@FeignClient之前就都是这个逻辑。确认好basePackages后,就遍历basePackages,利用扫描器扫出各个路径下的@FeignClient注解。
特别注意,@FeignClient是需要在定义扫描位置才能被解析的,如果你的feign客户端接口不在扫描范围是不会被入住到容器中,从而无法被使用。而且没有入口可以更改配置的扫描路径,在实际开发中需要注意。
这个扫描器ClassPathScanningCandidateComponentProvider又是spring的套路,通过配置的filters,找出需要的结果类。这个能力在自定义注解+扫描路径可配置的场景非常合适。
在确认好@FeignClient注解的是否为接口后,最后会解析配置,先调用registerClientConfiguration方法,后调用registerFeignClient方法。
registerClientConfiguration方法,前面已经提到过,这里会对每个FeignClient都进行调用,所以会把@FeignClient上配置的configuration包装成FeignClientSpecification到容器中。
registerFeignClient方法把FeignClientFactoryBean注入到容器,FeignClientFactoryBean用于生产FeignClient,后续详细。
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
validate(attributes);
definition.addPropertyValue("url", getUrl(attributes));
definition.addPropertyValue("path", getPath(attributes));
String name = getName(attributes);
definition.addPropertyValue("name", name);
definition.addPropertyValue("type", className);
definition.addPropertyValue("decode404", attributes.get("decode404"));
definition.addPropertyValue("fallback", attributes.get("fallback"));
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
String alias = name + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null
beanDefinition.setPrimary(primary);
String qualifier = getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
FeignClientSpecification继承NamedContextFactory.Specification,而NamedContextFactory用与创建子上下文将NamedContextFactory.Specification放入其中。
NamedContextFactory中维护一个context的map,value是AnnotationConfigApplicationContext即子上下文。
在feign中定义了一个FeignContext继承NamedContextFactory,来统一维护feign中各个feign客户端相互隔离的上下文。
类相互依赖图:

FeignContext的代码:
public class FeignContext extends <FeignClientSpecification> {
public FeignContext() {
super(FeignClientsConfiguration.class, "feign", "feign.client.name");
}
}
NamedContextFactory中的defaultConfigType被设置为FeignClientsConfiguration。
这里我们先看一下NamedContextFactory中的createContext方法的实现:
protected AnnotationConfigApplicationContext createContext(String name) {
// 每次调用new一个AnnotationConfigApplicationContext
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
//在子上下文上就注册name对应的configuration
if (this.configurations.containsKey(name)) {
for (Class<?> configuration : this.configurations.get(name)
.getConfiguration()) {
context.register(configuration);
}
}
//注册default configuration
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
if (entry.getKey().startsWith("default.")) {
for (Class<?> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
// 将this.defaultConfigType即FeignClientsConfiguration也注册上
context.register(PropertyPlaceholderAutoConfiguration.class,
this.defaultConfigType);
context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
this.propertySourceName,
Collections.<String, Object> singletonMap(this.propertyName, name)));
// 父上下文设置,所有的子上下文都是一个父上下文,当子上下文找不到时,就去父上下文找
if (this.parent != null) {
// Uses Environment from parent as well as beans
context.setParent(this.parent);
}
context.refresh();
return context;
}
FeignContext注册到容器是在FeignAutoConfiguration上完成的:
@Autowired(required = false)
private List<FeignClientSpecification> configurations = new ArrayList<>();
@Bean
public FeignContext feignContext() {
FeignContext context = new FeignContext();
context.setConfigurations(this.configurations);
return context;
}
在初始化FeignContext时,会把configurations在容器中放入FeignContext中。configurations的来源就是在前面registerFeignClients方法中将@FeignClient的配置configuration。
结束
关键需要理解的是在feign中为每一个client准备了FeignContext,内部维护这个自定义配置的内容比如Encoder,Decoder等,从而实现对每个client自定义能力。
Feign源码解析系列-注册套路的更多相关文章
- Feign源码解析系列-那些注解们
开始 Feign在Spring Cloud体系中被整合进来作为web service客户端,使用HTTP请求远程服务时能就像调用本地方法,可见在未来一段时间内,大多数Spring Cloud架构的微服 ...
- Feign源码解析系列-最佳实践
前几篇准备写完feign的源码,这篇直接给出Feign的最佳实践,考虑到目前网上还没有一个比较好的实践解释,对于新使用spring cloud的同学会对微服务之间的依赖产生一些迷惑,也会走一些弯路.这 ...
- Feign源码解析系列-核心初始化
开始 初始化Feign客户端当然是整个过程中的核心部分,毕竟初始化完毕就等着调用了,初始化时候准备的什么,流程就走什么. 内容 从上一篇中,我们已经知道,对于扫描到的每一个有@FeignClient, ...
- Spring源码解析系列汇总
相信我,你会收藏这篇文章的 本篇文章是这段时间撸出来的Spring源码解析系列文章的汇总,总共包含以下专题.喜欢的同学可以收藏起来以备不时之需 SpringIOC源码解析(上) 本篇文章搭建了IOC源 ...
- 【原】Android热更新开源项目Tinker源码解析系列之三:so热更新
本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源文件热更新 A ...
- 【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新
[原]Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代. Tinker github地址:http ...
- 【原】Android热更新开源项目Tinker源码解析系列之二:资源文件热更新
上一篇文章介绍了Dex文件的热更新流程,本文将会分析Tinker中对资源文件的热更新流程. 同Dex,资源文件的热更新同样包括三个部分:资源补丁生成,资源补丁合成及资源补丁加载. 本系列将从以下三个方 ...
- Cwinux源码解析系列
Cwinux源码解析系列
- 【安卓网络请求开源框架Volley源码解析系列】定制自己的Request请求及Volley框架源码剖析
通过前面的学习我们已经掌握了Volley的基本用法,没看过的建议大家先去阅读我的博文[安卓网络请求开源框架Volley源码解析系列]初识Volley及其基本用法.如StringRequest用来请求一 ...
随机推荐
- div+css的常规使用
代码如下 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3 ...
- 进到页面后input输入框自动获取焦点
<html> <head></head> <body> 用户名:<input type="text" ...
- springmvc核心技术
目录 异常处理 类型转换器 数据验证 文件上传与下载 拦截器 异常处理 Spring MVC中, 系统的DAO, Service, Controller层出现异常, 均通过throw Exceptio ...
- LeetCode 15. 3Sum 16. 3Sum Closest 18. 4Sum
n数求和,固定n-2个数,最后两个数在连续区间内一左一右根据当前求和与目标值比较移动,如果sum<target,移动较小数,否则,移动较大数 重复数处理: 使i为左至右第一个不重复数:while ...
- CentOS-7-1804下MySQL安装及防火墙设置
第一步,下载MySQL Linux 版本安装包,这里使用 这个版本. 第二步,上传安装包到Linux系统中. 第三步,解压安装包 tar -zxvf mysql--linux-glibc2.-x86_ ...
- TreeSet VS HashSet VS LinkedHashSet; TreeMap VS HashMap VS LinkedHashMap
From online resources Set HashSet is much faster than TreeSet (constant-time versus log-time for mos ...
- Python filter() 函数
Python filter() 函数 描述 filter() 函数用于过滤序列,过滤掉不符合条件的元素,返回由符合条件元素组成的新列表. 该接收两个参数,第一个为函数,第二个为序列,序列的每个元素作为 ...
- form表单提交到Servlet后,弹出对话框,然后在跳转页面
在Servlet中添加一下代码即可 out.print("<script>alert('添加成功!');window.location='index.jsp';</scri ...
- 三元运算和bytes数据类型笔记
三元运算 result = 值1 if条件 else 值2 如果条件为真:result = 值1 如果条件为假:result = 值2 bytes数据类型 字符串可以编码成字节包,字节包也可以解码成 ...
- Main Steps to Setup an ODI data sync
0. Get ODI installed 1. Topo physical Architecture/new physical schema 2. New Logical schema 3. New ...