99%的人把Spring的自动注入理解错了!(范例→源码证明)

突然发现现在有很多人把 @Autowired 注解加在属性上,认为是自动注入。

笔者阅读官网之后,发现这是一个天大的误解
于是,笔者试着,写一些代码案例,来证明,这和 Spring 所提供的自动注入是有很大差别的。
当然,通过源码,更能直观的说明,这不属于 Spring 所提供的自动注入。

再谈自动注入之前,首先得先明白,什么是依赖注入:
官网上的描述有些过于复杂,对于我们来说,简单点理解,就是把一个 bean 作为另一个 bean 的属性填充进去。

不过,这么一句简单的话,背后隐藏着许许多多的知识点:

  • 首先,怎么注入呢?
  • Spring 又该怎么寻找的注入的对象呢?
  • Spring 又要如何判断,注入的对象,是否是正确的呢?

这是最根本的问题,到后文再进行讨论。

首先,要弄明白的是,Spring 有哪些注入的方式?
(我们至少要先把应用理解清楚)

在 Spring 的官网上,官方给出的解释是:
DI 存在两个主要变体:基于构造函数的依赖注入基于 Setter 的依赖注入

也就是说,其实 Spring 的依赖注入,粗划分,就是两种:

  • 一种是用构造方法,传参注入;
  • 还有一种,就是 new 了对象之后,再注入;

也就是说,我们通常使用的 set 方法注入,就是基于 Setter 的依赖注入的一种变体;
@Autowired 注解注入,也是基于 Setter 的依赖注入的一种变体,它们本质上是出于一家的。

不过,虽是同根生,但毕竟,它们已经不再是同一种方式了。

现在,我们来一点点演示。

先:
最普通的,我们此时,通过一个 xml 来配置 bean 的依赖关系。

@Component
public class A {
     
	private B b;
	public void setB(B b) {
     
		this.b = b;
	}
	public B getB() {
     
		return b;
	}
}
<bean id="a" class="com.jiang.bean.A">
	<property name="b" ref="b"></property>
</bean>

<bean id="b" class="com.jiang.bean.B">
</bean>
public static void main(String[] args) {
     
	ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
	A a = (A) applicationContext.getBean("a");
	// 我们来打印a中注入的b
	System.out.println(a.getB());
}

99%的人把Spring的自动注入理解错了!(范例→源码证明)_第1张图片
可以发现,注入成功了。

然后,我们再来看一个基于注解 @Autowired 的例子,大家可能平时会把它当做自动注入。
(这么说实际上不对,马上我来解释)

@Component
public class A {
     
	@Autowired
	private B b;
	// 没有提供set方法
	public B getB() {
     
		return b;
	}
}
public static void main(String[] args) {
     
	ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);
	A a = (A) applicationContext.getBean("a");
	System.out.println(a.getB());
}

99%的人把Spring的自动注入理解错了!(范例→源码证明)_第2张图片
然后我们可以发现,虽然没有 set 方法,但是仍然注入成功了。
而且,由于没有配置,指定的注入的对象是谁,所以人们公认为这是自动注入。

不要心急,我继续写一个例子,慢慢对比,你就会慢慢理解:
这时,我把第一个例子,配置的 xml 里面,a 的属性指定注释掉,而给 bean 添加了一个 autowire=“byType”。

<bean id="a" class="com.jiang.bean.A" autowire="byType">
	<!--<property name="b" ref="b"></property>-->
</bean>

99%的人把Spring的自动注入理解错了!(范例→源码证明)_第3张图片
这时,我们发现,A能注入B,但是在 xml 配置文件中并没有去手动维护、描述他们之间的依赖关系,而是在 xml 的那个 bean 上面写了 autowire=“byType”。
而关于自动注入的歧义或者被人误解的地方就是这个 autowire=“byType” 引起的。

有些人发现了 @Autowired 不等于 byType,因为,它百度出来,@Autowired 会先根据类型找,然后又根据名字找。
所以说 @Autowired 同时包含了 byType 和 byName。
但是,这么说就是误人子弟!!!

那么,我举了这些例子,有什么用意呢?

这就牵扯到了一个很常见的面试题:
spring有几种依赖注入方式?

Spring 在官网中描述了有 4 种自动装配模式(Autowiring modes):

  • no
  • byName
  • byType
  • constructor

99%的人把Spring的自动注入理解错了!(范例→源码证明)_第4张图片
我觉得大部分人都是会回答这个问题的,都早已耳熟能详。

但是假设面试官深入问:
依赖注入和装配模型有什么关系和区别,很多人可能就会回答不出来。

对于依赖注入和注入模型,我们从一个教简单的角度来回答:

  • 依赖注入是一个过程,主要通过 setter 和构造方法以及一些变体的方式完成把对象依赖、或者填充上的这个过程叫做依赖注入,不管手动装配还是自动装配都有这个过程;
  • 而自动装配模型是一种完成自动装配依赖的手段体现,每一种模型都使用了不同的技术去查找和填充 bean;

那么,它们之间又有什么联系呢?
还记不记得我之前说的:
依赖注入存在两个主要变体:基于构造函数的依赖注入基于 Setter 的依赖注入

  • 对于 byName 和 byType,都是基于 setter 的依赖注入;
  • 对于 constructor,则是基于构造函数的依赖注入。

可能很多人会不理解,这么说好像跟没说没有什么区别!
那么,接下来,我就要给你演示,其中的注意点:

首先,我把代码 A 进行了修改,没有属性,只有一个 set 方法。

@Component
public class A {
     
	public void setB(B b) {
     
		System.out.println("set方法被调用");
	}
}

然后,不论是 ref=“b” 或者是 byType 或者 byName
都会输出如下结果:
在这里插入图片描述

也就是,基于 Setter 的依赖注入与属性无关!

也就是,不论是你手动注入,还是你自动注入,只要是基于 Setter 的依赖注入,那么就一定会去调用我们的 setter 方法,而根本不关心是否真的有这么一个属性。

所以,对于 Spring 来说,set 注入,不过是一个 set 方法调用,只不过我们平时都用于完成属性注入,这已经成为了业内的一种规范。
而对于 Spring 这样的底层实现来说,它要完成的,只是一个对需求的实现,对这么一个标准的实现,调用 set 方法,就是完成了它的实现,我们只要让 set 方法注入属性,就能完成属性填充。
而至于,这个方法到底做了什么,则不是 Spring 关心的事。

现在,我再修改一下代码:

public void setXXXXXXX(B b) {
     
	System.out.println("set方法被调用");
}

99%的人把Spring的自动注入理解错了!(范例→源码证明)_第5张图片
发现,set 后面即使胡乱写一堆名字,方法仍然会被调用。

然后,尝试把方法名的 set 也去掉,改成乱七八糟的名字:

public void xxx(B b) {
     
	System.out.println("set方法被调用");
}

99%的人把Spring的自动注入理解错了!(范例→源码证明)_第6张图片
发现,程序跑完了都没有执行那个方法。

所以,这个现象也可以证明,基于 Setter 的依赖注入,Spring 看的,就是方法的名字,是不是 setxxxxx…
如果是的,并且容器里存在这个 bean 可以传参给它,就会调用这个方法。

所以,setter 的依赖注入,Spring 关注的是 setter 方法,而根本不关心 bean 的属性。

那么,Autowired 的呢?
我到这里还没有对上面抛出的问题进行解答。

首先,@Autowired 在不需要 setter 方法的情况下也可以注入,所以,这个已经不属于常规的 set 方法注入。
不过我们可以说,是 setter 依赖注入方式的一种的变体。
不过,也可以说是一种不同于以上的一种新的方式。因为 Spring 并没有给出明确的定义,我们程序员也不必钻牛角尖,非要给一个定义,我们更重要的,是理解它的实现和原理。

除了不用 set 方法之外,为什么说 @Autowired 的不是自动装配呢?
我们看起来没有指定注入的 bean,很显然是 Spring 自动找到 bean 来进行注入的,那为什么说不是自动注入呢?

首先,并不是说它完全剔除了自动,只是为了区别于 Spring 所声明的 4 种注入模型。
很多人把 @Autowired 当做是一种 byType 的注入模型,这是完全不对的。

@Autowired 包含了一定的自动性,但是并不和其它的自动注入一样,是完全的自动。

  • 我们之前其实可以发现,如果配置了自动注入,则不需要在 set 方法,以及属性上添加任何标注,Spring 会自动的寻找需要注入的地方进行注入;
  • 而对于 @Autowired 注解,需要手动指定在某个属性,或者某个方法上,来指定,要在这个地方进行注入。

也就是,自动注入,不用指定哪些需要注入,而 @Autowired,需要指定哪些属性或者方法需要注入。

这是把 @Autowired 区别于 byType 的第一个原因,因为没有完全消除自动化

第二,@Autowired 实际上和 byType 的注入选择也不一样:

  • byType 是会根据类型去找合适的 bean,如果无法确定类型,就直接放弃注入;
  • 但是 @Autowired 会先根据类型找,如果找到多个同类型的,它不会直接放弃注入,
    而会继续再找,是否有名字符合的,如果还找不到才会罢休。

于是,我增加了一个接口 I;
让 B、C 实现这个接口;
那么 A 在注入 I 的时候,就会找到两个 bean 可以注入:

@Component
public class A {
     
	private I b;

	public void setB(I b) {
     
		this.b = b;
	}
}

99%的人把Spring的自动注入理解错了!(范例→源码证明)_第7张图片
于是,在使用 byType 自动注入的时候,就把偶错了,因为找到两个可以注入的 bean,所以 Spring 就会迷茫,无法再进行注入,就会报错。

那么,就不用 set 方法自动注入,我们用 @Autowired 来注入

@Component
public class A {
     
	@Autowired
	private I b;
}

99%的人把Spring的自动注入理解错了!(范例→源码证明)_第8张图片
这时,我们就会发现,用 @Autowired 注入成功了,并且注入的是 b,也就是我们属性名字也进行了指定。
所以,@Autowired 区别于 byType 的第二个原因,就是因为,它并不是只根据类型注入。

第三,我觉得说服力更大的则是,代码中可以获取 bean 的自动注入模型,所以,我们只要看一下,代码返回的 bean 的自动注入模型,那么就足以证明。
(因为 Spring 的源码都这么写了,还能有错?)
99%的人把Spring的自动注入理解错了!(范例→源码证明)_第9张图片
查看 Spring 源码,我们可以分别看到,这四种注入模型分别对应着 0、1、2、3。

这时,我给属性添加了 @Autowired 注解,然后直接在控制台,打印该 bean 的自动注入模型是哪一个。

public static void main(String[] args) {
     
	ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);
	// 打印注入模型的信息
	System.out.println(((GenericBeanDefinition) ((AnnotationConfigApplicationContext) applicationContext).getBeanFactory().getBeanDefinition("a")).getAutowireMode());
}

99%的人把Spring的自动注入理解错了!(范例→源码证明)_第10张图片
然后,结果打印出是 0,再一次证明了,Spring 的 @Autowired 属于注入模型的 no,就是无自动装配。

所以,对于 @Autowired 注解来注入属性,实际上仅仅只是一个 setter 注入的一个过程,而不属于任何一种自动注入模型。
它属于 Spring 后来开发出的一种新的注入方式,而不是 Spring 原始的自动注入模型中的一种。

那么,既然区分清楚了自动注入的概念,接下来,就要从源码的角度来进行分析。

首先,对于 Spring 来说,我们都比较清楚,bean 的创建有合并 bean 定义信息、实例化、查找元数据、属性填充、调用生命周期回调方法、AOP 代理等等的过程。
而此处探讨的自动注入,则一定在属性填充这一个过程中,所以我们来找到属性填充的源代码。

我们可以先 debug 一下,查看 @Autowired 和 byType 分别是在什么时候注入:
我们可以先看如果是自动注入,set 方法何时被调用:
99%的人把Spring的自动注入理解错了!(范例→源码证明)_第11张图片
99%的人把Spring的自动注入理解错了!(范例→源码证明)_第12张图片
我们发现,在 applyPropertyValues(beanName, mbd, bw, pvs); 这一行代码处:
还没有运行的时候,没有执行 set 方法,所以控制台没有打印信息;
而等到这一行代码执行了,控制台就打印了信息,表示 set 方法被执行了,进行了自动注入。

然后我们再看 @Autowired 注解:
99%的人把Spring的自动注入理解错了!(范例→源码证明)_第13张图片
99%的人把Spring的自动注入理解错了!(范例→源码证明)_第14张图片
我们可以发现,对于 @Autowired 注解,注入的时机则在 metadata.inject(bean, beanName, pvs); 这一行代码这里。
执行了这一行代码过后,属性才被注入。

所以,又可以体现出,@Autowired 注解,和真实的自动注入,并不是同一回事。

那么,我们可以好好研究 Spring 的自动注入:
99%的人把Spring的自动注入理解错了!(范例→源码证明)_第15张图片
在这里,我们光看方法的名字,我们就可以猜想出,根据 byType 找出 bean 要填充的属性就是在这里。
于是,我加一个断点,debug 一下:
99%的人把Spring的自动注入理解错了!(范例→源码证明)_第16张图片
可以发现,确实在这里,把需要填充的那一个属性找出来了。

不过,既然是 byType,那么一定是根据类型去找的,我们可以发现,里面的 beanName 也是 “b”;
不过,由于我提到,Spring 的自动注入是基于方法的,也就是和属性无关,那么,我们就可以继续研究一下,它是怎么推断出这个属性的:

于是,我把 set 方法的名字,做了一个修改,把方法名改了;
方法的参数名字也改了;
然后,继续 debug 到刚才的断点。

public void setBlalala(B bbbb) {
     
	System.out.println("set方法被调用");
}

99%的人把Spring的自动注入理解错了!(范例→源码证明)_第17张图片
然后,我们可以很明确地看出,这个要 set 填充的属性,属性名就是方法 set 的名字,而属性名填什么,都是无关的;
不过,由于是 byType,所以,名字都无关紧要,我们发现,即使名字是乱七八糟的,但是,Spring 根据属性,也已经找到了要填充的那个属性。

然后,我们可以再尝试一下 byName。

public void setB(B bbbb) {
     
	System.out.println("set方法被调用");
}

99%的人把Spring的自动注入理解错了!(范例→源码证明)_第18张图片
我们可以发现,即使参数的名字随便写,但是,只要方法名写对,就能获取到要自动注入的属性。
而当方法名乱写的话,即使参数名正确,那么结果也是不会被找到:
99%的人把Spring的自动注入理解错了!(范例→源码证明)_第19张图片
我们查看一下源码,可以发现,对于自动注入,Spring 是用了 Java 的 BeanInfo 来实现的:
99%的人把Spring的自动注入理解错了!(范例→源码证明)_第20张图片
那么,对于 @Autowired 注解呢?
99%的人把Spring的自动注入理解错了!(范例→源码证明)_第21张图片
看到这样的代码,一般不用点进去看你都知道里面会做什么。
我们可以看到,在这个 doWithLocalFields 方法里面传入了一个 lambda 表达式,那一定意味着,里面的某一处一定调用了这个方法;

所以,就会在这个方法中,找到加了 @Autowired 属性;
然后,开始执行判断,这个 field 是不是 static 的,如果不是,就把属性和 required 值存起来。

那么之后,就可以根据找到的属性来进行属性注入了。
99%的人把Spring的自动注入理解错了!(范例→源码证明)_第22张图片
我们可以发现,当 @Autowired 加载属性上的时候,就是直接 Field.set 填充属性,
所以根本没有用到 set 方法;

当然,由于 @Autowired 也可以加在方法上,所以当 @Autowired 加在方法上的时候,就会调用一次该方法。
所以我们也可以看到代码里有一个 method.invoke。

既然之前提到,在源码中判断的时候,有一步判断是非 static,那么就可以来测试一下:
我们把 a 的属性改成 static,然后尝试打印 a 的属性:
在这里插入图片描述
我们也可以发现确实如此,Spring 也打印了一句日志,来提醒不可以注入 static 属性。

当然,我再这里提到的,仍然只是一部分,由于 Spring 的源码太过庞大,笔者也不可能把所有的地方全部看一遍,写一遍。
所以,我在这里提及的,也仅仅是有关方法注入,以及 @Autowired 的属性注入。

但是,除去这些,Spring 还有构造方法自动注入,@Autowired 也可以加在构造方法上,所以这里也涉及到很多的知识点,有关 Spring 是如何去推断出构造方法的。

所以,对于属性的注入,还得去继续研究构造方法,你可以去我的这篇博客先看看,开头的连环构造方法轰炸,你能答对几轮。
99%的人答不对Spring推断构造方法!打破你三观!!!(范例→源码)

你可能感兴趣的:(Spring,spring,java,编程语言)