写点什么

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

  • 2024-02-22
    福建
  • 本文字数:6900 字

    阅读完需:约 23 分钟

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


因此,从今天开始,我将采取以下几个步骤:首先,我会提前画出一张图来展示本章节要讲解的内容的调用链路,供大家参考。其次,在文章中,我只会展示最核心的代码或关键的类。剩下的内容将主要用来讲解原理。如果你真的在学习 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 注解


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


属性注入


基本用法示例:


@Componentpublic class UserService {  @Autowired	public OrderService orderService;}
复制代码


setter 方法注入


基本用法示例:


@Componentpublic class UserService {
public OrderService orderService; @Autowired public void setOrderService(OrderService orderService){ System.out.println(0); this.orderService = orderService; }}
复制代码


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


@Componentpublic class UserService {


@Componentpublic 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),并进行依赖注入操作。



属性字段注入


  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{}
复制代码


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


@Componentpublic 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

体验地址:http://www.jnpfsoft.com/?from=001

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
源码剖析Spring依赖注入:今天你还不会,你就输了_spring_快乐非自愿限量之名_InfoQ写作社区