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

news2024/9/29 7:30:14

1. 问题分析

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

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

@Component
public class A {
    @Autowired
    B 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")
    @Primary
    B 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;
	}
	// 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;
		}
	}
	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 {
    @Autowired
    B b1;

}
@Configuration
@ComponentScan
public class JavaConfig {

    @Bean
    public B b1() {
        return new B();
    }

    @Bean
    B b2() {
        return new B();
    }

}

3. 小结

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

windows上先简单使用libevent,运行demo

使用libevent 1:获取libevent库。 从libevent 官网中获取&#xff0c;这里获取到的版本是libevent-2.1.12-stable.tar 2:windows上编译libevent库&#xff08;VS2019上使用&#xff09; 2.1&#xff1a;cmake准备 注&#xff1a;这里要用cmake进行编译&#xff0c;除此之外…

变动的Python爬虫实现

在电商时代&#xff0c;了解商品价格的变动对于购物者和卖家来说都非常重要。本文将分享一种基于Python的实时监控电商平台商品价格变动的爬虫实现方法。通过本文的解决方案和代码示例&#xff0c;您将能够轻松监控商品价格&#xff0c;并及时做出决策。 一、了解需求和目标 在…

HyperMotion高度自动化云迁移至华为HCS8.1解决方案

项目背景 2020 年以来&#xff0c;金融证券已经成为信创落地最快的领域。2021 年证监会发布的《证券期货业科技发展十四五规划》中&#xff0c;将“加强信创规划与实施”作为证券行业重点建设任务之一。为了符合国家信创标准&#xff0c;某证券企业计划将网管系统、呼叫中心管…

操作系统——Linux基本命令

文章目录 1.目录切换命令2. 目录的操作命令(增删改查)3. 文件的操作命令(增删改查)4. 压缩文件的操作命令5. Linux 的权限命令6. Linux 用户管理7. Linux 系统用户组的管理8. 其他常用命令 Linux 命令大全&#xff1a;http://man.linuxde.net/ 1.目录切换命令 cd usr&#xf…

《TCP IP网络编程》第十七章

第 17 章 优于 select 的 epoll 17.1 epoll 理解及应用 select 复用方法由来已久&#xff0c;因此&#xff0c;利用该技术后&#xff0c;无论如何优化程序性能也无法同时介入上百个客户端。这种 select 方式并不适合以 web 服务器端开发为主流的现代开发环境&#xff0c;所以需…

RCNA——静态路由配置

静态路由的一堆特点就不多说了&#xff0c;之前文章有提到过。 一&#xff0c;实验背景 公司扔给你几台路由器&#xff0c;并和你说&#xff0c;我们公司需要一条通往公司外的网络&#xff0c;你帮忙配置一下。这类网络很显然要利用VPN等技术&#xff0c;奈何公司穷的一批&…

[语音识别] 基于Python构建简易的音频录制与语音识别应用

语音识别技术的快速发展为实现更多智能化应用提供了无限可能。本文旨在介绍一个基于Python实现的简易音频录制与语音识别应用。文章简要介绍相关技术的应用&#xff0c;重点放在音频录制方面&#xff0c;而语音识别则关注于调用相关的语音识别库。本文将首先概述一些音频基础概…

【Rust】Rust学习 第十六章Rust 的面向对象特性

面向对象编程&#xff08;Object-Oriented Programming&#xff0c;OOP&#xff09;是一种模式化编程方式。对象&#xff08;Object&#xff09;来源于 20 世纪 60 年代的 Simula 编程语言。这些对象影响了 Alan Kay 的编程架构中对象之间的消息传递。他在 1967 年创造了 面向对…

【Unity3D】程序纹理简单应用

1 几何纹理应用 1.1 边框 1&#xff09;边框子图 Border.shadersubgraph 说明&#xff1a;Any 节点用于判断输入向量中是否存在一个分量非零&#xff0c;Branch 节点根据输入的真假走不同的分支&#xff0c;详见→Shader Graph节点。 2&#xff09;圆环 Ring.shadergraph 说明…

Web 拦截器-interceptor

拦截器是一种动态拦截方法调用的机制&#xff0c;类似于过滤器&#xff0c;是Spring框架提出的&#xff0c;用来动态拦截控制器方法的执行。 其作用是拦截请求&#xff0c;在指定方法调用前后&#xff0c;根据业务执行预设代码。 实现步骤 1.定义拦截器&#xff0c;实现Handl…

欧拉计划44题

Pentagon numbers Pentagonal numbers are generated by the formula, . The first ten pentagonal numbers are: 1,5,12,22,35,51,70,92,117,145,… It can be seen that . However, their difference, 70−2248, is not pentagonal. Find the pair of pentagonal numbers, a…

室颤(VF)检测的算法 TCSC

室颤检测检测算法很多&#xff0c;最早关注的是TCI 算法。看看原始论文的 TCI 参数分布图&#xff1a; 分布图上显示&#xff0c;VFVT 和 NSR 分离特性很好。但是这有一个前提&#xff0c;和选择的数据集有很大关系。 在看看下面一篇论文&#xff1a; A simple time domain al…

Java-图书登录系统的实现

实现效果 它将面对 管理员 和 普通用户 两种用户来提供服务&#xff0c;并且各自的服务并不相同。 实现思路 一般写项目&#xff0c;每个独立的功能都会写成一个类&#xff0c;而有关联的功能&#xff0c;都会将多个类存放在一个包中&#xff0c;此项目我们将用 3 个包来体现我…

023:vue中解决el-date-picker更改样式不生效问题

第023个 查看专栏目录: VUE ------ element UI 本文章目录 修改后的效果示例源代码&#xff08;共52行&#xff09;核心内容步骤&#xff1a;&#xff08;1&#xff09;更改样式&#xff08;2&#xff09;添加参数 专栏目标 在vue项目开发中&#xff0c;我们打算保持颜色的一致…

辅助笔记-安装CentOS8.1虚拟机

安装CentOS8.1虚拟机 文章目录 安装CentOS8.1虚拟机1. CentOS8.1的安装1.1 下载CentOS8.1镜像1.2 检查BIOS虚拟化支持1.3 新建虚拟机1.4 安装系统1.5 测试上网和终端 2. CentOS8.0和CentOS7.0的区别(了解) 本文主要参考B站视频“P116_ 韩顺平Linux_cntos8安装和介绍”。 本文目…

小红书kol投放怎么做,kol投放工作规划!

作为分享类平台&#xff0c;小红书有着众多的kol类型。但是该如何合理的使用这些达人&#xff0c;达到品牌传播的目的&#xff0c;就需要一份详尽的计划。今天就跟大家分享一下&#xff0c;小红书kol投放怎么做&#xff0c;kol投放工作规划&#xff01; 什么是kol投放 kol投放即…

迈向未来的大门:人脸识别技术的突破与应用

迈向未来的大门&#xff1a;人脸识别技术的突破与应用 人脸识别&#xff1a;人脸识别的工作流程人脸识别的作用人脸识别技术的突破与应用 在深度学习人脸识别之前我们要先知道人脸识别是什么。 人脸识别&#xff1a; 人脸识别是一种基于人脸图像或视频进行身份验证或识别的技术…

近实时智能应答 2D 数字人搭建

背景 早在大语言模型如 GPT-3.5 等的兴起和被日渐广泛地采用之前&#xff0c;教育行业已经在 AI 辅助教学领域有过各种各样的尝试。在教育行业&#xff0c;人工智能技术的采用帮助教育行业更好地实现教学目标、提高教学质量、提高学习效率、提高学习体验、提高学习成果。例如&a…

动态内存开辟

动态内存开辟 1.动态内存开辟相关试题 题目1&#xff1a; void GetMemory(char *p) {p (char *)malloc(100); } void Test(void) {char *str NULL;GetMemory(str);strcpy(str, "hello world");printf(str); }解释&#xff1a;这里在Test函数中&#xff0c;只是将…

FTP“方便”又“便宜”,为什么有必要替代?

FTP作为全世界第一款文件传输协议&#xff0c;在全球范围内得到大量应用&#xff0c;它为特定场景下的专业传输需求提供了解决方案&#xff0c;被各个行业和领域采用。 FTP使用普遍&#xff0c;主要得益于FTP的经济成本低&#xff0c;且使用方便。目前&#xff0c;开源FTP软件有…