源码剖析Spring依赖注入:今天你还不会,你就输了

在之前的讲解中,我乐意将源码拿出来并粘贴在文章中,让大家看一下。然而,我最近意识到这样做不仅会占用很多篇幅,而且实际作用很小,因为大部分人不会花太多时间去阅读源码。

因此,从今天开始,我将采取以下几个步骤:首先,我会提前画出一张图来展示本章节要讲解的内容的调用链路,供大家参考。其次,在文章中,我只会展示最核心的代码或关键的类。剩下的内容将主要用来讲解原理。如果你真的在学习Spring源码,我希望你能打开你的项目,并跟着我一起深入阅读源码。现在,让我们开始吧。今天的重点是Spring的依赖注入。

基本使用

首先,值得注意的是,在Spring框架中,依赖注入是在bean生成后进行属性赋值的。由于我们的bean通常都是单例模式,所以每个类的属性都必须进行注入。在这个过程中,会涉及到代理、反射等技术的应用。如果你对这些概念不太熟悉的话,建议你提前补充一下相关的前提知识。了解这些基本概念将有助于你更好地理解和掌握Spring框架的依赖注入机制。

首先需要注意的是,尽管图示可能只展示了类之间的简单调用关系,但这并不代表实际的依赖注入过程就是如此简单。实际上,Spring框架的版本和配置方式可能会导致不同的链路调用。然而,无论具体的版本差异如何,Spring框架的依赖注入机制的基本逻辑大致是一样的。

本节课的链路调用图例地址:https://viewer.diagrams.net/index.html?tags={}&highlight=0000ff&edit=_blank&layers=1&nav=1&title=未命名绘图.drawio#Uhttps%3A%2F%2Fraw.githubusercontent.com%2FStudiousXiaoYu%2Fdraw%2Fmain%2F未命名绘图.drawio

Spring的依赖注入有两种方式:手动注入、自动注入。下面我们详细讲解一下这两种方式。

手动注入

在手动注入中,离不开XML配置。有两种常见的方式可以实现手动注入:通过属性和通过构造器。手动就是我们人为控制注入的值,下面是两种配置方式:

<bean id="user" class="com.xiaoyu.service.UserService" >
		<property name="orderService" ref="orderService"/>
</bean>

上面是通过使用set方法进行依赖注入的方式来实现。

<bean id="user" class="com.xiaoyu.service.UserService">
		<constructor-arg index="0" ref="orderService"/>
</bean>

上面是通过使用构造方法进行依赖注入的方式来实现。

自动注入

XML配置

XML也有自动分配的机制,只要不是我们手动指定注入类,那就是自动注入,让我们一起了解如何进行设置。

在XML中,我们可以通过在定义一个Bean时指定自动注入模式来进行优化。这些模式包括byType、byName、constructor、default和no。通过使用这些模式,我们可以更灵活地控制Bean的注入方式。

<bean id="user" class="com.xiaoyu.service.UserService" autowire="byType"/>

<bean id="user" class="com.xiaoyu.service.UserService" autowire="byName"/>

剩下的不举例了,这两种类型,都需要我们的UserService对象有相应的set方法。因为注入的点就是先找到set方法,然后在填充属性之前,Spring会去解析当前类,把当前类的所有方法都解析出来。Spring会解析每个方法,得到对应的PropertyDescriptor对象。PropertyDescriptor对象中包含了几个属性:

name:获取截取后的方法名称:截取规则如下:

  • get开头,则去除get,比如“getXXX”,那么name=XXX(首字母小写),需无参或者第一个参数为int类型

  • is开头不并且返回值为boolean类型,比如“isXXX”,那么name=XXX(首字母小写),需无参

  • set开头并且有无返回值,比如“setXXX”,那么name=XXX(首字母小写),前提是得有入参,如果无入参是解析不到set开头的方法的

readMethodRef:如果是get开头或者is开头的方法,都是readMethodRef,并且存储的引用。

readMethodName:是get开头或者is开头的方法名。包含get/is

writeMethodRef:set开头的方法引用。

writeMethodName:set开头的方法名,包含set。

propertyTypeRef:如果是读方法,则获取的是返回值类型,如果是set写方法,则获取的是入参类型。

具体实现可自行查看源码:java.beans.Introspector#getTargetPropertyInfo()

@Autowired注解

这个注解大家都很熟悉,我简单介绍一下它的基础用法。最后,通过查看源码,我们将依赖注入的过程完整地连起来。

属性注入

基本用法示例:

@Component
public class UserService {
  @Autowired
	public OrderService orderService;
}

setter方法注入

基本用法示例:

@Component
public class UserService {

	public OrderService orderService;
	
	@Autowired
	public void setOrderService(OrderService orderService){
		System.out.println(0);
		this.orderService = orderService;
	}
}

构造器注入基本用法示例:

@Componentpublic class UserService {

@Component
public class UserService {

	public OrderService orderService;
	
	@Autowired
	public UserService(OrderService orderService){
		this.orderService = orderService;
	}
	
}

依赖注入关键源码解析

寻找注入点

在创建一个Bean的过程中,Spring会利用AutowiredAnnotationBeanPostProcessor的postProcessMergedBeanDefinition()方法来找出注入点并进行缓存。具体的找注入点的流程如下:

  1. 如果一个Bean的类型是String,那么则根本不需要进行依赖注入

  2. 遍历目标类中的所有Field字段,field上是否存在@Autowired、@Value、@Inject中的其中一个

  3. static 字段不是注入点,不会进行自动注入

  4. 构造注入点,获取@Autowired中的required属性的值,将字段封装到AutowiredFieldElement对象。

  5. 遍历目标类中的所有Method方法。

  6. method上是否存在@Autowired、@Value、@Inject中的其中一个

  7. static method不是注入点,不会进行自动注入

  8. set方法最好有入参,没有入参或提示日志。

  9. 构造注入点,获取@Autowired中的required属性的值,将方法封装到AutowiredMethodElement对象。

  10. 查看是否还有父类,如果有再次循环直到没有父类。

  11. 将刚才构造好的注入点全都封装到InjectionMetadata,作为当前Bean对于的注入点集合对象,并缓存。

static字段或方法为什么不支持注入

在源码中,Spring会判断字段或方法是否是static来决定是否进行注入。如果字段或方法是static的,Spring不会进行注入操作。这是因为静态字段或方法是属于类的,而不是属于具体的实例。因此,在进行依赖注入时,Spring会注入给具体的实例,而不是整个类。

我们知道Spring是支持创建原型bean的,也就是多例模式。

@Component
@Scope("prototype")
public class UserService {
  @Autowired
  private static OrderService orderService;
  public void test() {
  System.out.println("test123");
  }
}

确实,如果OrderService是prototype类型的,并且Spring支持注入static字段,那么每次注入OrderService到UserService时都会创建一个新的实例。这样做确实违背了static字段的本意,因为static字段是属于类的,而不是实例的。

注入点注入

在依赖注入的过程中,注入点的注入肯定会在populateBean方法中进行属性注入。在这个过程中,会调用AutowiredAnnotationBeanPostProcessor的postProcessProperties()方法,该方法会直接给对象中的属性赋值。这个方法会遍历每个注入点(InjectedElement),并进行依赖注入操作。

image

属性字段注入

  1. 遍历所有AutowiredFieldElement对象。

  2. 将对应的字段封装到DependencyDescriptor。

  3. 调用beanFactory.resolveDependency来获取真正需要注入的bean。

  4. 最后将此次封装的DependencyDescriptor和beanname缓存起来,主要考虑到了原型bean的创建

  5. 利用反射给filed赋值

setter方法注入

  1. 遍历所有AutowiredMethodElement对象。

  2. 调用resolveMethodArguments方法

  3. 遍历每个方法参数,找到匹配的bean对象,将方法对象封装到DependencyDescriptor中。

  4. 调用beanFactory.resolveDependency来获取真正需要注入的bean。

  5. 最后将此次封装的DependencyDescriptor和beanname缓存起来,主要考虑到了原型bean的创建

  6. 利用反射给filed赋值

我们只需要关注findAutowiringMetadata方法的实现,因为大家普遍了解注入的概念。我们主要关注的是它是如何找到注入点的。

	private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
		// Fall back to class name as cache key, for backwards compatibility with custom callers.
		String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
		// Quick check on the concurrent map first, with minimal locking.
		InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
		if (InjectionMetadata.needsRefresh(metadata, clazz)) {
			synchronized (this.injectionMetadataCache) {
				metadata = this.injectionMetadataCache.get(cacheKey);
				if (InjectionMetadata.needsRefresh(metadata, clazz)) {
					if (metadata != null) {
						metadata.clear(pvs);
					}
					// 解析注入点并缓存
					metadata = buildAutowiringMetadata(clazz);
					this.injectionMetadataCache.put(cacheKey, metadata);
				}
			}
		}
		return metadata;
	}

	private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
		// 如果一个Bean的类型是String...,那么则根本不需要进行依赖注入
		if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) {
			return InjectionMetadata.EMPTY;
		}

		List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
		Class<?> targetClass = clazz;

		do {
			final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();

			// 遍历targetClass中的所有Field
			ReflectionUtils.doWithLocalFields(targetClass, field -> {
				// field上是否存在@Autowired、@Value、@Inject中的其中一个
				MergedAnnotation<?> ann = findAutowiredAnnotation(field);
				if (ann != null) {
					// static filed不是注入点,不会进行自动注入
					if (Modifier.isStatic(field.getModifiers())) {
						if (logger.isInfoEnabled()) {
							logger.info("Autowired annotation is not supported on static fields: " + field);
						}
						return;
					}

					// 构造注入点
					boolean required = determineRequiredStatus(ann);
					currElements.add(new AutowiredFieldElement(field, required));
				}
			});

			// 遍历targetClass中的所有Method
			ReflectionUtils.doWithLocalMethods(targetClass, method -> {

				Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
				if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
					return;
				}
				// method上是否存在@Autowired、@Value、@Inject中的其中一个
				MergedAnnotation<?> ann = findAutowiredAnnotation(bridgedMethod);
				if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
					// static method不是注入点,不会进行自动注入
					if (Modifier.isStatic(method.getModifiers())) {
						if (logger.isInfoEnabled()) {
							logger.info("Autowired annotation is not supported on static methods: " + method);
						}
						return;
					}
					// set方法最好有入参
					if (method.getParameterCount() == 0) {
						if (logger.isInfoEnabled()) {
							logger.info("Autowired annotation should only be used on methods with parameters: " +
									method);
						}
					}
					boolean required = determineRequiredStatus(ann);
					PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
					currElements.add(new AutowiredMethodElement(method, required, pd));
				}
			});

			elements.addAll(0, currElements);
			targetClass = targetClass.getSuperclass();
		}
		while (targetClass != null && targetClass != Object.class);

		return InjectionMetadata.forElements(elements, clazz);
	}

@Resource

说到这里,可能有些小伙伴还会使用@Resource注解来进行依赖注入。其实,这和@Autowired注解的逻辑是一样的,只是调用的是其他类的相关方法。具体来说,通过org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#postProcessMergedBeanDefinition方法来查找注入点,然后在org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#postProcessProperties方法中进行属性填充。关于这些细节我们就不详细讨论了,如果感兴趣的话,可以查看一下源码。

@Qualifier

对于使用过@Autowired注解的同学来说,他们肯定也了解@Qualifier注解的作用。@Qualifier主要用于解决一个接口有多个实现类的情况。为了更好地理解,我们来举一个简单的例子:

public interface User {
}

@Component
@Qualifier("userF")
public class UserF implements User{
}

@Component
@Qualifier("userM")
public class UserM implements User{
}

在上述内容中,简要定义了两个实现。现在我们需要使用它们。

@Component
public class UserService {

	@Autowired
	@Qualifier("userM")
	public User user;
}

在这种情况下,会去匹配userM的实体类,而不会出现多个匹配类导致异常。那么它是如何解决这个问题的呢?它是在什么时候找到@Qualifier注解的呢?具体的源码如下所示:

	protected boolean checkQualifier(
			BeanDefinitionHolder bdHolder, Annotation annotation, TypeConverter typeConverter) {
		// 检查某个Qualifier注解和某个BeanDefinition是否匹配

		// annotation是某个属性或某个方法参数前上所使用的Qualifier
		Class<? extends Annotation> type = annotation.annotationType();
		RootBeanDefinition bd = (RootBeanDefinition) bdHolder.getBeanDefinition();

		// 首先判断BeanDefinition有没有指定类型的限定符
		AutowireCandidateQualifier qualifier = bd.getQualifier(type.getName());
		if (qualifier == null) {
			qualifier = bd.getQualifier(ClassUtils.getShortName(type));
		}
		if (qualifier == null) {
			// First, check annotation on qualified element, if any
			Annotation targetAnnotation = getQualifiedElementAnnotation(bd, type);
			// Then, check annotation on factory method, if applicable
			if (targetAnnotation == null) {
				targetAnnotation = getFactoryMethodAnnotation(bd, type);
			}
			if (targetAnnotation == null) {
				RootBeanDefinition dbd = getResolvedDecoratedDefinition(bd);
				if (dbd != null) {
					targetAnnotation = getFactoryMethodAnnotation(dbd, type);
				}
			}
			if (targetAnnotation == null) {
				// Look for matching annotation on the target class
				if (getBeanFactory() != null) {
					try {
						// 拿到某个BeanDefinition对应的类上的@Qualifier
						Class<?> beanType = getBeanFactory().getType(bdHolder.getBeanName());
						if (beanType != null) {
							targetAnnotation = AnnotationUtils.getAnnotation(ClassUtils.getUserClass(beanType), type);
						}
					}
					catch (NoSuchBeanDefinitionException ex) {
						// Not the usual case - simply forget about the type check...
					}
				}
				if (targetAnnotation == null && bd.hasBeanClass()) {
					targetAnnotation = AnnotationUtils.getAnnotation(ClassUtils.getUserClass(bd.getBeanClass()), type);
				}
			}
			// 注解对象的equals比较特殊,JDK层面用到了动态代理,会比较value
			if (targetAnnotation != null && targetAnnotation.equals(annotation)) {
				return true;
			}
		}
		......
		return true;
	}

他其实是在我们上面所说的属性注入的时候去匹配查找的。具体来说,他会调用beanFactory.resolveDependency方法来获取真正需要注入的bean时进行查找。如果想要查看相关的源码,可以去查看org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver#isAutowireCandidate方法。在这个方法中会有更详细的解释。

总结

今天我们主要讲解的是Spring依赖注入。在本文中,我们主要围绕bean填充属性的字段和setter方法展开讨论。要记住的是,在进行属性注入时,我们首先需要找到注入点并进行缓存,然后才会真正进行属性注入。需要注意的是,静态字段或方法是不会进行依赖注入的。最后,我们简单地介绍了一下关键源码,以及对@Resource和@Qualifier进行了简单的分析。如果想要学习Spring源码,一定要结合图例去理解,否则很容易晕头转向。

文章转载自:努力的小雨

原文链接:https://www.cnblogs.com/guoxiaoyu/p/17963111

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Vue3】toRefs和toRef在reactive中的一些应用

&#x1f497;&#x1f497;&#x1f497;欢迎来到我的博客&#xff0c;你将找到有关如何使用技术解决问题的文章&#xff0c;也会找到某个技术的学习路线。无论你是何种职业&#xff0c;我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章&#xff0c;也欢…

sql server想要小数点后向下取整怎么搞

select FORMAT(3.169, N2) as 四舍五入1, CAST(3.169 AS decimal(9,2)) as 四舍五入2, ROUND(3.169, 2) as 四舍五入3, CAST(FLOOR(3.169 * 100) / 100 AS decimal(9,2)) as 向下取整1, FLOOR(3.169 * 100) / 100 as 向下取整2, ceiling(3.169 * 100) / 100 as 向上取整—…

如何使用 GitHub Action 在 Android 中构建 CI-CD

如何使用 GitHub Action 在 Android 中构建 CI-CD 一、什么是 CI/CD&#xff1f;二、什么是 CI&#xff1a;持续集成&#xff1f;三、什么是CD&#xff1a;持续部署&#xff1f;3.1 持续交付3.2 持续部署 四、使用 GitHub 操作在 Android 中构建 CI-CD 管道4.1 步骤 五、什么是…

K210基础实验——点亮LED灯

一、目的是点亮K210开发板左下角的LED0和LED1&#xff0c;LED0是红灯&#xff0c;LED1是绿灯&#xff0c;两颗LED灯都是低电平点亮&#xff0c;高电平熄灭。 二、这是原理图上的硬件连接&#xff0c;LED0连接的是IO0&#xff0c;LED1连接的是IO17。 三、在src目录下新建文件夹 …

P2670 [NOIP2015 普及组] 扫雷游戏 ---- 洛谷

题目描述 扫雷游戏是一款十分经典的单机小游戏。在 n 行 m 列的雷区中有一些格子含有地雷&#xff08;称之为地雷格&#xff09;&#xff0c;其他格子不含地雷&#xff08;称之为非地雷格&#xff09;。玩家翻开一个非地雷格时&#xff0c;该格将会出现一个数字——提示周围格子…

《游戏引擎架构》--学习3

内存管理 优化动态内存分配 维持最低限度的堆分配&#xff0c;并且永不在紧凑循环中使用堆分配 容器 迭代器 Unicode

uni-app搭建h5项目

一、 打开官方网站 https://uniapp.dcloud.net.cn/quickstart-cli.html 二、找到使用vue-cli命令行&#xff0c;按照文档上的步骤进行搭建 全局安装 vue-cli npm install -g vue/cli搭建项目 可以根据命令行搭建&#xff0c;搭建vue2.0对应的是webpack&#xff0c; 也可以搭…

vulnhub靶场---->DOUBLETROUBLE: 1

详细思路 知识点1.网段探测2.nmap扫描3.目录扫描4.隐写5.上传木马文件6.awk提权 知识点 图片隐写----->steghide 密码爆破----->stegseek awk提权------>sudo awk BEGIN {system("/bin/bash")}1.网段探测 kali:192.168.0.131 靶机&#xff1a;192.168.0.1…

微信小程序-消息订阅整体流程

1.首先登录微信公众平台&#xff0c;网址&#xff1a;微信公众平台 2.找到“订阅消息”&#xff0c;如果订阅消息没开启&#xff0c;可以点击开启&#xff0c;然后根据自己的消息需求到公共模版里找合适的&#xff0c;然后点击选用&#xff0c;如果没有比较合适的可以申请模版…

【开源】SpringBoot框架开发音乐平台

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块三、系统展示 四、核心代码4.1 查询单首音乐4.2 新增音乐4.3 新增音乐订单4.4 查询音乐订单4.5 新增音乐收藏 五、免责说明 一、摘要 1.1 项目介绍 基于微信小程序JAVAVueSpringBootMySQL的音乐平台&#xff0c;包含了音乐…

使用maven集成spring在测试的时候报出了如下的异常:version 60

使用maven集成spring在测试的时候报出了如下的异常&#xff1a; Caused by: java.lang.IllegalArgumentException: Unsupported class file major version 60 解决&#xff1a;

MySQL学习笔记3: MySQL数据库基础

目录 前言目标数据库操作&#xff08;针对database 的操作&#xff09;1. 创建数据库 create database 数据库名;2. 查看数据库 show databases;3. 选中数据库 use 数据库名;4. 删除数据库 drop database 数据库名; mysql中支持的数据类型1. 数值类型: NUMERIC(M,D)2. 字符串类…

LeetCode | 两数相加 C语言

Problem: 2. 两数相加 文章目录 思路解题方法Code一些感想 思路 主要是一一相加和逆序的方式存储 先说逆序储存&#xff0c;看下图 我们先声明出指针p和指针q&#xff0c;还有指针head&#xff08;主要用于return上而已&#xff09;&#xff0c;然后进行一系列操作&#xff0c…

【Spring Cloud】实现微服务调用的负载均衡

文章目录 什么是负载均衡自定义实现负载均衡启动shop-product微服务通过nacos查看微服务的启动情况自定义实现负载均衡 基于Ribbon实现负载均衡添加注解修改服务调用的方法Ribbon支持的负载均衡策略通过修改配置来调整 Ribbon 的负载均衡策略通过注入Bean来调整 Ribbon 的负载均…

【Vue】本地使用 axios 调用第三方接口并处理跨域

前端处理跨域 一. 开发准备 开发工具&#xff1a;VScode框架&#xff1a;Vue2项目结构&#xff1a;vue脚手架生成的标准项目&#xff08;以下仅显示主要部分&#xff09; 本地已搭建好的端口&#xff1a;8080要请求的第三方接口&#xff1a;http://1.11.1.111:端口号/xxx-api…

3.1 XPath的使用

目录 3.1.1 XPath的作用 3.1.2 XPath使用前提 3.1.3 XPath的使用 &#xff08;1&#xff09;属性匹配和获取一个属性值节点的内容 &#xff08;2&#xff09;属性多值匹配 &#xff08;3&#xff09;获取<>中的内容 &#xff08;4&#xff09;多属性匹配 &#…

通知:全面停工停产6个月,员工解雇工

随着新能源汽车兴起&#xff0c;汽车行业涌现出许多造车新势力&#xff0c;包括蔚来、理想、小鹏、哪吒、零跑等。然而&#xff0c;近日&#xff0c;高合汽车陷入了困境&#xff0c;宣布全面停工停产&#xff0c;员工被解雇&#xff0c;新车降价清仓。 据百能云芯电子元器件商城…

力扣225 用队列实现栈

题目链接 如果用两个栈来实现队列&#xff0c;那么可以用两个队列来实现栈吗 首先看入栈操作和入队是一样的&#xff0c;直接用 就是这个出栈&#xff0c;每次出栈的是队列里的最后一个元素&#xff0c;那么将前面的元素依次入到第二个队列&#xff0c;然后再将所剩的最后一个…

Spring解决循环依赖

目录 什么是spring循环依赖 什么情况下循环依赖可以被处理&#xff1f; spring 如何解决循环依赖 创建A这个Bean的流程 答疑 疑问&#xff1a;在给B注入的时候为什么要注入一个代理对象&#xff1f; 初始化的时候是对A对象本身进行初始化&#xff0c;而容器中以及注入到B…

windows系统下载安装 memcached

好 之前说 带大家要用spring boot整合cache 其中包括 memcached 那么今天就带大家 下载安装一下 我们先访问地址 https://cloud.tencent.com/developer/article/2049577 这也是别人发的一个网址 里面放了他放在网上的资源 简单归纳为 windows 32位 链接 https://share.weiyun…