我是子路,一个靠Java吃饭的男人。
上一篇文章,我花了很长的篇幅介绍了Spring的循环引用,今天我就来好好跟大家讲讲你可能真的不了解的Spring自动注入。
要是提到spring的自动注入,作为一个java程序员肯定自信无比了解;但是笔者要说的自动注入可能会和你理解有很大出入。
首先搞明白什么是自动注入,自动注入也可以叫做自动装配(springboot也有一个自动装配但是我认为翻译的不够准确,springboot的应该叫做自动配置和这里说的自动注入是两回事,笔者不是什么大牛或者权威;所以读者如果你坚持认为springboot也叫自动装配那也无可厚非,只是在这篇文章里面所谓的自动注入就是自动装配,关于springboot的自动配置我以后更新文章再来说);
自动注入需要相对于手动装配来说;在spring应用程序当中假设你的A类依赖了B类,需要在A类当中提供一个B类的属性,再加上setter,继而在xml当中配置、描述一下这两个类之间的依赖关系。如果做完当容器初始化过程中会实例化A,在实例化A的过程中会填充属性,由于在xml中已经配置、描述好两者的关系,故而spring会把B给A装配上;这种由程序员自己配置、描述好依赖关系的写法叫做手动装配;看个例子吧:
package com.luban.app;
public class A {
B b;
public void setB(B b) {
this.b = b;
}
}
package com.luban.app;
public class B {
}
但是实际开发中手动装配的场景比较少(比如在缺少源码的情况下可能会使用这种手动装配情况);
关于依赖注入的资料可以 参考官网
这一章节提到了一个非常重要的知识点,也是一个常见的spring面试题目。spring有几种依赖注入方式?那么这个问题应该怎么回答呢?
官网的意思是DI(依赖注入)一共有两种主要的变体(注意会考),分别是基于构造方法的依赖注入和基于setter(setXxxx(…))的依赖注入,不管是手动装配还是自动装配都是基于这两种方式或者变体方式来的;但是这里一定要回答到主要和变体两个名词,因为有的注入方式就不是这两种,而是这两种其中一种的变体方式;比如在一个类的属性上面加@Autowired,这种方式注入属性的方式就是利用了java的反射知识,field.set(value,targetObject);关于这个我在后面的文章中对spring源码解析的时候会说明@Autowired的原理;所以@Autowired这种注入的方式是setter注入方式的一种变体。
但是这里需要说明的是所谓的setter其实和属性无关,什么意思呢?一般的setter方法会对应一个属性,但是spring的基于setter的注入方式是不需要属性的,仅仅只需要一个setter方法,下面这个例子来说明这个问题:
B.java
public class B {
}
A.java
public class A {
public void setXxx(B b) {
System.out.println("spring 找到符合的setter");
System.out.println("和属性无关,甚至可以不要属性");
System.out.println("可以直接调用,这个A里面就没有任何属性");
}
}
xml配置文件
运行上面的代码可以看到spring也会调用这个setXxx方法,如果仔细观察调用栈可以看到这个方法是在spring容器初始化的时候实例化A,完成A的注入功能时候调用过来的,下图是笔者运行结果的截图:
可能有的读者会认为上面的xml配置文件中已经手动装配了B给A,肯定会调用setXxx方法,其实不然,即是我使用自动装配也还是会调用的(关于什么是自动装配,下文会详细介绍),因为前文说过,注入方式与手动注入、自动注入无关。笔者改一下代码,把A的注入模型改成自动注入来看看结果:
把代码改成上面这样自动装配结果是一样的,A类当中的setXxx方法还是会调用,不需要提供任何属性,至于原理笔者后面更新到spring源码的时候再来详细说道;可能有读者会说如果使用注解呢?比如如下代码:
A.java
@Component
public class A {
@Autowired
B b;
public void setB(B b) {
System.out.println("如果你使用注解,这个setter变得毫无意义");
System.out.println("这个方法甚至都不会调用");
System.out.println("因为前文说过这种方法用的是field.set");
}
}
B.java
import org.springframework.stereotype.Component;
@Component
public class B {
}
上面代码同样会注入b,但是是调用field.set去完成注入的,不是通过setter,当然再次说明一下这是setter的一种变体;上面代码直到A完成注入B后setter方法也不会调用;
重点来了,要考。笔者看过很多资料说@Autowired也算自动装配,关于这点笔者不敢苟同,在一个属性上面加@Autowired注解应该属于手动装配,我会通过大量源码和例子来证明笔者的这个理论。
之所以会有人把@Autowired也理解为自动装配的原因是因为bytype引起的,因为spring官网有说明自动装配有四种模型分表是no、bytype、byname、constructor;参考官网资料,而现在流行着这么一种说法:@Autowired就是通过bytype来完成注入的。如果你也认为这种说法成立,那么就是默认了@Autowired是自动装配且装配模型是bytype。笔者见过很多程序员和很多资料都支持这种说法,其实严格意义上来讲这句话大错特错,甚至有误人子弟的嫌疑。因为bytype仅仅是一种自动注入模型而已,这种模型有其固有的技术手段,而@Autowired是一个注解,这个注解会被spring的后置处理器解析,和处理bytype不是同一回事。如果需要讲清楚他们的区别和证明@Autowired不是自动装配则首先要搞明白什么自动装配。笔者接下来会花一定篇幅来解释自动装配的知识,然后回过头来讲他们的区别和证明@Autowired属性手动装配。
如果现在已经理解了手动装配也叫手动注入,也已经理解了注入方式(setter和构造方法),那么接下来讨论自动注入或者叫自动装配;自动注入的出现是因为手动装配过于麻烦,比如某个类X当中依赖了10个其他类那么配置文件将会变的特别冗余和臃肿,spring的做法是可以为这个X类提供一种叫做自动装配的模型,无需程序员去手动配置X类的依赖关系。
有读者会疑问,用注解不也是可以解决这个xml臃肿的问题?确实用注解可以解决,但是我们现在讨论的是自动装配的问题,就不能用注解;
为什么不能用注解来讨论自动装配的问题呢?因为在不配置BeanFactoryPostProcessor和修改beanDefinition的情况下注解的类是不支持自动装配的(关于BeanFactoryPostProcessor和beanDefinition的知识笔者以后有时间更新);这也是证明@Autowired默认不是自动装配的一个证据,那又如何证明注解类是默认不支持自动装配呢?下文我会解释一个注解类默认是不支持自动装配的。
也就是说如果讨论自动装配最好是用xml形式来配置spring容器才会有意义;当然读者如果对spring比较精通其实通过修改注解类的beanDefinition来说明自动装配的问题,鉴于大部分读者对spring不精通故而笔者还是用xml来讨论吧;比如下面这个例子:
A.java
public class A {
B b;
public void setB(B b) {
System.out.println("在不考虑注解的情况下");
System.out.println("如果配置了自动装配则不需要手动配置");
}
}
B.java
public class B {
}
上面代码运行起来,A能注入B,但是在xml配置文件中并没有去手动维护、描述他们之间的依赖关系,而是在xml的根标签上面写了一行default-autowire=“byType”,其实关于自动注入的歧义或者被人误解的地方就是这个default-autowire="byType"引起的;那么这行代码表示什么意思呢?表示所在配置在当前xml当中的bean都以bytype这种模式自动装配(如果没有特殊指定,因为bean还可以单独配置装配模式的);这需要注意笔者的措辞,笔者说的bytype这自动装配模式,是一种模式,这个不是笔者信口开河,因为在官网文档里面spring也是这么定义的。
首先官网介绍beanDefinition这一章节的时候有提到bean的Autowiring mode
再就是spring官网在介绍spring自动注入知识的章节也提到过这个名词。
Autowiring mode 笔者姑且翻译为自动注入模型吧,如果有211、雅思读者可以给读者留言更合适的翻译。
那为什么要锱铢在这个名词上呢?笔者觉得真的非常重要,很多初学spring的程序员都是粗学spring,忽略甚至混淆了很多关键的名词定义,导致很多观念根深蒂固后面想深入学习spring源码的时候就比较困难。
这个名字叫做自动注入模型和前面提到的依赖注入方式(setter和构造方法)是两回事,简而言之:依赖注入是一个过程,主要通过setter和构造方法以及一些变体的方式完成把对象依赖、或者填充上的这个过程叫做依赖注入,不管手动装配还是自动装配都有这个过程;而自动装配模型是一种完成自动装配依赖的手段体现,每一种模型都使用了不同的技术去查找和填充bean;而从spring官网上面可以看到spring只提出了4中自动装配模型(严格意义上是三种、因为第一种是no,表示不使用自动装配、使用),这四个模型分别用一个整形来表示,存在spring的beanDefinition当中,任何一个类默认是no这个装配模型,也就是一个被注解的类默认的装配模型是no也就是手动装配;其中no用0来表示;bytype用2来表示;如果某个类X,假设X的bean对应的beanDefinition当中的autowireMode=2则表示这个类X的自动装配模型为bytype;如果autowireMode=1则表示为byname装配模型;需要注意的是官网上面说的四种注入模型其中并没有我们熟悉的@Autowired,这也再一次说明@Autowired不是自动装配;可能有读者会提出假设我在A类的某个属性上面加上@Autowired之后这个A类就会不会成了自动装配呢?@Autowired是不是会改变这个类A当中的autowireMode呢?
我们可以写一个例子来证明一下:
xml配置了A 和B 都是自动装配模型为bytype讲道理要实现autowireMode=2
A.java
public class A {
}
B.java
public class B {
}
提供一个后置处理器来获取A的自动装配模型
ZiluBeanFactoryPostprocessor.java
@Component
public class ZiluBeanFactoryPostprocessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
GenericBeanDefinition a = (GenericBeanDefinition)
beanFactory.getBeanDefinition("a");
//打印A 的注入模型
System.out.println("a mode="+a.getAutowireMode());
}
}
讲道理由于是bytype所以应该打印出来2; 结果如图:
如果笔者把注入模型改成byname则结果应该会改变
接下来笔者验证一个通过注解配置的类加上@Autowried后的注入模型的值
可以看到结果为0,说明这个A类不是自动装配,其实这已经能证明@Autowried不是自动装配了,但是还有更直接的证据证明他不是自动装配,就是通过spring源码当中处理@Autowried的代码可以看出,首先我们写一个bytype的例子看看spring如何处理的:
A.java
public class A {
B b;
/**
* 如果是自动装配需要提供setter
* 或者提供构造方法
*/
public void setB(B b) {
this.b = b;
}
}
B.java
public class B {
}
上面代码运行起来,调试spring源码当中的org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#populateBean方法,这个方法主要就是完成属性填充的,也就是大家说的注入
可以看上图1399行有一个判断,判断当前类的注入模式是否bynane或者bytype如果是其中一种则会进入1401行代码执行自动装配的逻辑;因为当前代码我在xml当中配置了自动注入模型为bytype所以这里一定会进入,从上图debug的结果我们可以得知确实也进入了1401行,那我们再看看@Autowried到底是否是一样的呢?
好吧作为良心笔者特意录了一个gif:
从上面那个笔者良心录制的gif可以看到当我们使用@Autowired注解去注入一个属性的时候spring在完成属性注入的过程中和自动注入(byname、bytype)的过程不同,spring注解跳过了那个判断,因为不成立,而是用后面的代码去完成属性注入;这也是能说明@Autowired不是自动装配的证据、更是直接打脸@Autowired是先bytype的这种说法;当然除了这个证据还有更加直接的证据,先看代码:
@Component
public class A {
B b;
public A(B b){
this.b=b;
System.out.println(b);
System.out.println("这个b能正常打印,那这算不算自动装配呢?");
System.out.println("看起来像是constructor这种自动装配模型");
System.out.println("但是实际呢?我们通过源码来说明");
System.out.println("这个构造方法没有加任何注解");
System.out.println("这里主要来说明,一个注解类是不是自动装配");
}
}
说明一下这个A类的构造方法注入的B是可以注入的,这样算不算注解类的自动注入呢?看起来像,其实本质上他用的也是自动注入的代码实现的,但是笔者还是认为他不是;为什么呢?我们先来分析一下这个b能被注入的原因是什么,同样给一个gif来说明:
其实最关键的就是这行代码:
if (ctors != null || mbd.getResolvedAutowireMode() ==
AUTOWIRE_CONSTRUCTOR ||
mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) { return autowireConstructor(beanName, mbd, ctors, args);
}
这里有个判断 首先判断 ctors != null 然后判断类的自动注入模型是不是等于AUTOWIRE_CONSTRUCTOR也是3;分两种情况来分析:
第一种情况:假设你指定了类的自动注入模型为constructor那么这个if一定成立因为他是||判断只要一个成立整个if成立;很显然我们这里不是这种情况,笔者并没有指定A类的自动注入模型为3,上面的那个gif里面我给读者展示了,后面那个判断不成立,因为mbd.getResolvedAutowireMode()=0;再一次说明了一个注解类默认的自动注入模型为no也就是autowireMode=0;
第二种情况:没有指定类的自动注入模型,笔者代码例子就是这种情况,那么spring会首先推断出构造方法,笔者A里面一个带参数的构造方法,所以再进行第一个判断ctors!= null的时候就已经成立了,于是也会进入。
结论:在一个注解类里面提供了一个构造方法之所以能达到和自动注入的效果一样并不是因为这种方式就是自动装配,而是因为spring源码当中做了判断;使这种情况下调用的代码和自动装配调用的逻辑一下。但是有的读者会说那这样也能算自动装配啊,当然如果读者一定这么认为那么也无可厚非;仁者见仁智者见智。
那哔哩哔哩说了这么多@Autowried到底和bytype有什么关系呢?为什么有资料会把他们联系在一起呢?
首先bytype是一种自动注入模型,spring会更加类型去查找一个符合条件的bean如果没找到则不注入,程序也不会报错;如果找到多个spring也不报错,但是也不完成注入,让这个属性等于null,比如你有个接口I,提供两个实现I1和I2都存在容器当中,然后在A类当中去注入I,并且指定自动注入模型为bytype那么这个时候会找到两个符合条件的bean,spring就迷茫了,注入哪个呢?spring哪个都不注入直接让这个属性为null而且也不出异常;但是如果找到了一个那么注入的时候会调用setter,如果没有提供setter就不会注入;
其次@Autowried是一个注解,加在属性上面spring,
spring会首先根据属性的类型去容器中找,如果没有找到在根据属性的名字找,找到了则注入,没有找到则异常,下图结果就是容器当中没有一个符合的类型和名字都不符合的则异常;
如果先根据类型找到了一个,那么直接注入,下图就是只有一个符合要求的类型能够完美注入;
如果先根据类型找到了多个,那么spring不会立马异常,而是根据名字再找去找,如果根据名字找到一个合理的则注入这个合理的,下图就是I1和I2都符合,但是spring最后却没有异常,是因为属性名字叫i1故而先类型在名字找到了合理的;
如果先根据类型找到了多个,那么spring不会立马异常,而是根据名字再找去找,如果根据名字还是没有找到那么时候spring会异常,下图就是两个i1和i2都符合,于是spring通过名字找i3也找不到,故而出异常;
以后如果再听到@Autowried是bytype请你纠正他,bytype是一种自动注入模型;@Autowried是一个注解,两个人没有关系,一点关系都没有;@Autowried讲道理算是手动装配;那么一个注解的类到底能不能开启自动装配呢?答案是可以的,但是需要你对spring比较精通,以后笔者再更新;
本文最早更新在我的博客上面,现在都会同步在头条号等自媒体平台上。转载请注明出处,欢迎各位关注!
哔哩哔哩有更多关于Spring的视频讲解,欢迎关注:子路玩Java