深入剖析 Spring 中的构造器注入


点击上方 "程序员小乐" ,关注公众号

8点20分,第一时间与你相约

每日英文

Sometimes, I’d rather feel nothing. It’s better. It’s easier.

有时候我真希望自己没有感觉,那就好过得多了。


每日掏心话

也许时间会夺去你那令人妒忌的容颜,却无法抢走我爱你坚定的信念,因为我有一种无穷而又温柔的力量,它就是一心一意!


来自:Static_lin | 责编:乐乐

链接:blog.csdn.net/qq_41737716

640?wx_fmt=png

图片来自网络



   正文   



关于更多Spring深度分析的文章,可以点击这里访问。

https://blog.csdn.net/qq_41737716/article/details/84936140

在往期文章中我没有提到在Spring中是如何将Bean注入到构造器,其实这个过程发生在实例化Bean时期,由AbstractAutowireCapableBeanFactory的createBeanInstance方法来做的。

若想快速得到结论,可以参照文末的总结。

1、示例

先来看一个例子,看看什么是构造器注入。

这里我写了一个类,分别有两个构造器,一个是注入一个Bean的构造器,一个是注入两个Bean的构造器:

public class ConstructorAutowiredTest {	
    private User user;	
    private Role role;	
    public ConstructorAutowiredTest() {	
    }	
    public ConstructorAutowiredTest(User user) {	
        this.user = user;	
    }	
    public ConstructorAutowiredTest(User user, Role role) {	
        this.user = user;	
        this.role = role;	
    }	
    public void test(){	
        System.out.println("user: "+user);	
        System.out.println("role: "+role);	
    }	
}

Model类User与Role我就不贴代码了,分别是有两个变量,一个id,一个name。

然后就是Spring的配置文件context.xml:

<?xml version="1.0" encoding="UTF-8" ?>	
<beans xmlns="http://www.springframework.org/schema/beans"	
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"	
       xmlns:context="http://www.springframework.org/schema/context"	
       xsi:schemaLocation="http://www.springframework.org/schema/beans	
       http://www.springframework.org/schema/beans/spring-beans-2.0.xsd	
       http://www.springframework.org/schema/context	
        http://www.springframework.org/schema/context/spring-context-4.0.xsd ">	
    <!-- 使Spring关注Annotation -->	
    <context:annotation-config/>	
    <bean class="com.mytest.demo.model.Role" id="role">	
        <property name="name" value="testRole"/>	
        <property name="id" value="2"/>	
    </bean>	
    <bean class="com.mytest.demo.model.User" id="user">	
        <property name="id" value="1"/>	
        <property name="name" value="testUser"/>	
    </bean>	
    <bean class="com.mytest.demo.autowired.ConstructorAutowiredTest" id="test"/>	
</beans>

注意,如果需要使用构造器注入,需要 此自定义标签开启(关于自定义标签,在本人往期的Spring系列中有详细介绍),具体作用后面再作分析。

那么,该类三个构造器,Spring会使用哪个构造器初始化ConstructorAutowiredTest这个Bean呢?写个测试便知:

public class TestBeanAutowiredConstructor {	
    public static void main(String[] args) {	
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext();	
        context.setConfigLocation("context.xml");	
        context.refresh();	
        ConstructorAutowiredTest test = (ConstructorAutowiredTest) context.getBean("test");	
        test.test();	
    }	
}

执行test方法,控制台打印:

640?wx_fmt=png

从这里我们可以看出来,此时三个构造器中Spring使用了无参构造器。

我们换一个方式,将无参构造器注释掉,看看Spring到底会使用哪个构造器呢?同样执行test方法测试,控制台打印:

640?wx_fmt=png

此时控制台报错,大致意思是Bean的实例化失败了,没有无参的构造器方法调用。此时有个疑问,明明构造器中的参数都是Bean,为什么不能初始化,一定要使用无参的构造器呢?是否是因为有两个构造器的原因?此时我们再注释掉任意一个构造函数,使测试类只有一个带参构造函数:

//    public ConstructorAutowiredTest() {	
//    }	
    public ConstructorAutowiredTest(User user) {	
        this.user = user;	
    }	
//    public ConstructorAutowiredTest(User user, Role role) {	
//        this.user = user;	
//        this.role = role;	
//    }

再次运行测试类,控制台打印:

640?wx_fmt=png

如果是注释掉第二个构造函数,则结果是两个对象都有。从这里我们可以看出,如果只有一个构造器,且参数为IOC容器中的Bean,将会执行自动注入。这里又有一个疑问,这也太不科学了吧,强制用户一定只能写一个构造器?这时我们猜想@Autowired注解是否能解决这种问题?来试试吧。我们将构造器全部解除注释,将第三个构造器打上@Autowired注解:

public ConstructorAutowiredTest() {	
}	
public ConstructorAutowiredTest(User user) {	
    this.user = user;	
}	
@Autowired	
public ConstructorAutowiredTest(User user, Role role) {	
    this.user = user;	
    this.role = role;	
}

运行测试,控制台打印:

640?wx_fmt=png

不出所料,@Autowired注解可以解决这种问题,此时Spring将使用有注解的构造函数进行Bean的初始化。那么,如果有两个@Autowired注解呢?结果肯定是报错,因为@Autowired的默认属性required是为true的,也就是说两个required=true的构造器,Spring不知道使用哪一个。但如果是这样写的话:

public ConstructorAutowiredTest() {	
}	
@Autowired(required = false)	
public ConstructorAutowiredTest(User user) {	
    this.user = user;	
}	
@Autowired(required = false)	
public ConstructorAutowiredTest(User user, Role role) {	
    this.user = user;	
    this.role = role;	
}

结果是怎样的呢?看看控制台打印:

640?wx_fmt=png

使用参数最多的那一个构造器来初始化Bean。又如果两个有参构造器顺序调换又是怎样的呢?一个required为false一个为true,结果又是怎样的呢?这里直接给出答案,顺序调换依然使用多参数构造器,并且required只要有一个true就会报错。有兴趣的读者可以自己试试,下面将深入源码分析构造器注入的过程,相信上述所有疑问都能得到解答。

疑问点小结

从现象看本质,我们从上面的例子中,大致可以得到以下几个疑问:

  • 为什么写三个构造器(含有无参构造器),并且没有@Autowired注解,Spring总是使用无参构造器实例化Bean?

  • 为什么注释掉两个构造器,留下一个有参构造器,并且没有@Autowired注解,Spring将会使用构造器注入Bean的方式初始化Bean?

  • 为什么写三个构造器,并且在其中一个构造器上打上@Autowired注解,就可以正常注入构造器?并且两个@Autowired注解就会报错,一定需要在所有@Autowired中的required都加上false即可正常初始化等等?

或许有一些没有提到的疑问点,但大致就这么多吧,举多了也没用,因为在下面深入源码的分析中读者若是可以理解,关于此类的一系列问题都将可以自己思考得出结果,得到可以举一反三的能力。

2、依赖注入伊始

在开头,我们有提到,如果需要构造器注入的功能的话,我们需要在xml配置中写下这样一段代码:

<!-- 使Spring关注Annotation -->	
<context:annotation-config/>

如果有看过本人自定义标签或是有自定义标签的基础的读者,第一反应应该是先看自定义标签的context对应的命名空间是哪个:

xmlns:context="http://www.springframework.org/schema/context"

我们全局搜索此命名空间(http后需要加一个斜杆符号\)得到一个spring.handlers文件:

http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler

这里说明了此命名空间对应的NamespaceHandler,进入此类看看其init方法:

public void init() {	
    registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());	
    registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());	
    registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());	
    registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());	
    registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());	
    registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());	
    registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());	
    registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());	
}

我们的自定义标签是叫作 “annotation-config" ,所以对应的解析器是AnnotationConfigBeanDefinitionParser这个类,进入这个类的parse方法:

public BeanDefinition parse(Element element, ParserContext parserContext) {	
    Object source = parserContext.extractSource(element);	
    // Obtain bean definitions for all relevant BeanPostProcessors.	
    //定义一系列BeanDefinition,放入一个Set集合中	
    Set<BeanDefinitionHolder> processorDefinitions =	
        AnnotationConfigUtils.registerAnnotationConfigProcessors(parserContext.getRegistry(), source);	
   //注册一系列BeanDefinition,此处略过..	
}

这里只需要关注registerAnnotationConfigProcessors方法,看看到底需要注册哪些Bean:

public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(	
    BeanDefinitionRegistry registry, @Nullable Object source) {	
    //将registry对象转型成beanFactroy	
    DefaultListableBeanFactory beanFactory = unwrapDefaultListableBeanFactory(registry);	
    Set<BeanDefinitionHolder> beanDefs = new LinkedHashSet<>(8);	
    //判断容器中是否已经存在名为AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME的Bean	
    if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {	
        //若是不存在,创建一个	
        RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);	
        def.setSource(source);	
        beanDefs.add(registerPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));	
    }	
    //中途还添加了一系列BeanDefinition,不是本文讨论范围,略过...	
    return beanDefs;	
}

到这里我们可以知道,此自定义标签注册了一个AutowiredAnnotationBeanPostProcessor类的Bean到IOC容器。那么此类是干什么用的呢?

3、初始化Bean

我们将思路转到IOC容器初始化Bean的流程中来,在getBean方法获取一个Bean的时候,IOC容器才开始将Bean进行初始化,此时会先实例化一个Bean,然后再对Bean进行依赖注入,然后执行一系列初始化的方法,完成Bean的整个初始化过程。而本文讨论的构造器注入,则是在实例化Bean的过程中进行的。

在AbstractAutowireCapableBeanFactory类中的doCreateBean方法获取Bean的开始,将调用createBeanInstance方法进行Bean的实例化(选择Bean使用哪个构造器实例化):

protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {	
    //略过一些与本文无关的其他实例化的方式的代码...	
    // Candidate constructors for autowiring?	
    // 查找是否存在候选的自动注入构造器	
    Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);	
    if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||	
        mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {	
        //使用注入的方式调用构造器实例化	
        return autowireConstructor(beanName, mbd, ctors, args);	
    }	
    // No special handling: simply use no-arg constructor.	
    //使用无参构造器实例化(利用反射newInstance)	
    return instantiateBean(beanName, mbd);	
}

到这里我们可以知道,determineConstructorsFromBeanPostProcessors方法将选择是否有适合的自动注入构造器,如果没有,将使用无参构造器实例化,关键就在这个方法中,是如何判断哪些构造器使用自动注入的呢:

protected Constructor<?>[] determineConstructorsFromBeanPostProcessors(@Nullable Class<?> beanClass, String beanName)	
    throws BeansException {	
    //容器中若存在InstantiationAwareBeanPostProcessors	
    if (beanClass != null && hasInstantiationAwareBeanPostProcessors()) {	
        //遍历此BeanPostProcessors	
        for (BeanPostProcessor bp : getBeanPostProcessors()) {	
            //只使用SmartInstantiationAwareBeanPostProcessor类型	
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {	
                SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;	
                Constructor<?>[] ctors = ibp.determineCandidateConstructors(beanClass, beanName);	
                if (ctors != null) {	
                    return ctors;	
                }	
            }	
        }	
    }	
    return null;	
}

这段代码告诉我们,这里会使用SmartInstantiationAwareBeanPostProcessor类型的BeanPostProcessor进行判断,我们回顾一下上面的依赖注入伊始的时候我们说的自定义标签注册的类的结构:

640?wx_fmt=png

有看过本人关于Sprng扩展篇的文章或是有Spring扩展点基础的读者,应该可以知道,若是注册一个BeanPostProcessor到IOC容器中,在AbstractApplicationContext中的refresh方法会对这些类型的Bean进行处理,存放在一个集合,此时getBeanPostProcessors方法就可以获取到所有BeanPostProcessor集合,遍历集合,便可以调用到我们自定义标签中注册的这个类型的Bean。

当然,SmartInstantiationAwareBeanPostProcessor类型的Bean有很多,但依赖注入是使用上述这个Bean来完成的。回到主线,下面会调用它的determineCandidateConstructors方法进行查找对应构造器(核心方法):

 
   

从这段核心代码我们可以看出几个要点:

在没有@Autowired注解的情况下:
  • 无参构造器将直接加入defaultConstructor集合中。

  • 在构造器数量只有一个且有参数时,此唯一有参构造器将加入candidateConstructors集合中。

  • 在构造器数量有两个的时候,并且存在无参构造器,将defaultConstructor(第一条的无参构造器)放入candidateConstructors集合中。

  • 在构造器数量大于两个,并且存在无参构造器的情况下,将返回一个空的candidateConstructors集合,也就是没有找到构造器。

在有@Autowired注解的情况下:
  • 判断required属性:

true:先判断requiredConstructor集合是否为空,若不为空则代表之前已经有一个required=true的构造器了,两个true将抛出异常,再判断candidates集合是否为空,若不为空则表示之前已经有一个打了注解的构造器,此时required又是true,抛出异常。若两者都不为空将放入requiredConstructor集合中,再放入candidates集合中。

false:直接放入candidates集合中。

  • 判断requiredConstructor集合是否为空(是否存在required=true的构造器),若没有,将默认构造器也放入candidates集合中。

  • 最后将上述candidates赋值给最终返回的candidateConstructors集合。

4、总结

综上所述,我们可以回答开篇疑问点小结所总结的一系列问题了:

为什么写三个构造器(含有无参构造器),并且没有@Autowired注解,Spring总是使用无参构造器实例化Bean?

答:参照没有注解的处理方式:若构造器只有两个,且存在无参构造器,将直接使用无参构造器初始化。若大于两个构造器,将返回一个空集合,也就是没有找到合适的构造器,那么参照第三节初始化Bean的第一段代码createBeanInstance方法的末尾,将会使用无参构造器进行实例化。这也就解答了为什么没有注解,Spring总是会使用无参的构造器进行实例化Bean,并且此时若没有无参构造器会抛出异常,实例化Bean失败。

为什么注释掉两个构造器,留下一个有参构造器,并且没有@Autowired注解,Spring将会使用构造器注入Bean的方式初始化Bean?

答:参照没有注解的处理方式:构造器只有一个且有参数时,将会把此构造器作为适用的构造器返回出去,使用此构造器进行实例化,参数自然会从IOC中获取Bean进行注入。

为什么写三个构造器,并且在其中一个构造器上打上@Autowired注解,就可以正常注入构造器?

答:参照有注解的处理方式:在最后判断candidates适用的构造器集合是否为空时,若有注解,此集合当然不为空,且required=true,也不会将默认构造器集合defaultConstructor加入candidates集合中,最终返回的是candidates集合的数据,也就是这唯一一个打了注解的构造器,所以最终使用此打了注解的构造器进行实例化。

两个@Autowired注解就会报错,一定需要在所有@Autowired中的required都加上false即可正常初始化?

答:参照有注解的处理方式:当打了两个@Autowired注解,也就是两个required都为true,将会抛出异常,若是一个为true,一个为false,也将会抛出异常,无论顺序,因为有两层的判断,一个是requiredConstructor集合是否为空的判断,一个是candidates集合为空的判断,若两个构造器的required属性都为false,不会进行上述判断,直接放入candidates集合中,并且在下面的判断中会将defaultConstructor加入到candidates集合中,也就是candidates集合有三个构造器,作为结果返回。

至于第四条结论,返回的构造器若有三个,Spring将如何判断使用哪一个构造器呢?在后面Spring会遍历三个构造器,依次判断参数是否是Spring的Bean(是否被IOC容器管理),若参数不是Bean,将跳过判断下一个构造器,也就是说,例如上述两个参数的构造器其中一个参数不是Bean,将判断一个参数的构造器,若此参数是Bean,使用一个参数的构造器实例化,若此参数不是Bean,将使用无参构造器实例化。

也就是说,若使用@Autowired注解进行构造器注入,required属性都设置为false的话,将避免无Bean注入的异常,使用无参构造器正常实例化。若两个参数都是Bean,则就直接使用两个参数的构造器进行实例化并获取对应Bean注入构造器。

在这里最后说一点,从上面可以看出,若想使用构造器注入功能,最好将要注入的构造器都打上@Autowired注解(若有多个需要注入的构造器,将所有@Autowired中required属性都设置为false),若有多个构造器,只有一个构造器需要注入,将这个构造器打上@Autowired注解即可,不用设置required属性。

如果不打注解也是可以使用构造器注入功能的,但构造器数量只能为1,且代码可读性较差,读代码的人并不知道你这里使用了构造器注入的方式,所以这里我建议若使用构造器注入打上@Autowired注解会比较好一点。

本文讲述了从自定义注册Bean开始,到解析IOC容器初始化Bean的判断的一系列过程,从现象看本质,分析了Spring中的构造器注入的原理,并且分析了各种情况,相信理解了的读者将来遇到这类的别的问题可以独立思考出答案。

欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,学习能力的提升上有新的认识,欢迎转发分享给更多人。


欢迎各位读者加入程序员小乐技术群,在公众号后台回复“加群”或者“学习”即可。

640?wx_fmt=png


猜你还想看


阿里、腾讯、百度、华为、京东最新面试题汇集

当我们遇到100亿次请求?该如何设计后端架构?

高中就开始学的正态分布,原来如此重要

一个合格的技术面试官是怎么样的?

百度地震了,也许早晚的事

Google 程序员有哪些高效的编程习惯?

640?wx_fmt=png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值