Spring IoC 概述
IoC:Inverse of Control(控制反转)
- 读作“反转控制”,更好理解,不是什么技术,而是一种设计思想,就是将原本在程序中手动创建对象的控制权,交由Spring框架来管理。
- 正控:若要使用某个对象,需要自己去负责对象的创建
- 反控:若要使用某个对象,只需要从 Spring 容器中获取需要使用的对象,不关心对象的创建过程,也就是把创建对象的控制权反转给了Spring框架
- 好莱坞法则:Don’t call me ,I’ll call you
IoC
中文名称为控制反转,它就是用来解决上述问题的一种设计思想,它能指导我们设计出耦合性更低,更易于管理的代码。传统的程序,都是在需要使用的地方主动地创建对象,而IoC
的思想却是将创建对象的工作交给IoC
容器去进行。我们告诉IoC
容器,它需要创建那些对象,IoC
容器创建好这些对象,然后主动地将它们注入到需要使用的地方。此时,需要使用对象的那些类,并不主动地获取对象,而且由IoC
容器为它们分配,控制权在IoC
容器手上,这就是控制反转。当然,IoC
容器并不只是控制对象,可以理解为控制外部资源的获取。
IoC
很好的体现了面向对象设计法则之一—— 好莱坞法则:“别找我们,我们找你”;即由IoC
容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。
一个例子
控制反转显然是一个抽象的概念,我们举一个鲜明的例子来说明。
在现实生活中,人们要用到一样东西的时候,第一反应就是去找到这件东西,比如想喝新鲜橙汁,在没有饮品店的日子里,最直观的做法就是:买果汁机、买橙子,然后准备开水。值得注意的是:这些都是你自己“主动”创造的过程,也就是说一杯橙汁需要你自己创造。
然而到了今时今日,由于饮品店的盛行,当我们想喝橙汁时,第一想法就转换成了找到饮品店的联系方式,通过电话等渠道描述你的需要、地址、联系方式等,下订单等待,过一会儿就会有人送来橙汁了。
请注意你并没有“主动”去创造橙汁,橙汁是由饮品店创造的,而不是你,然而也完全达到了你的要求,甚至比你创造的要好上那么一些。
IoC 和DI的关系
DI
中文名称为依赖注入,它其实和IoC
是相同的概念,或者可以理解为它是IoC
的一种具体的实现方式。IoC
的概念可能比较模糊,控制反转只是一种思想,可能仅仅只是停留在由其他组件控制对象,而不是在使用的地方直接创建这一层面,但是具体如何实现并没有指明。而DI
就是它的一种实现思路,由容器创建对象,并主动将对象注入到需要使用它的地方。
Spring IoC 阐述
这就是一种控制反转的理念,上述的例子已经很好的说明了问题,我们再来描述一下控制反转的概念:控制反转是一种通过描述(在 Java 中可以是 XML 或者注解)并通过第三方(Spring)去产生或获取特定对象的方式。
- 好处:
降低对象之间的耦合
我们不需要理解一个类的具体实现,只需要知道它有什么用就好了(直接向 IoC 容器拿)
主动创建的模式中,责任归于开发者,而在被动的模式下,责任归于 IoC 容器,基于这样的被动形式,我们就说对象被控制反转了。(也可以说是反转了控制)
Spring IoC 容器
Spring 会提供 IoC 容器来管理和容纳我们所开发的各种各样的 Bean,并且我们可以从中获取各种发布在 Spring IoC 容器里的 Bean,并且通过描述可以得到它。
Spring IoC 容器的设计
Spring IoC 容器的设计主要是基于以下两个接口:
- BeanFactory
- ApplicationContext
其中 ApplicationContext 是 BeanFactory 的子接口之一,换句话说:BeanFactory 是 Spring IoC 容器所定义的最底层接口,而 ApplicationContext 是其最高级接口之一,并对 BeanFactory 功能做了许多的扩展,所以在绝大部分的工作场景下,都会使用 ApplicationContext 作为 Spring IoC 容器。
BeanFactory
从上图中我们可以几乎看到, BeanFactory 位于设计的最底层,它提供了 Spring IoC 最底层的设计,为此,我们先来看看该类中提供了哪些方法:
由于这个接口的重要性,所以有必要在这里作一下简短的说明:
- 【getBean】 对应了多个方法来获取配置给 Spring IoC 容器的 Bean。
① 按照类型拿 bean:bean = (Bean) factory.getBean(Bean.class);
注意:要求在 Spring 中只配置了一个这种类型的实例,否则报错。(如果有多个那 Spring 就懵了,不知道该获取哪一个)
② 按照 bean 的名字拿 bean:bean = (Bean) factory.getBean("beanName");
注意:这种方法不太安全,IDE 不会检查其安全性(关联性)
③ 按照名字和类型拿 bean:(推荐)bean = (Bean) factory.getBean("beanName", Bean.class);
- 【isSingleton】 用于判断是否单例,如果判断为真,其意思是该 Bean 在容器中是作为一个唯一单例存在的。而【isPrototype】则相反,如果判断为真,意思是当你从容器中获取 Bean,容器就为你生成一个新的实例。
注意:在默认情况下,【isSingleton】为 ture,而【isPrototype】为 false - 关于 type 的匹配,这是一个按 Java 类型匹配的方式
- 【getAliases】方法是获取别名的方法
这就是 Spring IoC 最底层的设计,所有关于 Spring IoC 的容器将会遵守它所定义的方法。
ApplicationContext
根据 ApplicationContext 的类继承关系图,可以看到 ApplicationContext 接口扩展了许许多多的接口,因此它的功能十分强大,所以在实际应用中常常会使用到的是 ApplicationContext 接口,因为 BeanFactory 的方法和功能较少,而 ApplicationContext 的方法和功能较多。
我们来认识一个 ApplicationContext 的子类——ClassPathXmlApplicationContext。
1. 先在【src】目录下创建一个 【bean.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 通过 xml 方式装配 bean -->
<bean name="source" class="pojo.Source">
<property name="fruit" value="橙子"/>
<property name="sugar" value="多糖"/>
<property name="size" value="超大杯"/>
</bean>
</beans>
2. 这里定义了一个 bean ,这样 Spring IoC 容器在初始化的时候就能找到它们,然后使用 ClassPathXmlApplicationContext 容器就可以将其初始化:
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
Source source = (Source) context.getBean("source", Source.class);
System.out.println(source.getFruit());
System.out.println(source.getSugar());
System.out.println(source.getSize());
这样就会使用 Application 的实现类 ClassPathXmlApplicationContext 去初始化 Spring IoC 容器,然后开发者就可以通过 IoC 容器来获取资源了啦!
ApplicationContext 常见实现类:
1.ClassPathXmlApplicationContext:
读取classpath中的资源
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
2:FileSystemXmlApplicationContext:-
读取指定路径的资源
ApplicationContext ac = new FileSystemXmlApplicationContext("c:/applicationContext.xml");
3.XmlWebApplicationContext:
需要在Web的环境下才可以运行
XmlWebApplicationContext ac = new XmlWebApplicationContext(); // 这时并没有初始化容器
ac.setServletContext(servletContext); // 需要指定ServletContext对象
ac.setConfigLocation("/WEB-INF/applicationContext.xml"); // 指定配置文件路径,开头的斜线表示Web应用的根目录
ac.refresh(); // 初始化容器
BeanFactory 和 ApplicationContext 的区别:
- BeanFactory:是Spring中最底层的接口,只提供了最简单的IoC功能,负责配置,创建和管理bean。
在应用中,一般不使用 BeanFactory,而推荐使用ApplicationContext(应用上下文),原因如下。 - ApplicationContext:
1.继承了 BeanFactory,拥有了基本的 IoC 功能;
2.除此之外,ApplicationContext 还提供了以下功能:
① 支持国际化;
② 支持消息机制;
③ 支持统一的资源加载;
④ 支持AOP功能;
Spring IoC 的容器的初始化和依赖注入
虽然 Spring IoC 容器的生成十分的复杂,但是大体了解一下 Spring IoC 初始化的过程还是必要的。这对于理解 Spring 的一系列行为是很有帮助的。
注意:Bean 的定义和初始化在 Spring IoC 容器是两大步骤,它是先定义,然后初始化和依赖注入的。
- Bean 的定义分为 3 步:
1.Resource 定位
Spring IoC 容器先根据开发者的配置,进行资源的定位,在 Spring 的开发中,通过 XML 或者注解都是十分常见的方式,定位的内容是由开发者提供的。
2.BeanDefinition 的载入
这个时候只是将 Resource 定位到的信息,保存到 Bean 定义(BeanDefinition)中,此时并不会创建 Bean 的实例
3.BeanDefinition 的注册
这个过程就是将 BeanDefinition 的信息发布到 Spring IoC 容器中
注意:此时仍然没有对应的 Bean 的实例。
做完了以上 3 步,Bean 就在 Spring IoC 容器中被定义了,而没有被初始化,更没有完成依赖注入,也就是没有注入其配置的资源给 Bean,那么它还不能完全使用。
对于初始化和依赖注入,Spring Bean 还有一个配置选项——【lazy-init】,其含义就是是否初始化 Spring Bean。在没有任何配置的情况下,它的默认值为 default,实际值为 false,也就是 Spring IoC 默认会自动初始化 Bean。如果将其设置为 true,那么只有当我们使用 Spring IoC 容器的 getBean 方法获取它时,它才会进行 Bean 的初始化,完成依赖注入。
IoC 是如何实现的
最后我们简单说说IoC是如何实现的。想象一下如果我们自己来实现这个依赖注入的功能,我们怎么来做? 无外乎:
- 读取标注或者配置文件,看看JuiceMaker依赖的是哪个Source,拿到类名
- 使用反射的API,基于类名实例化对应的对象实例
- 将对象实例,通过构造函数或者 setter,传递给 JuiceMaker
这一块,我就简单地说一说我今天下午在研究Spring
的IoC
源码的过程中,了解到的一些内容。首先,Spring
的IoC
容器,可以简单地理解为就是BeanFactory
接口的一个实现类对象,比如Spring
的应用上下文接口ApplicationContext
就是继承自BeanFactory
,而我们使用较多的ClassPathXmlApplicationContext
就是ApplicationContext
的一个实现类。它们都可以理解为是Spring
的IoC
容器,而BeanFactory
就是这个继承体系中的最高层。下面我就以单例bean的创建,简单地说一说Spring
的IoC
实现的一个过程,假设使用到的是ClassPathXmlApplicationContext
这个容器,解析的是xml
配置文件:
- 容器解析
xml
配置文件,将声明在xml
文件中的bean
的配置信息提取出来,每一个bean
的配置信息被封装成一个BeanDefinitionHolder
对象。BeanDefinitionHolder
主要包含三个成员——BeanDefinition
对象,bean
的名称(id
),以及bean
的别名。BeanDefinition
对象就是用来保存一个bean
的配置信息,比如bean
的作用域,bean
的类型,bean
的依赖,bean
的属性...... - 容器创建一个
ConcurrentHashMap
对象,这个map
的key
是bean
的名称,而value
则是BeanDefinition
对象的引用。容器将第一步中解析出的每一个BeanDefinitionHolder
对象,它对应的bean
名称,以及拥有的BeanDefinition
引用放入这个map
中。所以,这个map
保存了我们在程序中声明的所有的bean
的配置信息。 - 容器初始化完成后,开始创建
bean
,遍历第二步中的map
集合,获取到bean
的名称以及对应的BeanDefinition
对象,通过BeanDefinition
对象中的信息,判断这个bean
有没有定义为延迟加载,若没有,则需要现在就进行创建。在创建前,先通过BeanDefinition
中的配置信息,判断此bean
有没有depend-on
(依赖)其他bean
,若有,则先创建当前bean
依赖的bean
(此处的depend-on
不是bean
的属性,而是需要通过配置项进行配置的)。之后,则创建当前遍历到的bean
。 bean
在创建完成后,通过bean
的BeanDefinition
对象,获取bean
需要注入值的属性,然后为属性赋值,若属性的值类型是其他的bean
,则以上面相同的步骤,创建属性对应的bean
;- 容器创建一个
ConcurrentHashMap
,将创建好的单例bean
保存在其中。我们在代码中可以通过容器的getBean
方法,传入bean
的名称或类型获取单例bean
。容器会先去这个map
中查找,若map
中不存在,且这个bean
的配置在第2
步中存储配置信息的map
中能够找到,则创建这个bean
,放入存储bean
的map
中(因为bean
可以配置延迟加载,即第一次获取时加载);
Spring
中,bean
最基本的两种作用域就是singleton
(单例)和prototype
(多例),默认为单例。以上过程是单例bean
的创建过程,若作用域为prototype
,则每一次调用getBean
方法,都会创建一个新的bean
。顺带一提,创建bean
的方式是通过反射机制,这个大部分人应该都知道。
我们发现其实自己来实现也不是很难,Spring实际也就是这么做的。这么看的话其实IoC就是一个工厂模式的升级版!当然要做一个成熟的IoC框架,还是非常多细致的工作要做,Spring不仅提供了一个已经成为业界标准的Java IoC框架,还提供了更多强大的功能,所以大家就别去造轮子啦!希望了解IoC更多实现细节不妨通过学习Spring的源码来加深理解!
Bean的生命周期:
上面介绍了一下IoC的实现,而针对每一个单独的Bean,Spring容器如何处理?这就涉及到Bean的生命周期了。这里就来简单介绍一下Bean是生命周期,首先通过一张图了解:
上面这张图的描述如下:
-
首先由
Spring
容器创建bean
实例; -
为
bean
的属性注入值; -
若
bean
实现了各类Aware
接口,则调用相应的set
方法,比如说,实现了BeanNameAware
接口的bean
,此时容器将调用bean
的setBeanName
方法,将bean
的name
作为参数;实现了ApplicationContextAware
接口的bean
,此时容器将执行bean
的setApplicationContext
方法,将bean
所在的上下文对象作为参数…… -
若容器中包含实现了
BeanPostProcessor
接口的bean
,则此时将调用这些bean
的postProcessBeforeInitialization
方法,将当前正在创建的bean
的引用以及bean
的name
作为参数传入; -
若
bean
实现了InitializingBean
接口,此时将调用bean
的afterPropertiesSet
方法; -
若
bean
指定了自定义的初始化方法,比如说通过配置文件的init-method
选项,则此时将执行这个自定义初始化方法; -
若容器中包含实现了
BeanPostProcessor
接口的bean
,则此时将调用这些bean
的postProcessAfterInitialization
方法,将当前正在创建的bean
的引用以及bean
的name
作为参数传入; -
这个时候,
bean
就算是初始化完毕,可以被使用了,在容器销毁之前,这个对象将一直保存在容器中; -
若
bean
实现了DisposableBean
接口,则在容器销毁时,会调用bean
的destroy
方法; -
如果
bean
定义了自定义的销毁方法,则在容器销毁时,会调用这个自定义的销毁方法; -
容器销毁成功,
bean
也被回收;