IOC

这几天一直在看IOC的相关内容,无奈IOC实在内容太多,自己感觉有点消化不良,尤其是在IOC初始化和依赖注入的源码中有一系列名字超长的类,读了两天感觉自己一脸懵逼,这里简单的写一下自己的总结吧。

一. 概述

1. 概念

IOC即控制反转,也有叫依赖注入的,对于这二者是否有区别,维基百科上说依赖注入是Martin Fowler这个大神给IOC提出来的新名字。所谓控制反转,也就是把合作对象的引用或依赖关系的控制权反转交给IOC容器。

2. 注入方式

最常见的注入方式有三种:

  • 构造器注入
  • setter注入
  • 自动装配
  • 接口注入

2.1 构造器注入

这种方式的注入是指带有参数的构造函数注入,看下面的例子,我创建了两个成员变量SpringDao和User,但是并未设置对象的set方法,所以就不能支持第一种注入方式,这里的注入方式是在SpringAction的构造函数中注入,也就是说在创建SpringAction对象时要将SpringDao和User两个参数值传进来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SpringAction {  
//注入对象springDao
private SpringDao springDao;
private User user;

public SpringAction(SpringDao springDao,User user){
this.springDao = springDao;
this.user = user;
System.out.println("构造方法调用springDao和user");
}

public void save(){
user.setName("卡卡");
springDao.save(user);
}
}

在XML文件中同样不用的形式,而是使用标签,ref属性同样指向其它标签的name属性:

1
2
3
4
5
6
7
8
<!--配置bean,配置后该类由spring管理-->  
<bean name="springAction" class="com.bless.springdemo.action.SpringAction">
<!--(2)创建构造器注入,如果主类有带参的构造方法则需添加此配置-->
<constructor-arg ref="springDao"></constructor-arg>
<constructor-arg ref="user"></constructor-arg>
</bean>
<bean name="springDao" class="com.bless.springdemo.dao.impl.SpringDaoImpl"></bean>
<bean name="user" class="com.bless.springdemo.vo.User"></bean>

2.2 setter注入

这是最简单的注入方式,假设有一个SpringAction,类中需要实例化一个SpringDao对象,那么就可以定义一个private的SpringDao成员变量,然后创建SpringDao的set方法(这是ioc的注入入口):

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SpringAction {  
//注入对象springDao
private SpringDao springDao;

//一定要写被注入对象的set方法
public void setSpringDao(SpringDao springDao) {
this.springDao = springDao;
}

public void ok(){
springDao.ok();
}
}

随后编写spring的xml文件,中的name属性是class属性的一个别名,class属性指类的全名,因为在SpringAction中有一个公共属性Springdao,所以要在标签中创建一个标签指定SpringDao。标签中的name就是SpringAction类中的SpringDao属性名,ref指下面,这样其实是spring将SpringDaoImpl对象实例化并且调用SpringAction的setSpringDao方法将SpringDao注入:

1
2
3
4
5
6
<!--配置bean,配置后该类由spring管理-->  
<bean name="springAction" class="com.bless.springdemo.action.SpringAction">
<!--(1)依赖注入,配置当前类中相应的属性-->
<property name="springDao" ref="springDao"></property>
</bean>
<bean name="springDao" class="com.bless.springdemo.dao.impl.SpringDaoImpl"></bean>

2.3 自动装配

  • 组建扫描(component scanning):Spring会自动发现应用上下文中锁创建的bean
  • 自动装载(autowiring):Spring自动满足bean之间的依赖
    这两个分别对应了注解@Component和@Autowiring

@Component:这个注解表明该类会作为组件类,并告知Spring要为这个类创建bean
@AutoWiring:可以用在构造方法或set方法上,表明注入一个依赖

2.4 接口注入

这种一般就是通过简单工厂或者工厂方法来实现注入,没用过就不写了。

二. 核心类

1. BeanFactory和ApplicationContext

在Spring的IOC容器中主要就是这两个分支,BeanFactory是最基础的IOC容器,是所有容器的父接口,他只实现了最基础功能,相当于屌丝版,而ApplicationContext是实现了BeanFactory的高富帅版,高级IoC容器,除了基本的IoC容器功能外,支持不同信息源、访问资源、支持事件发布等功能。

继承了以下接口:

  • ListableBeanFactory:继承自BeanFactory,在此基础上,添加了containsBeanDefinition、getBeanDefinitionCount、getBeanDefinitionNames等方法。
  • HierarchicalBeanFactory:继承自BeanFactory,在此基础之上,添加了getParentBeanFactory、containsLocalBean这两个方法。
  • AutoWireCapableBeanFactory:继承自BeanFactory
  • MessageSource:用于获取国际化信息
  • ApplicationEventPublisher:因为ApplicationContext实现了该接口,因此spring的ApplicationContext实例具有发布事件的功能。

2.Resource

在Spring内部,针对于资源文件有一个统一的接口Resource表示。其主要实现类有ClassPathResource、FileSystemResource、UrlResource、ByteArrayResource、ServletContextResource和InputStreamResource。Resource接口中主要定义有以下方法:

  • exists():用于判断对应的资源是否真的存在。
  • isReadable():用于判断对应资源的内容是否可读。需要注意的是当其结果为true的时候,其内容未必真的可读,但如果返回false,则其内容必定不可读。
  • isOpen():用于判断当前资源是否代表一个已打开的输入流,如果结果为true,则表示当前资源的输入流不可多次读取,而且在读取以后需要对它进行关闭,以防止内存泄露。该方法主要针对于InputStreamResource,实现类中只有它的返回结果为true,其他都为false。
  • getURL():返回当前资源对应的URL。如果当前资源不能解析为一个URL则会抛出异常。如ByteArrayResource就不能解析为一个URL。
  • getFile():返回当前资源对应的File。如果当前资源不能以绝对路径解析为一个File则会抛出异常。如ByteArrayResource就不能解析为一个File。
  • getInputStream():获取当前资源代表的输入流。除了InputStreamResource以外,其它Resource实现类每次调用getInputStream()方法都将返回一个全新的InputStream。

实现类:

  • ClassPathResource可用来获取类路径下的资源文件。假设我们有一个资源文件test.txt在类路径下,我们就可以通过给定对应资源文件在类路径下的路径path来获取它,new ClassPathResource(“test.txt”)。
  • FileSystemResource可用来获取文件系统里面的资源。我们可以通过对应资源文件的文件路径来构建一个FileSystemResource。FileSystemResource还可以往对应的资源文件里面写内容,当然前提是当前资源文件是可写的,这可以通过其isWritable()方法来判断。FileSystemResource对外开放了对应资源文件的输出流,可以通过getOutputStream()方法获取到。
  • UrlResource可用来代表URL对应的资源,它对URL做了一个简单的封装。通过给定一个URL地址,我们就能构建一个UrlResource。
  • ByteArrayResource是针对于字节数组封装的资源,它的构建需要一个字节数组。
  • ServletContextResource是针对于ServletContext封装的资源,用于访问ServletContext环境下的资源。ServletContextResource持有一个ServletContext的引用,其底层是通过ServletContext的getResource()方法和getResourceAsStream()方法来获取资源的。
  • InputStreamResource是针对于输入流封装的资源,它的构建需要一个输入流。

3.ResourceLoader

通过上面介绍的Resource接口的实现类,我们就可以使用它们各自的构造函数创建符合需求的Resource实例。但是在Spring中提供了ResourceLoader接口,用于实现不同的Resource加载策略,即将不同Resource实例的创建交给ResourceLoader来加载,这也是ApplicationContext等高级容器中使用的策略。

接口中有两个主要的方法:

  • getResource():在ResourceLoader接口中,主要定义了一个方法:getResource(),它通过提供的资源location参数获取Resource实例,该实例可以是ClasPathResource、FileSystemResource、UrlResource等,但是该方法返回的Resource实例并不保证该Resource一定是存在的,需要调用exists方法判断。
  • getResourceByPath:这个方法被声明为protected,所以在它的子类中基本都重写了这个方法。

4. BeanDefinition

一个BeanDefinition描述了一个bean的实例,包括属性值,构造方法参数值和继承自它的类的更多信息。这个东西会贯穿整个IOC的初始化

三. Bean的注入

1. IOC初始化

上面说到了BeanDefinition会伴随整个IOC初始化的过程初始化,其实整个IOC容器的初始化过程大致分为以下四个步骤:

  1. Resource定位过程
  2. BeanDefinition载入
  3. BeanDefinition解析
  4. BeanDefinition注册

最终配置的bean以BeanDefinition的数据与结构存在于IOC容器之中,这个过程不涉及bean的依赖注入,也不产生任何bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class FileSystemXmlApplicationContext extends AbstractXmlApplicationContext {
//核心构造器
public FileSystemXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)
throws BeansException {
super(parent);
setConfigLocations(configLocations);
if (refresh) {
refresh();//Ioc容器的refresh()过程,是个非常复杂的过程,但不同的容器实现这里都是相似的,因此基类中就将他们封装好了
}
}
//通过构造一个FileSystemResource对象来得到一个在文件系统中定位的BeanDefinition
//采用模板方法设计模式,具体的实现用子类来完成
@Override
protected Resource getResourceByPath(String path) {
if (path != null && path.startsWith("/")) {
path = path.substring(1);
}
return new FileSystemResource(path);
}
}

这里我们主要看一下这部分

1
2
3
4
5
6
7
8
public FileSystemXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)
throws BeansException {
super(parent);
setConfigLocations(configLocations);
if (refresh) {
refresh();//Ioc容器的refresh()过程,是个非常复杂的过程,但不同的容器实现这里都是相似的,因此基类中就将他们封装好了
}
}

这里我们可以看到调用了一个 refresh(),这个方法在父类AbstractApplicationContext中已经封装好了。它详细描述了整个ApplicationContext的初始化过程,比如BeanFactory的更新、MessageSource和PostProcessor的注册等。这里看起来像是对ApplicationContext进行初始化的模版或执行提纲,这个执行过程为Bean的生命周期管理提供了条件。

refresh为初始化IoC容器的入口,但是具体的资源定位还是在XmlBeanDefinitionReader读入BeanDefinition时完成,loadBeanDefinitions() 加载BeanDefinition的载入。由于源码分析过于冗长我就直接介绍一下每一步的大致思路,如果想看具体的分析可以参考http://www.cnblogs.com/ITtangtang/p/3978349.html,这篇文章比较详细。这里直接给出一个流程性的总结:

  1. 首先需要获得一个IOC容器才能操作其控制的Bean,对于IOC容器的初始化来说,他通过一个refresh()函数作为开头,首先判断是否已经创建了BeanFactory,如果创建了则销毁关闭该BeanFactory,接着会创建相应的读取器(Reader),通过相应loadBeanDefinitions函数获取资源定位
  2. 对xml的资源文件的加载将他们转换为一个Document对象进行处理,载入过程实际上就是Resource对象转换成Document对象的过程,也就是一个XML文件解析的过程,Spring中使用的是JAXP解析,生成的Document文件就是org.w3c.dom.Document中的Document文件;
  3. 就是解析相应的Document对象,这个没有什么好说的类似于用DOM解析xml文件
  4. 向IoC容器注册解析的BeanDefiniton ,完成前面的步骤用户定义的BeanDefiniton已经在IoC容器里建立相应的数据结构和表示,但不能直接使用,需要进行注册,简单来说就是通过一个ConcurrentHashMap存储。

2. 依赖注入

假设我们已经完成了IoC容器的初始化,首先要注意到的一点,Spring中的依赖注入是lazy-loading,即用户第一次向容器索要Bean,调用相应的getBean()函数,但是也能通过设置Bean的lazy-init属性来控制预实例化过程,这个预实例化在初始化容器时完成Bean的依赖注入。

他的主要流程也就下面这几个步骤:

  1. AbstractBeanFactory中的getBean方法来获取Bean:

在缓存中查找,如果存在则直接返回,否则开始下一步创建Bean

  1. AbstractAutowireCapableBeanFactory类中创建Bean,这个类中有几个关键方法:

    1. createBean方法:创建容器指定的Bean实例对象的入口

同时还对创建的Bean实例对象进行初始化处理比如init-method、后置处理器等,然后调用下面的两个方法创建实例并注入依赖

2. createBeanInstance方法:创建Bean的Java实例对象,分两种情况:

对于使用工厂方法和自动装配特性的bean的实例化:则调用对应的工厂方法或者参数匹配的构造方法即可完成实例化对象的工作
否则调用默认的无参构造器进行实例化即SimpleInstantiationStrategy的instantiate方法:

1. 使用Java的反射技术
2. 使用CGLIB
  1. populateBean方法:实例化之后,根据属性类型决定是否需要解析,最后通过BeanWrapperImpl类完成对属性的注入,对属性的类型也要进行判断:
  • 属性值类型不需要转换时,不需要解析属性值,直接准备进行依赖注入
  • 属性值需要进行类型转换时,如对其他对象的引用等,首先需要解析属性值,然后对解析后的属性值进行依赖注入。解析过程由BeanDefinitionValueResolver类的setPropertyValue方法完成
  1. BeanWrapperImpl对Bean属性的依赖注入:
  • 对于集合类属性,将其属性值解析为目标类型的集合后直接赋值给属性。
  • 对于非集合类型的属性,大量使用了JDK的反射和内省机制,通过属性的getter方法(reader method)获取指定属性注入以前的值,同时调用属性的setter方法(writer method)为属性设置注入后的值。

总结一下就是先判断是否有缓存,有的话直接取,没有就创建实例,接着会判断bean是否能被实例化,以及实例化这个bean的方法,根据相应策略来创建bean,之后填充属性完成创建并返回。

3. 循环依赖

循环依赖就是循环引用,就是两个或多个Bean相互之间的持有对方,比如CircleA引用CircleB,CircleB引用CircleC,CircleC引用CircleA,则它们最终反映为一个环。此处不是循环调用,循环调用是方法之间的环调用。

循环调用是无法解决的,除非有终结条件,否则就是死循环,最终导致内存溢出错误。Spring容器循环依赖包括构造器循环依赖和setter循环依赖,那Spring容器如何解决循环依赖呢?首先让我们来定义循环引用类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package cn.javass.spring.chapter3.bean;  
public class CircleA {
private CircleB circleB;
public CircleA() {
}
public CircleA(CircleB circleB) {
this.circleB = circleB;
}
public void setCircleB(CircleB circleB)
{
this.circleB = circleB;
}
public void a() {
circleB.b();
}
}
package cn.javass.spring.chapter3.bean;
public class CircleB {
private CircleC circleC;
public CircleB() {
}
public CircleB(CircleC circleC) {
this.circleC = circleC;
}
public void setCircleC(CircleC circleC)
{
this.circleC = circleC;
}
public void b() {
circleC.c();
}
}
package cn.javass.spring.chapter3.bean;
public class CircleC {
private CircleA circleA;
public CircleC() {
}
public CircleC(CircleA circleA) {
this.circleA = circleA;
}
public void setCircleA(CircleA circleA)
{
this.circleA = circleA;
}
public void c() {
circleA.a();
}
}

1.构造器循环依赖

表示通过构造器注入构成的循环依赖,此依赖是无法解决的,只能抛出BeanCurrentlyInCreationException异常表示循环依赖。

如在创建CircleA类时,构造器需要CircleB类,那将去创建CircleB,在创建CircleB类时又发现需要CircleC类,则又去创建CircleC,最终在创建CircleC时发现又需要CircleA;从而形成一个环,没办法创建。
Spring容器将每一个正在创建的Bean 标识符放在一个“当前创建Bean池”中,Bean标识符在创建过程中将一直保持在这个池中,因此如果在创建Bean过程中发现自己已经在“当前创建Bean池”里时将抛出BeanCurrentlyInCreationException异常表示循环依赖;而对于创建完毕的Bean将从“当前创建Bean池”中清除掉。
这个调用会是这样的流程:

  1. Spring容器创建“circleA” Bean,首先去“当前创建Bean池”查找是否当前Bean正在创建,如果没发现,则继续准备其需要的构造器参数“circleB”,并将“circleA” 标识符放到“当前创建Bean池”;
  2. Spring容器创建“circleB” Bean,首先去“当前创建Bean池”查找是否当前Bean正在创建,如果没发现,则继续准备其需要的构造器参数“circleC”,并将“circleB” 标识符放到“当前创建Bean池”;S
  3. pring容器创建“circleC” Bean,首先去“当前创建Bean池”查找是否当前Bean正在创建,如果没发现,则继续准备其需要的构造器参数“circleA”,并将“circleC” 标识符放到“当前创建Bean池”;
  4. 到此为止Spring容器要去创建“circleA”Bean,发现该Bean 标识符在“当前创建Bean池”中,因为表示循环依赖,抛出BeanCurrentlyInCreationException。

1.setter循环依赖

对于setter注入造成的依赖是通过Spring容器提前暴露刚完成构造器注入但未完成其他步骤(如setter注入)的Bean来完成的,而且只能解决单例作用域的Bean循环依赖。具体步骤如下:

  1. Spring容器创建单例“circleA” Bean,首先根据无参构造器创建Bean,并暴露一个“ObjectFactory ”用于返回一个提前暴露一个创建中的Bean,并将“circleA” 标识符放到“当前创建Bean池”;然后进行setter注入“circleB”;
  2. Spring容器创建单例“circleB” Bean,首先根据无参构造器创建Bean,并暴露一个“ObjectFactory”用于返回一个提前暴露一个创建中的Bean,并将“circleB” 标识符放到“当前创建Bean池”,然后进行setter注入“circleC”;
  3. Spring容器创建单例“circleC” Bean,首先根据无参构造器创建Bean,并暴露一个“ObjectFactory ”用于返回一个提前暴露一个创建中的Bean,并将“circleC” 标识符放到“当前创建Bean池”,然后进行setter注入“circleA”;进行注入“circleA”时由于提前暴露了“ObjectFactory”工厂从而使用它返回提前暴露一个创建中的Bean;
  4. 最后在依赖注入“circleB”和“circleA”,完成setter注入。

对于“prototype”作用域Bean,Spring容器无法完成依赖注入,因为“prototype”作用域的Bean,Spring容器不进行缓存,因此无法提前暴露一个创建中的Bean。

四. 参考

http://paine1690.github.io/2017/01/02/spring/Spring%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90(2)%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5%E7%9A%84%E5%AE%9E%E7%8E%B0/
http://blog.battcn.com/2018/01/17/spring/spring-4/#more
http://www.cnblogs.com/ITtangtang/p/3978349.html
http://jinnianshilongnian.iteye.com/blog/1415278