Spring 中 @Primary 注解的原理是什么?

1. 问题分析

当我们使用 Spring 的时候,有时候会遇到下面这种情况。

假设我有 A、B 两个类,在 A 中注入 B,如下:

@Component
public class A {@AutowiredB b;
}

至于 B,则在配置类中存在多个实例:

@Configuration
@ComponentScan
public class JavaConfig {@Bean("b1")B b1() {return new B();}@Bean("b2")B b2() {return new B();}
}

这样的项目启动之后,必然会抛出如下异常:

当然,对于这样的问题,相信有经验的同学都知道该怎么解决:

  1. 可以使用 @Resource 注解,使用该注解时指定具体的 Bean 名称即可。
  2. 在 @Autowired 注解之上,再多加一个 @Qualifier(“b1”) 注解,通过该注解去指定要加载的 Bean 名称。
@Component
public class A {@Autowired@Qualifier("b1")B b;
}
  1. 在多个 B 对象的某一个之上,添加 @Primary 注解,表示当存在重复的 B 对象时,优先使用哪一个。
@Configuration
@ComponentScan
public class JavaConfig {@Bean("b1")@PrimaryB b1() {return new B();}@Bean("b2")B b2() {return new B();}
}

除了这三个,还有没有其他办法呢?必须有!!!在 Spring 中 @Qualifier 注解还能这么用? 一文中,松哥还和大家扩展了 @Qualifier 注解的其他用法,感兴趣的小伙伴不要错过哦。

这里三个方法,其中 @Resource 是 JSR 中提供的注解,我这里先不展开,松哥后面专门再来和大家聊 @Resource 注解的注入原理。今天我主要是想和小伙伴们分享一下后面两种方案的实现原理。

2. 源码解析

本文基于前面@Autowired 到底是怎么把变量注入进来的?一文展开,所以如果还没看过改文章的小伙伴,建议先去阅读一下,这有助于更好的理解本文。

2.1 doResolveDependency

在@Autowired 到底是怎么把变量注入进来的?的 3.3 小节中,我们提到,给 A 注入 B 的时候,会调用到 doResolveDependency 方法,我们再来看下该方法:

DefaultListableBeanFactory#doResolveDependency:

@Nullable
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {//...Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);if (matchingBeans.isEmpty()) {if (isRequired(descriptor)) {raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);}return null;}if (matchingBeans.size() > 1) {autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);if (autowiredBeanName == null) {if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {return descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans);}else {// In case of an optional Collection/Map, silently ignore a non-unique case:// possibly it was meant to be an empty collection of multiple regular beans// (before 4.3 in particular when we didn't even look for collection beans).return null;}}instanceCandidate = matchingBeans.get(autowiredBeanName);//...
}

在这个方法中,首先调用了 findAutowireCandidates 方法去找到所有满足条件的 Class。Map 中的 key 就是 Bean 的名称,value 则是一个 Class,此时还没有实例化。

如果我们是通过 @Qualifier 注解来解决问题的,那么问题就在 findAutowireCandidates 方法中被解决了。这个在前面的文章 Spring 中 @Qualifier 注解还能这么用? 中已经和小伙伴们聊过了。

如果 @Qualifier 注解没把问题解决掉,就会导致最终查询到的 matchingBeans 的数量大于 1,那么就会进入到接下来的 if 环节中,通过 determineAutowireCandidate 方法进一步确定到底使用哪一个 Bean,@Primary 注解的处理,就在该方法中完成。

2.2 determineAutowireCandidate

DefaultListableBeanFactory#determineAutowireCandidate

@Nullable
protected String determineAutowireCandidate(Map<String, Object> candidates, DependencyDescriptor descriptor) {Class<?> requiredType = descriptor.getDependencyType();String primaryCandidate = determinePrimaryCandidate(candidates, requiredType);if (primaryCandidate != null) {return primaryCandidate;}String priorityCandidate = determineHighestPriorityCandidate(candidates, requiredType);if (priorityCandidate != null) {return priorityCandidate;}// Fallbackfor (Map.Entry<String, Object> entry : candidates.entrySet()) {String candidateName = entry.getKey();Object beanInstance = entry.getValue();if ((beanInstance != null && this.resolvableDependencies.containsValue(beanInstance)) ||matchesBeanName(candidateName, descriptor.getDependencyName())) {return candidateName;}}return null;
}

这个方法里边一共做了三种尝试:

  1. 第一个尝试就是调用 determinePrimaryCandidate 方法去确定最佳候选 Bean,这个方法本质上就是通过 @Primary 注解找到最佳 BeanName。
  2. 如果第一步没有找到最佳 BeanName,那么接下来会调用 determineHighestPriorityCandidate 方法去查找最佳 Bean,该方法本质上是通过查找 JSR-330 中的 @Priority 注解,来确定 Bean 的优先级。
  3. 如果前两步都没找到合适的 BeanName,那么接下来这个 for 循环则是通过 Bean 的名称进行匹配了,即 A 类中变量的名称和目标 Bean 的名称是否匹配,如果能匹配上,那也可以。这也就是我么常说的 @Autowired 注解先按照类型去匹配,如果类型匹配不上,就会按照名称去匹配。

上面大致介绍了这个方法的执行思路,接下来我们就来看一下执行细节。

2.2.1 determinePrimaryCandidate

@Nullable
protected String determinePrimaryCandidate(Map<String, Object> candidates, Class<?> requiredType) {String primaryBeanName = null;for (Map.Entry<String, Object> entry : candidates.entrySet()) {String candidateBeanName = entry.getKey();Object beanInstance = entry.getValue();if (isPrimary(candidateBeanName, beanInstance)) {if (primaryBeanName != null) {boolean candidateLocal = containsBeanDefinition(candidateBeanName);boolean primaryLocal = containsBeanDefinition(primaryBeanName);if (candidateLocal && primaryLocal) {throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(),"more than one 'primary' bean found among candidates: " + candidates.keySet());}else if (candidateLocal) {primaryBeanName = candidateBeanName;}}else {primaryBeanName = candidateBeanName;}}}return primaryBeanName;
}
protected boolean isPrimary(String beanName, Object beanInstance) {String transformedBeanName = transformedBeanName(beanName);if (containsBeanDefinition(transformedBeanName)) {return getMergedLocalBeanDefinition(transformedBeanName).isPrimary();}return (getParentBeanFactory() instanceof DefaultListableBeanFactory parent &&parent.isPrimary(transformedBeanName, beanInstance));
}

我们来看下这个方法的执行逻辑。

参数 candidates 中保存了所有符合条件的 BeanDefinition,参数 key 就是 Bean 的名称,Value 则是对应的 BeanDefinition。现在就去遍历 candidates,在遍历的时候,调用 isPrimary 方法去判断这个 BeanDefinition 上是否含有 @Primary 注解,isPrimary 方法的逻辑比较简单,我就不啰嗦了,该方法中涉及到 getMergedLocalBeanDefinition 方法去父容器中查找两个细节,这个松哥在之前的文章中也都和大家聊过了(Spring BeanDefinition:父子关系解密、Spring 中的父子容器是咋回事?)。

在查找的过程中,如果有满足条件的 BeanName,则赋值给 primaryBeanName 变量然后返回,如果存在多个满足条件的 BeanName,那就抛出 NoUniqueBeanDefinitionException 异常。

2.2.2 determineHighestPriorityCandidate

要理解 determineHighestPriorityCandidate 方法,得先了解 @Priority 注解的用法。考虑到有的小伙伴可能还不熟悉 @Priority 注解,我这里也跟大家稍微说两句。

@Priority 注解作用有点类似于 @Order,可以用来指定一个 Bean 的优先级,这是 JSR 中提供的注解,所以如果想使用这个注解,需要先添加依赖:

<dependency><groupId>jakarta.annotation</groupId><artifactId>jakarta.annotation-api</artifactId><version>2.1.1</version>
</dependency>

然后在类上添加该注解,像下面这样:

public interface IBService {
}
@Component
@Priority(100)
public class BServiceImpl1 implements IBService{
}
@Component
@Priority(101)
public class BServiceImpl2 implements IBService{
}

@Priority 注解中的数字表示优先级,数字越大优先级越小。将来在 A 中注入 IBService 时,就会优先查找优先级高的 Bean。虽然 @Priority 注解可以加在类上,也可以加在方法上,但是在具体实践中,加在方法上这个注解并不会生效,只能加在类上面。至于原因,大家看完接下来的源码分析就懂了。

现在我们再来看下 determineHighestPriorityCandidate 方法:

@Nullable
protected String determineHighestPriorityCandidate(Map<String, Object> candidates, Class<?> requiredType) {String highestPriorityBeanName = null;Integer highestPriority = null;for (Map.Entry<String, Object> entry : candidates.entrySet()) {String candidateBeanName = entry.getKey();Object beanInstance = entry.getValue();if (beanInstance != null) {Integer candidatePriority = getPriority(beanInstance);if (candidatePriority != null) {if (highestPriorityBeanName != null) {if (candidatePriority.equals(highestPriority)) {throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(),"Multiple beans found with the same priority ('" + highestPriority +"') among candidates: " + candidates.keySet());}else if (candidatePriority < highestPriority) {highestPriorityBeanName = candidateBeanName;highestPriority = candidatePriority;}}else {highestPriorityBeanName = candidateBeanName;highestPriority = candidatePriority;}}}}return highestPriorityBeanName;
}

determineHighestPriorityCandidate 方法的整体处理思路跟 determinePrimaryCandidate 方法特别像,不同的是 determinePrimaryCandidate 方法处理的是 @Primary 注解,而 determineHighestPriorityCandidate 方法处理的是 @Priority 注解。

determineHighestPriorityCandidate 方法也是遍历 candidates,然后调用 getPriority 方法获取到具体的优先级的值。然后根据这个具体的数字选定一个合适的 beanName 返回,如果存在多个优先级相同的 bean,那么就会抛出 NoUniqueBeanDefinitionException 异常。

最后再来看下 getPriority 方法,几经辗转之后,该方法会调用到 AnnotationAwareOrderComparator#getPriority 方法:

@Override
@Nullable
public Integer getPriority(Object obj) {if (obj instanceof Class<?> clazz) {return OrderUtils.getPriority(clazz);}Integer priority = OrderUtils.getPriority(obj.getClass());if (priority == null  && obj instanceof DecoratingProxy decoratingProxy) {return getPriority(decoratingProxy.getDecoratedClass());}return priority;
}

可以看到,这里最终就是调用 OrderUtils.getPriority 方法去查找参数 clazz 上的 @Priority 注解,并找到注解上对应的值返回。OrderUtils.getPriority 在执行的时候,参数时 clazz,即只会查找 clazz 上的 注解,并不会查找方法上的注解,因此前面我说 @Priority 注解要加在类上才有效。

2.2.3 按名称匹配

最后我们再来看下按照名字去匹配的逻辑:

// Fallback
for (Map.Entry<String, Object> entry : candidates.entrySet()) {String candidateName = entry.getKey();Object beanInstance = entry.getValue();if ((beanInstance != null && this.resolvableDependencies.containsValue(beanInstance)) ||matchesBeanName(candidateName, descriptor.getDependencyName())) {return candidateName;}
}
protected boolean matchesBeanName(String beanName, @Nullable String candidateName) {return (candidateName != null &&(candidateName.equals(beanName) || ObjectUtils.containsElement(getAliases(beanName), candidateName)));
}

可以看到,这里也是遍历 candidates 集合,然后调用 matchesBeanName 方法,在该方法中,会去判断候选的 BeanName 和需要注入的变量名(descriptor.getDependencyName())是否相等,如果相等,就直接返回即可。即下面这种代码不需要额外的注解是可以运行不会报错的:

@Component
public class AService {@AutowiredB b1;}
@Configuration
@ComponentScan
public class JavaConfig {@Beanpublic B b1() {return new B();}@BeanB b2() {return new B();}}

3. 小结

好啦,经过上面的分析,现在小伙伴们明白了 @Primary 注解的完整处理逻辑了吧~本文结合@Autowired 到底是怎么把变量注入进来的? 和 Spring 中 @Qualifier 注解还能这么用? 一起食用效果更好哦!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/84578.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【python】jupyter notebook导出pdf和pdf不显示中文问题

文章目录 写在前面1. 使用jupyter notebook导出pdf1.1 安装Pandoc1.2 安装MiKTex1.3 示例导出pdf 2. 中文显示问题2.1 显示中文问题示例2.2 解决办法1&#xff1a;修改tex2.3 解决办法2&#xff1a;修改内置文件 写在前面 使用jupyter notebook导出pdf时&#xff0c;出现了一些…

一文1500字从0到1搭建 Jenkins 自动化测试平台

Jenkins 自动化测试平台的作用 自动化构建平台的执行流程&#xff08;目标&#xff09;是&#xff1a; 我们将代码提交到代码托管工具上&#xff0c;如github、gitlab、gitee等。 1、Jenkins要能够检测到我们的提交。 2、Jenkins检测到提交后&#xff0c;要自动拉取代码&#x…

开源与区块链:去中心化的未来

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

git通过fork-merge request实现多人协同

一、问题 对于一个项目&#xff0c;如果需要多人协同开发&#xff0c;大家都在原始仓库中进行修改提交&#xff0c;经常会发生冲突&#xff0c;而且一不小心会把别人的代码内容覆盖掉。为了避免这样的问题&#xff0c;git提供了fork-merge request这样的协同方式。 二、仓库框…

深度学习4. 循环神经网络 – Recurrent Neural Network | RNN

目录 循环神经网络 – Recurrent Neural Network | RNN 为什么需要 RNN &#xff1f;独特价值是什么&#xff1f; RNN 的基本原理 RNN 的优化算法 RNN 到 LSTM – 长短期记忆网络 从 LSTM 到 GRU RNN 的应用和使用场景 总结 百度百科维基百科 循环神经网络 – Recurre…

Django(3)-创建第一个数据模型-ORM映射

数据库配置 根目录下settings.py 。这是个包含了 Django 项目设置的 Python 模块。 通常&#xff0c;这个配置文件使用 SQLite 作为默认数据库。如果你不熟悉数据库&#xff0c;或者只是想尝试下 Django&#xff0c;这是最简单的选择。Python 内置 SQLite&#xff0c;所以你无…

糟改押ong韵诗词,末三字改qiao ben zhong

题目给出诗词行的汉语拼音&#xff0c;糟改诗词押ong诗词行末三字“敲笨钟“。 (本笔记适合初通 Python 的 coder 翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Free&#xff1a;大咖免费“圣经”教程《 python 完全自学教程》&#xff…

websocket和uni-app里使用websocket

一、HTTP是无状态协议 特点&#xff1a; 1、浏览器发送请求时&#xff0c;浏览器和服务器会建立一个连接。完成请求和响应。在http1.0之前&#xff0c;每次请求响应完毕后&#xff0c;会立即断开连接。在http1.1之后&#xff0c;当前网页的所有请求响应完毕后&#xff0c;才断…

springboot+mp完成简单案例

目录 1.框架搭建 2.前端搭建 3.后端编写 需求&#xff1a;完成简单的连表条件查询以及添加即可 1.框架搭建 1.创建springboot项目 2.相关依赖 <!--web依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boo…

数据库——Redis 没有使用多线程?为什么不使用多线程?

文章目录 Redis6.0 之后为何引入了多线程&#xff1f; 虽然说 Redis 是单线程模型&#xff0c;但是&#xff0c; 实际上&#xff0c;Redis 在 4.0 之后的版本中就已经加入了对多线程的支持。 不过&#xff0c;Redis 4.0 增加的多线程主要是针对一些大键值对的删除操作的命令&a…

00-音视频-概述

有很多场合会使用的音视频&#xff0c;比如安防、视频闸机、影音播放器、视频通话&#xff0c;短视频等等。 从摄像头采集到用户观看&#xff0c;这中间涉及到了很多技术。 用户一般观看的高清视频1080P30帧。若按24位RGB对视频进行存储&#xff0c;一个60分钟视频所占空间 …

大语言模型微调实践——LoRA 微调细节

1. 引言 近年来人工智能领域不断进步&#xff0c;大语言模型的崛起引领了自然语言处理的革命。这些参数量巨大的预训练模型&#xff0c;凭借其在大规模数据上学习到的丰富语言表示&#xff0c;为我们带来了前所未有的文本理解和生成能力。然而&#xff0c;要使这些通用模型在特…