Spring框架浅析 -- IoC容器与Bean的生命周期

本文深入探讨Spring IoC容器的工作原理,包括Bean的注入、生命周期管理和依赖注入机制。详细分析了XML配置和注解配置两种方式,以及BeanFactory和ApplicationContext的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为什么要使用IoC

在介绍Spring IoC容器之前,先让我们来回顾一下什么是IoC,权威的概念如下:https://en.wikipedia.org/wiki/Inversion_of_control

简单说就是,不要再手写代码来维护类与类的依赖关系了,而是将依赖关系配置化,交由容器去解析、识别、处理,按照配置文件的方式,完成类及其依赖类的实例化、初始化等工作。

那么为什么要使用IoC呢?其实这个问题的实质是:编码式实例化Bean有哪些弊端,使用配置方式描述Bean的依赖关系并进行注入又有什么好处。

编码式实例化Bean并注入容器的弊端:

  1. 实现不够灵活,靠编码来实现Bean的依赖关系,一旦依赖关系发生变化(哪怕只是值发生变化),也需要更改代码,重新build工程,重新发布;
  2. 业务逻辑与可抽象出来的容器(即业务逻辑所处的环境)混在在一起,牵一发动全身,领域模型不够抽象,容器功能也难以沉淀,代码可复用性差。

而依靠配置描述Bean之间的依赖关系,可针对以上两点不足进行改进:

 

  1. 依赖关系配置化,与代码隔离开(如果使用xml文件方式的话);即使使用Annotation的方式,也可以将配置与代码的主体逻辑部分隔离开,职责更加明晰,改动起来也比较方便;
  2. 业务模块与通用功能可以抽象出来。使用容器实现通用功能,将复杂业务逻辑落在业务模块上,二者职责明晰,便于维护。

Spring IoC容器

IoC算是Spring最为核心的功能之一了,其重点在于对BeanFactory和ApplicationContext的理解。

BeanFactory可认为是Spring框架中管理Bean的容器,所有需要引入的Bean都会注册到BeanFactory中。使用者可通过getBean(String beanName)来根据Bean的名称获取到该Bean。

ApplicationContext继承了BeanFactory,顾名思义,ApplicationContext主要保有了应用的上下文信息,上下文中显然包含了注入的Bean的集合,此外还包含了应用名称、启动时间等。

那么Spring是如何在web容器启动的时候,获取启动事件,将BeanFactory启动,将Web容器中的上下文获取并存储在ApplicationContext中,之后又是如何将需要注入的Bean按照配置化的依赖注入信息,依次有序地注入到BeanFactory中,并对这些需要注入的Bean进行加工、处理(后边会提到BeanFactoryProcessor、BeanPostProcessor等组件的处理),实现对Bean的生命周期进行管理的呢?我将在下文一一进行分析和介绍。

Spring的配置及与Web容器的集成

以使用tomcat作为web容器的web工程为例,通常需要设置web.xml作为web配置文件。在web.xml中,通常会设置以下listener作为监听web容器启动/销毁事件的监听器。

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener
</listener>

ContextLoaderListener实现了javax.servlet.ServletContextListener接口,当web容器启动时,监听到javax.servlet.ServletContextEvent事件,通过事件获取ServletContext,作为Spring容器启动的主入口,启动Spring容器。

Spring IoC容器的启动过程

ContextLoaderListener作为主入口,创建ApplicationContext(默认为XmlWebApplicationContext),然后调用其refresh方法(本质上是调用其父类AbstractApplicationContext的refresh方法)。

在refresh方法的调用链中,最为重要的方法为:

  1. obtainFreshBeanFactory:对于xml配置方式,解析配置xml,将xml中配置的bean注入到BeanFactory中,并在此过程中解析它的依赖关系;对于Annotation配置方式(需要在xml配置文件中,使用context:component-scan设置扫描包路径),基于asm,对class文件进行解析,获取到符合条件的Bean,然后注入到BeanFactory中;
  2. invokeBeanFactoryPostProcessors:调用注册到BeanFactory中的BeanFactoryPostProcessor集合,依次调用其postProcessBeanFactory方法,在BeanFactory层级,在Bean的生命周期中,对Bean进行加工处理与修饰;
  3. finishBeanFactoryInitialization:将注册到BeanFactory中的Bean进行处理,完成其生命周期中的实例化、初始化,以及调用BeanPostProcessor等容器级别组件,对Bean进行适当的加工处理与修饰

针对这三个方法,我们将其分为两大类:获取BeanFactory(即1)和Bean的创建、实例化与修饰(即2,3),在下文中,我们将一一加以介绍。

获取BeanFactory

Bean的配置方式主要有两种:基于xml配置文件的配置方式和基于Annotation的配置方式。两种方式下,获取BeanFactory的过程略有不同。

1. xml方式注入

先看基于xml配置文件的配置方式,Bean是如何注入到容器的。

obtainFreshBeanFactory方法调用链中主要进行了如下操作:

  1. 通过AbstractRefreshableApplicationContext的createBeanFactory创建出beanFactory,这是一个DefaultListableBeanFactory,可认为是BeanFactory的经典默认实现版;
  2. 通过AbstractRefreshableApplicationContext的customizeBeanFactory方法,对上一步创建出来的BeanFactory进行修饰,修饰过程中设置了两个非常重要的属性:allowBeanDefinitionOverriding和allowCircularReferences。顾名思义,allowBeanDefinitionOverriding用来标识在BeanFactory中,面对先后两个同名Bean,是否允许进行覆盖;而allowCircularReferences标识了是否允许有限度地解决环形依赖的问题(下文中将介绍何为有限度地支持);
  3. 通过AbstractRefreshableApplicationContext的loadBeanDefinitions加载配置文件中配置的Bean,将其包装成为BeanDefinition。这一步应该算是最为重要的一步了,调用链也很长,但我们只需要抓住其中几个比较重要的点:从上层开始,层层调用到DefaultDocumentLoader的createDocumentBuilder方法,创建DocumentBuilder。也就是说,spring是通过dom来实现xml文件的解析与xml结构的加载,所以我个人其实并不建议在xml配置中配置得过于复杂,否则加载速度会比较慢。
  4. 执行完xml文件的加载之后,回到XmlBeanDefinitionReader的registerBeanDefinitions方法,根据上一步解析并加载后的org.w3c.dom.Document,对Bean进行注入。顺着调用链,层层向下到XmlBeanDefinitionReader的doRegisterBeanDefinitions方法,再到DefaultBeanDefinitionDocumentReader类的parseBeanDefinitions方法中,即可看到会根据XML element是否为默认namespace,来进行处理。对于默认namespace的,使用parseDefaultElement,对于xml配置方式,对Bean的解析和注入基本都在默认namespace的范围内;对于自定义namespace的,使用parseCustomElement(对于Annotation的配置方式,需要用到context namespace的解析器)。这一步之后就基本上完成了obtainFreshBeanFactory的调用链。

针对刚才所述的第四步,我们重点进行一下分析。

首先看默认命名空间的情况,会处理四种情况:import, alias, bean, beans,可以结合xml配置文件来看。

import

import的情况通常是在xml配置文件有这样的配置:

<import resource="xxxx.xml"/>

其作用在于,在主配置xml文件中,引入分xml文件。通常,我们会将jdbc的xml配置文件、rpc的xml配置文件、cache的xml配置文件、mq的xml配置文件单列成一个分xml文件,这样职责比较单一,整个工程组织也比较明晰。在主配置xml文件中,比如spring-root.xml中,需要import这些分xml文件,以达到将分xml文件中定义的bean也注入到BeanFactory中的效果。

alias

alias的情况通常是在xml配置文件有这样的配置:

<alias name="xxx" alias="yyy"/>

其作用在于,对于name为xxx的bean,其具备了别名yyy。不过,在实际工作中,我并没有这样使用过。

bean

bean的情况比较常见,简化版配置如下:

<bean id="xxx" class="yyy"/>

建议使用id属性来标识Bean。

此外,在此还可以设置bean的scope属性(作用域,singleton或prototype),lazy-init属性(是否懒加载),init-method/destroy-method(初始化方法/销毁方法)等属性。不过个人比较建议在xml配置中,仅进行简单的配置。

beans

重复上述过程,解析嵌套在其内部的标签。

2. 注解方式注入

与上述xml方式注入bean相比,注解方式注入bean仅在刚才梳理的obtainFreshBeanFactory方法调用链的第四步有所不同,即需要根据自定义namespace的方式进行解析。

对于注解方式注入bean而言,通常在spring配置文件中需要配上这么一段:

<context:component-scan base-package="xx">
  <context:include-filter type="yy" expression="..."/>
  <context:exclude-filter type="zz" expression="..."/>
</context>

通过这一段,我们知道这实际上是使用了context命名空间,在对应的spring-context-xx.xx.xx.jar包的META-INF文件夹下,不难发现context命名空间下对应的NamespaceHandler为org.springframework.context.config.ContextNamespaceHandler。

再到ContextNamespaceHanlder的init方法下,能够看到对于component-scan、annotation-config等属性,均注册了响应的BeanDefinitionParser。component-scan属性注册的BeanDefinitionParser为ComponentScanBeanDefinitionParser。

好了,现在我们回到obtainFreshBeanFactory方法调用链的第四步,回到DefaultBeanDefinitionDocumentReader的parseBeanDefinitions方法,对于自定义namespace,需要调用BeanDefinitionParserDelegate的parseCustomElement方法,顺着其调用链深入下去,可以看到以下这段代码:

public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
		String namespaceUri = getNamespaceURI(ele);
		NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
		if (handler == null) {
			error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
			return null;
		}
		return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
	}

从这段代码中,我们看到,首先通过获取到的context的namespaceUri,获取到NamespaceHandler,也就是刚才谈到的ContextNamespaceHandler,然后调用其parse方法,根据component-scan属性获取到对应的BeanDefinitionParser,即ComponentScanBeanDefinitionParser,进而调用其parse方法,完成以下几步:

  1. 配置Scanner,将xml文件中配置的base-package、include-filter和exclude-filter设置到ClassPathBeanDefinitionScanner中,当然还有默认的识别@Component的AnnotationTypeFilter。根据这些属性,我们即可知道Spring将扫描哪些类,从而将符合条件的Bean注入到BeanFactory中。此外,我们也明白了为何@Controller(web层),@Service(服务层),@Repository(数据层)标注的类能够被注入到BeanFactory中,原因就是他们都是Component,采用不同的标注只是为了显式区分各自的功能罢了;
  2. 调用scan.doScan,使用asm技术,从class文件中解析出类的元信息,包括其属性、方法、标注等,根据上一步设置的限制条件,获取符合条件的Bean;
  3. 注册BeanPostProcessor,用于在Bean创建、初始化过程中,对bean进行相应的修饰和加工,重要的BeanPostProcessor有:AutowiredAnnotationBeanPostProcessor,CommonAnnotationBeanPostProcessor,RequiredAnnotationBeanPostProcessor等。在下文,我们会对其进行详细介绍。

3. 小节

以上介绍了xml方式注入和注解注入两种注入方式,那么在实际使用过程中,我们应该如何进行选择呢?

其实在技术上,很难会有一个放之四海而皆准的金标准,也很难说哪种技术方案一定好,哪种技术方案一定不好,很多时候,都需要结合实际使用情况,结合技术方案的特点,根据业务需求进行权衡,寻找出最为适合的方案。

xml方式注入的优点是配置-代码分离,但是缺点是不如注解方式方便;

注解方式注入的优点是可以自动装配(Autowired),无需层层描述bean与bean的依赖关系,但是缺点是配置与代码在一起。

因此,在日常使用中,我通常会优先选择注解方式注入业务相关bean,而把框架相关bean或者说与业务无关的bean,放在xml配置文件中。因为业务相关bean会随着业务的发展不断演进,配置与代码在一起无伤大雅,且依赖关系较为复杂,使用注解方式注入,可以很好地利用其自动装配功能。对于框架相关bean,依赖关系可能还算简单,且会被多处业务代码所引用,当升级配置时(如中心注册服务器地址,或者是资源池大小),如果混在各处业务代码中,将是一种灾难。

 

Bean的创建、初始化与修饰

在上文中,大致介绍了如何解析配置文件(或解析代码中的Annotation),将解析得到的Bean注入到BeanFactory中(其实也就是容器中)。但是这些Bean还没有被实例化、初始化,也没有进行必要的加工修饰,还不能对外提供服务。本节将介绍Spring容器是如何将这些Bean创建、初始化的,如何利用容器组件(BeanFactoryPostProcessor,BeanPostProcessor)对Bean进行修饰、加工,使其具备对外服务的能力。

我们还是回到上文中所述的Spring IoC容器的启动过程,在执行完获取BeanFactory的方法之后,将执行以下两个方法:

 

  1. invokeBeanFactoryPostProcessors
  2. finishBeanFactoryInitialization

在方法一中,将调用注册到BeanFactory中的BeanFactoryPostProcessor集合,依次调用其postProcessBeanFactory方法,在BeanFactory层级,在Bean的生命周期中,对Bean进行加工处理与修饰。我们就不展开介绍了,感兴趣的同学可以深入调用链看一下Spring容器都有哪些BeanFactoryPostProcessor,又都完成了哪些操作。

在方法二中,将注册到BeanFactory中的Bean进行处理,完成其生命周期中的实例化、初始化,以及调用BeanPostProcessor等容器级别组件,对Bean进行适当的加工处理与修饰。这将是本节介绍的重点,我们对其进行展开。

 

  1. 沿着DefaultLisableBeanFactory的preInstantiateSingletons方法,一路向下到getBean方法,再到doGetBean方法。以作用域为Singleton的Bean为例,进入到AbstractBeanFactory类的getSingleton方法,再到AbstractAutowiredCapableBeanFactory的createBean方法。注意,这个方法和其调用链内的doCreateBean方法将是比较重要的两个方法;
  2. Bean实例化前的加工修饰:在AbstractAutowiredCapableBeanFactory的createBean方法中调用了resolveBeforeInstantiation方法,进而获取注册在BeanFactory中的InstantiationAwareBeanPostProcessor集合,并以此调用其postProcessBeforeInstantiation方法,在Bean实例化前进行加工修饰工作;
  3. 实例化Bean:在AbstractAutowiredCapableBeanFactory的createBeanInstance方法中,进而调用getInstantiationStrategy().instantiate(mbd, beanName, parent),利用JAVA反射,实例化Bean;
  4. 在Bean初始化之前,调用后处理器设置属性:AbstractAutowiredCapableBeanFactory的populateBean方法中,调用InstantiationAwareBeanPostProcessor的postProcessPropertyValues方法,设置属性。典型的InstantiationAwareBeanPostProcessor有AutowiredAnnotationBeanPostProcessor和CommonAnnotationBeanPostProcessor,分别处理bean中,被@Autowired和@Resource注解的属性Bean,对于这些被依赖的Bean,调用getBean方法获取其已可对外提供服务的实例,如果还没有达到可用状态,则尝试创建。这里边就涉及到环形依赖的问题,在小节中会给出介绍;
  5. 在Bean初始化前,调用后处理器,对Bean进行加工处理:AbstractAutowiredCapableBeanFactory的initializeBean方法中,调用BeanPostProcessor的postProcessBeforeInitialization方法,在bean初始化前进行准备工作。bean中标注了@PostConstruct的函数就是在此时被调用的,处理它的是InitDestroyAnnotationBeanPostProcessor。
  6. 调用bean的初始化方法;
  7. Bean初始化之后,调用后处理器,对Bean进行加工处理:调用BeanPostProcessor的postProcessAfterInitialization方法,在bean初始化后,对Bean进行加工处理。

至此,Bean已经被加工完毕,可对外提供服务。当需要销毁Bean的时候,调用Bean的destroy方法,对Bean进行销毁。

 

小节

截止到这里,我们已经大致介绍完了Spring IoC容器是如何启动的,如何将Bean包装成BeanDefinition,又是如何将其注入到BeanFactory中,以及如何在其生命周期中,基于容器内的多种组件对其进行加工、修饰,使其具备了对外提供服务的能力。

相信看完本文之后,你已经能够回答以下问题:

 

  1. Bean的scope主要有哪些:我们只考虑主要的,Singleton和Prototype,即单例与非单例;
  2. Spring容器中提供了哪些组件,能够在Bean的生命周期中起到作用:BeanFactoryPostProcessor, BeanPostProcessor;
  3. 被@Autowired标注的Bean是如何作为属性,注入到引用它的Bean中的:AutowiredAnnotationBeanPostProcessor在Bean初始化之前,调用postProcessPropertyValues方法,获取Bean中被标注了@Autowired的属性,然后将这些属性对应的Bean按类型进行获取,实例化、初始化,使用JAVA的反射,将已经可以对外提供服务的Bean设置到属性上
  4. 被@Resource标注的Bean是如何作为属性,注入到引用它的Bean中的:CommonAnnotationBeanPostProcessor在Bean初始化之前,调用postProcessPropertyValues方法,获取Bean中被标注了@Resource的属性,然后按照name(没有设置name就按照type),进行获取,实例化、初始化,最后使用JAVA的反射,将其设置到属性上
  5. 被@PostConstruct标注的方法,是在何时被调用的:CommonAnnotationBeanPostProcessor在Bean初始化之前,调用postProcessBeforeInitialization方法(实质上该方法是在其父类里定义的)。在方法内部,调用@PostConstruct标注的方法
  6. Spring容器对于环形依赖(circle reference)是如何处理的:分两种情况,对于作用域为prototype的bean A,经过一系列依赖关系,引用了作用域为prototype的bean A,抛错,此时无法解决环形依赖的问题;对于作用域为singleton的bean A,经过一系列的依赖关系,引用了作用域为singleton的bean A,由于当bean A实例化的时候,向DefaultSingletonBeanRegistry的singletonsCurrentlyInCreation属性中添加过bean A的beanName,且向earlySingletonObjects属性(key-beanName, value-bean object)中添加过bean A的早期半成品,所以可在此直接返回bean A的早期半成品,从而结束环形依赖。我理解这应该是最大限度上对环形依赖的解决方案了。
  7. Bean的生命周期是怎样的:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值