Spring IOC Container
All the notes are from Spring Framework 5 Doc.
一、Introduction to the Spring IOC Container and Beans
org.springframework.beans 和 org.springframework.context两个包是Spring IOC容器的基础。
BeanFactory接口提供了配置框架和基本功能,ApplicationContext接口则在此之上添加了更多满足企业应用规范的功能。
在Spring中,构成应用程序主干并由SpringIoC容器管理的"对象"称为Bean。Spring负责Bean的实例化、装配和管理(整个生命周期)。
所有Bean和它们之间的依赖关系,都反应在容器所使用的配置元数据(Configuration Metadata)中。
二、Conainer Overview
org.springframework.context.ApplicationContext代表了Spring IOC Container,因此它负责实例化、配置和装配Bean。
容器通过读取配置元数据(Configuration Metadata)获取关于要实例化、配置和组装哪些对象的说明。而配置元数据可以是XML文件、注解以及Java代码。通过它们,可以很好地表示组成应用程序的对象以及这些对象之间的丰富相互依赖关系。
如何通过这三种方式实例化ApplicationContext,在之前已经有提及过。
官方文档提到,在大多数应用程序场景中,不需要显式地用代码实例化Spring IoC容器的一个或多个实例。例如在Web应用中,最简单的做法就是在web.xml中通过ContextLoaderListener注册一个ApplicationContext。
三、Bean OverView
四、Dependencies
依赖注入(Dependency injection)是这样一个过程:
对象只通过下面的三种方式定义他们的依赖关系(即与自己一起协作的其他对象)
- 构造函数的参数;
- 工厂方法的参数;
- 在对象的实例通过构造函数或者工厂方法返回后,注入其属性;
然后,Spring Container在创建bean时便注入这些依赖。
使用DI原则,代码更简洁,当对象具有依赖关系时,解耦更有效。
对象不需要查找依赖项,也不知道依赖项的位置或类。因此,您的类变得更容易测试,特别是当它依赖于接口或抽象基类时。
DI主要有两种形式:
- 基于构造函数;
- 基于Setter函数——基于setter的DI是在调用无参构造函数或无参静态工厂方法来实例化bean之后,在bean上调用setter方法来完成的。
对于这两种方式,有个最佳实践:
对强制依赖项使用构造函数,为可选依赖项使用setter方法或配置方法( configuration methods)。注意,在setter方法上使用@Required注释可以使属性成为必需的依赖项。
Spring Team通常提倡构造函数注入,因为它允许您将应用程序组件实现为不可变对象,并确保所需的依赖项不是空的。此外,通过构造函数注入的组件总是以完全初始化后的状态返回给客户端的(调用)代码。
顺便提一句,构造函数参数过多是一种糟糕的代码,这意味着类可能承担了太多的责任。对此应该进行重构,以更好分离所关注的点。
而Setter注入应该主要用于可选的依赖项,这些依赖项可以在类中分配合理的默认值。不然的话,在任何使用该依赖项的地方都需要进行非空检查。Setter注入的一个优势是setter方法使该类的对象能够在以后重新进行配置或重新注入。
特殊情况下,例如,如果第三方类不公开任何setter方法,则构造函数注入可能是惟一可用的DI形式。
IOC Container是完成DI的步骤:
- 首先,使用描述所有bean的配置元数据来创建和初始化ApplicationContext。配置元数据可以由XML、Java代码或注释指定。
- 然后,对于每个bean,其依赖项以属性、构造函数参数或静态工厂方法的参数的形式表示(如果您使用它而不是普通的构造函数)。当bean实际被创建时,这些依赖项将提供给bean。
- 接着,每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个bean的引用。
- 最后,每个作为值的属性或构造函数参数都从其指定的格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring可以将以字符串格式提供的值转换为所有内置类型,例如int、long、string、boolean等等。
事实上,Spring的Container在被创建时就会验证每个bean的配置,尽管bean的属性直到bean在被创建时才被设置。
但是singleton-scoped和pre-instantiated (默认情况)的bean都是在Container创建时被创建。否在,bean都是在被请求时才被创建。
一个bean的创建可能会引起一系列的bean被创建,因为它的依赖项,以及依赖项的依赖项都会被创建和分配。
引申:关于循环依赖
如果主要使用构造函数注入,则可能会产生一个无法解决的“循环依赖”的场景。
例如:Class A requires an instance of class B through constructor injection, and class B requires an instance of class A through constructor injection. If you configure beans for classes A and B to be injected into each other, the Spring IoC container detects this circular reference at runtime, and throws a BeanCurrentlyInCreationException
.
一种解决方案是:让其中的一些类使用setter方法进行依赖注入。或者,全都只使用setter方法。换句话说,尽管不建议这样做,但可以使用setter注入解决循环依赖的问题。
在创建bean时,Spring会尽可能晚地去设置其属性以及解决其依赖项。这意味着,在你请求一个对象时,如果在创建对象或解决其依赖项时出现问题,正确加载的Spring容器可能稍晚才会生成异常。这也是为什么ApplicationContext的实现类默认会预先初始化单例的bean。在这些bean被实际请求前,花费一些前期时间和内存来创建他们,可以保证在ApplicationContext创建时发现一些配置问题。
当然你也可以覆盖这种默认的行为,让singleton bean晚点被初始化。
在不存在依赖循坏的情况下,如果Bean A依赖于Bean B,那么Spring会在完全初始化Bean B后,再进行将其注入A中。
五、Bean Scopes
Spring框架支持六个bean的作用域,而在web相关的ApplicaitonContext中,只支持其中四个。
还可以创建自定义scope。
以下是六种作用域,后四个是web相关:
singleton---一个bean的定义,在一个IOC容器中只会有它的一个实例。这意味着每次对这个容器中这个bean的请求,返回的都是同一个实例。
prototype---一个bean的定义,在一个IOC容器中可以有多个实例。每次的请求,都会引起同一个bean的新的实例的创建。
request--- bean作用于HTTP request的生命周期,而每个HTTP request都拥有它自己的实例bean,这个bean的声明周期也随同request的生命周期。只在web相关的Spring ApplicationContext中才有意义。
session--- bean作用于HTTP session的生命周期,每个HTTP session都有自己的实例bean。
application--- Scopes a single bean definition to the lifecycle of a ServletContext.
websocket---Scopes a single bean definition to the lifecycle of a WebSocket。
对于singleton和prototype的使用,有一个原则:应该对所有“有状态”的bean使用prototype作用域,对“无状态”的bean使用单例作用域。
数据访问对象(DAO)通常不配置为原型,因为典型的DAO不包含任何会话状态。
与其他作用域不同,Spring并不管理prototype bean的完整生命周期。
容器在初始化并配置好一个prototype对象后,就把它交给客户端,此后不在有这个prototype对象实例的任何记录。因此,尽管对所有对象都调用初始化生命周期回调方法,而不管作用域如何,但在prototype 的情况下,配置的销毁生命周期回调方法不会被调用。而客户端代码必须自己清理prototype实例对象并且释放它所占用的资源。Spring也为此提供了一个专用的容器,来释放被这些prototype-scoped bean所占用的资源(try using a custom bean post-processor, which holds a reference to beans that need to be cleaned up)。
某种程度上来说,Spring容器在prototype bean中的角色只是是Java new操作符的替代品。此后的所有生命周期管理都必须由客户端来处理。