Spring 的 IoC 容器

依赖注入DI 和 控制反转IoC

在spring文档中是这样描述的

IoC也称为依赖注入(dependency injection, DI)。它是一个对象去定义它的依赖的过程,这些依赖(即它去使用的其他对象),只能通过构造函数参数注入、工厂方法的参数注入、或是在对象实例化或是从工厂方法返回一个实例之后去设置属性。而容器是在创建bean的时候,注入这些依赖。这个过程基本上是反向的,因此称为控制反转(IoC),因为bean本身是通过使用类的直接构造或一种如服务定位器模式的机制来控制依赖的实例化或位置

依赖注入(Dependency Injection,DI) 是组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现

控制反转(Inversion of Control,IOC) 是说创建对象的控制权进行转移,以前创建对象的主动权和创建时机是由自己把控的,而现在这种权力转移到第三方,比如转移交给了IOC容器,它就是一个专门用来创建对象的工厂,你要什么对象,它就给你什么对象,有了IOC容器,依赖关系就变了,原先的依赖关系就没了,它们都依赖IOC容器了,通过IOC容器来建立它们之间的关系。

DI(依赖注入)其实就是IOC的另外一种说法,DI是由Martin Fowler 在2004年初的一篇论文中首次提出的。他总结道:控制的什么被反转了?就是获得依赖对象的方式反转了

个人理解: 简单来说,依赖注入就是控制反转,我把控制反转理解成是一个过程,依赖注入是一个动作(这里简化为一个动作)。控制反转把创建对象(依赖)的控制权交给了IoC容器,容器根据组件之间配置的依赖关系来创建对象,而依赖注入就是这个创建对象的动作,它在IoC的基础上完成操作(即为类里面的属性设置值)

IoC 容器

构成应用程序主干并由 Spring IoC 容器管理的对象称为 bean

org.springframework.context.ApplicationContext 代表 Spring IoC 容器,负责实例化、配置和组装 bean。容器通过读取配置元数据获取有关实例化、配置和组装哪些对象的说明。配置元数据用 XML、Java 注释或 Java 代码表示(用来描述对象之间的依赖关系)。
spring IoC容器

ApplicationContext 容器

最常被使用的 ApplicationContext 接口实现:

  • FileSystemXmlApplicationContext 该容器从XML文件中加载已被定义的bean,需要提供给构造器XML文件的完整路径
  • ClassPathXmlApplicationContext 该容器从XML文件中加载已被定义的bean,可以不需要提供XML文件的完整路径,只需正确配置CLASSPATH环境变量即可,容器会从CLASSPATH中搜索bean配置文件
  • WebXmlApplicationContext 该容器会在一个 web 应用程序的范围内加载在XML文件中已被定义的bean

这里用 ClassPathXmlApplicationContext 来举一个例子

HelloWorldDaoImpl.java

1
2
3
4
5
6
7
8
9
public class HelloWorldDaoImpl implements HelloWorldDao {
private String message;
public void setMessage(String message) {
this.message = message;
}
public void say() {
System.out.println(message);
}
}

HelloWorldSeriveceImpl.java

1
2
3
4
5
6
7
8
9
public class HelloWorldServiceImpl implements HelloWorldService{
private HelloWorldDao helloWorldDao;
public void setHelloWorldDao(HelloWorldDao helloWorldDao) {
this.helloWorldDao = helloWorldDao;
}
public void say() {
helloWorldDao.say();
}
}

application.xml 文件描述着bean之间的依赖关系

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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">

<bean id="helloWorldDao" class="com.spring.dao.HelloWorldDaoImpl">
<property name="message" value="Hello World!"/>
</bean>
<bean id="helloWorldService" class="com.spring.service.HelloWorldServiceImpl">
<property name="helloWorldDao" ref="helloWorldDao"/>
</bean>
</beans>

MainApp.java

1
2
3
4
5
6
7
public class MainApp {
public static void main( String[] args ){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application.xml");
HelloWorldService service = (HelloWorldService) applicationContext.getBean("helloWorldService");
service.say();
}
}


  • 第一步生成工厂对象。加载 bean 配置文件后,ClassPathXmlApplicationContext 负责生成和初始化XML配置文件中的 bean
  • 第二步利用第一步生成的上下文中的 getBean() 方法得到所需要的 bean。 这个方法通过配置文件中的 bean 的 id 来返回一个真正的对象。得到这个对象,就可以利用这个对象来调用任何方法。

HelloWorldDaoImplmessage , HelloWorldServiceImpl中的 helloWorldDao 都是通过容器根据 xml 文件里描述的依赖关系装配好的

运行程序: Hello World!

Spring Bean

构成应用程序主干并由Spring IoC容器管理的对象称为 bean, bean 是被Spring IoC容器实例化、组装、管理的对象。Spring IoC容器管理一个或多个 bean。这些 bean 是用我们提供给容器的配置元数据创建的(例如,XML \<bean/> 定义的形式)。

配置元数据转换为一组 组成每个bean定义的属性。下表描述了这些属性:

属性 描述
class 这个属性是强制性的,指定用来创建 bean 的 bean 类
name 这个属性指定唯一的 bean 标识符。在基于 XML 的配置元数据中,你可以使用 ID 和/或 name 属性来指定 bean 标识符
scope 这个属性指定由特定的 bean 定义创建的对象的作用域
constructor arguments 它是用来注入依赖关系的
properties 它是用来注入依赖关系的
autowiring mode 它是用来注入依赖关系的
lazy-initialization mode 延迟初始化的 bean 告诉 IoC 容器在它第一次被请求时,而不是在启动时去创建一个 bean 实例
initialization 方法 在 bean 的所有必需的属性被容器设置之后,调用回调方法
destruction 方法 当包含该 bean 的容器被销毁时,使用回调方法

bean 的作用域

作用域 描述
singleton (默认值) Spring IoC容器中只有一个bean实例,bean以单例方式存在
prototype 一个Bean定义可以创建任意多个实例对象,每次从容器中调用Bean时,都返回一个新的实例
request 限定一个Bean的作用域为单个HTTP请求的生命周期;也就是说,每次HTTP请求都会创建新的bean实例。仅在基于web的Spring ApplicationContext有效
session 限定一个Bean的作用域为HTTPSession的生命周期。仅在基于web的Spring ApplicationContext可用
globalSession 限定一个Bean的作用域为全局HTTPSession的生命周期。通常用于Portlet场景。仅基于web的Spring ApplicationContext可用
application 限定一个Bean的作用域为ServletContext的生命周期。仅基于web的Spring ApplicationContext可用
websocket 限定一个Bean的作用域为WebSocket的生命周期。仅基于web的Spring ApplicationContext可用

依赖注入

依赖注入主要使用两种方式,一种是基于构造函数的注入,另一种的基于Setter方法的依赖注入。

基于构造函数的注入

基于构造函数的依赖注入是由IoC容器来调用类的构造函数,构造函数的参数代表这个Bean所依赖的对象。

构造函数的参数解析

构造函数的参数解析是通过参数的类型来匹配的。如果在Bean的构造函数参数不存在歧义,那么构造器参数的顺序也就是就是这些参数实例化以及装载的顺序

1
2
3
4
5
6
7
package x.y;
public class Foo {

public Foo(Bar bar, Baz baz) {
// ...
}
}

假设BarBaz在继承层次上不相关,也没有什么歧义的话,下面的配置完全可以工作正常,不需要再去<constructor-arg>元素中指定构造函数参数的索引或类型信息。

1
2
3
4
5
6
7
8
9
<beans>
<bean id="foo" class="x.y.Foo">
<constructor-arg ref="bar"/>
<constructor-arg ref="baz"/>
</bean>

<bean id="bar" class="x.y.Bar"/>
<bean id="baz" class="x.y.Baz"/>
</beans>

当使用简单的类型的时候,容器可以通过使用构造函数参数的type属性来实现简单类型的匹配。例如

1
2
3
4
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>

或者使用index属性来指定构造参数的位置,这个索引也同时是为了解决构造函数中有多个相同类型的参数无法精确匹配的问题

1
2
3
4
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>

基于Setter方法的依赖注入

基于Setter函数的依赖注入则是容器会调用Bean的无参构造函数,或者无参数的工厂方法,然后再来调用Setter方法来实现的依赖注入

1
2
3
4
5
6
7
8
9
10
<bean id="exampleBean" class="examples.ExampleBean">
<property name="beanOne">
<ref bean="anotherExampleBean"/>
</property>
<property name="beanTwo" ref="yetAnotherBean"/>
<property name="integerProperty" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ExampleBean {

private AnotherBean beanOne;
private YetAnotherBean beanTwo;
private int i;

public void setBeanOne(AnotherBean beanOne) {
this.beanOne = beanOne;
}
public void setBeanTwo(YetAnotherBean beanTwo) {
this.beanTwo = beanTwo;
}
public void setIntegerProperty(int i) {
this.i = i;
}
}

基于构造函数注入中,我们使用的是〈bean〉标签中的〈constructor-arg〉元素,而在基于设值函数的注入中,我们使用的是〈bean〉标签中的〈property〉元素

把一个引用传递给一个对象,那么你需要使用标签的 ref 属性,而如果你要直接传递一个值,那么你应该使用 value 属性

使用 p-namespace 实现 XML 配置

p-namespace允许您使用bean元素的属性(而不是嵌套 <property/>元素)来描述属性值或是bean

Spring支持具有命名空间的可扩展配置格式,这些命名空间基于XML Schema定义

带有标签的标准 XML 配置文件

1
2
3
4
5
6
7
8
<bean id="john-classic" class="com.example.Person">
<property name="name" value="John Doe"/>
<property name="spouse" ref="jane"/>
</bean>

<bean name="jane" class="com.example.Person">
<property name="name" value="John Doe"/>
</bean>

使用 p-namespace 实现 XML 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="john-classic" class="com.example.Person"
p:name="John Doe"
p:spouse-ref="jane"/>

<bean name="jane" class="com.example.Person"
p:name="John Doe"/>
</beans>

-ref 部分表明这不是一个直接的值,而是对另一个 bean 的引用

内部bean

定义在元素的或者元素之内的 Bean 叫做内部Bean

1
2
3
4
5
6
7
8
<bean id="outer" class="...">
<property name="target">
<bean class="com.example.Person"> <!-- this is the inner bean -->
<property name="name" value="Fiona Apple"/>
<property name="age" value="25"/>
</bean>
</property>
</bean>

内部Bean的定义是不需要指定id或者名字的。如果指定了,容器也不会用之作为分别Bean的区分标识。容器同时也会无视内部Bean的scope标签:内部Bean 总是匿名的,而且随着外部的Bean同时创建的。不能将内部的Bean注入到外部Bean以外的其他Bean

注入集合

在 \<list/>, \<set/>, \<map/> 和 \<props/>元素中,可以配置 Java 集合类型 List, Set, Map 以及 Properties 的属性和参数

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
<bean id="moreComplexObject" class="example.ComplexObject">

<property name="adminEmails">
<props>
<prop key="administrator">administrator@example.org</prop>
<prop key="support">support@example.org</prop>
<prop key="development">development@example.org</prop>
</props>
</property>

<property name="someList">
<list>
<value>a list element followed by a reference</value>
<ref bean="myDataSource" />
</list>
</property>

<property name="someMap">
<map>
<entry key="an entry" value="just some string"/>
<entry key ="a ref" value-ref="myDataSource"/>
</map>
</property>

<property name="someSet">
<set>
<value>just some string</value>
<ref bean="myDataSource" />
</set>
</property>
</bean>

map的key或者value,或者是集合的value都可以配置为下列之中的一些元素

bean | ref | idref | list | set | map | props | value | null

null 和空字符串的注入

1
2
3
4
5
6
7
8
9
<bean id="..." class="exampleBean">
<property name="email" value=""/>
</bean>

<bean class="ExampleBean">
<property name="email">
<null/>
</property>
</bean>

beans自动装配

Spring 容器可以根据 Bean 之间的依赖关系自动装配

  • 自动装载能够明显的减少指定的属性或者是构造参数。
  • 自动装载可以扩展开发者的对象。比如说,如果需要加一个依赖,依赖就能够不需要开发者特别关心更改配置就能够自动满足。这样,自动装载在开发过程中是极度高效的,不用明确的选择装载的依赖会使系统更加的稳定。

当使用基于XML的元数据配置的时候,开发者可以指定自动装配的方式。通过配置 \<bean/> 元素的 autowire 属性就可以了。自动装载有如下四种方式,开发者可以指定每个 Bean 的装载方式,这样 Bean 就知道如何加载自己的依赖。

模式 解释
no (默认)不装载。Bean的引用必须通过ref元素来指定。对于比较大项目的部署,不建议修改默认的配置,因为特指会加剧控制。在某种程度上来说,默认的形式也说明了系统的结构
byName 通过名字来装配。Spring会查找所有的Bean直到名字和属性相同的一个Bean来进行装载。比如说,如果Bean配置为根据名字来自动装配,它包含了一个属性名字为master(也就是包含一个setMaster(..)方法),Spring就会查找名字为master的Bean,然后用之装载
byType 如果需要自动装配的属性的类型在容器之中存在的话,就会自动装配。如果容器之中存在不止一个类型匹配的话,就会抛出一个重大的异常,说明开发者最好不要使用byType来自动装配那个Bean。如果没有匹配的Bean存在的话,不会抛出异常,只是属性不会配置
constructor 类似于byType的注入,但是应用的构造函数的参数。如果没有一个Bean的类型和构造函数参数的类型一致,那么仍然会抛出一个重大的异常

正常情况下的配置文件 Beans.xml 文件

1
2
3
4
5
6
<bean id="textEditor" class="com.tutorialspoint.TextEditor">
<property name="spellChecker" ref="spellChecker" />
<property name="name" value="Generic Text Editor" />
</bean>

<bean id="spellChecker" class="com.tutorialspoint.SpellChecker"></bean>

如果使用自动装配 “byName”,那么你的 XML 配置文件,byType 和 constructor 也是类似的

1
2
3
4
5
6
<bean id="textEditor" class="com.tutorialspoint.TextEditor" 
autowire="byName">
<property name="name" value="Generic Text Editor" />
</bean>

<bean id="spellChecker" class="com.tutorialspoint.SpellChecker"></bean>

基于注解的配置

注解注入在XML注入之前执行。因此XML配置覆盖了通过这两种方法连接的属性的注解

想在 Spring 应用程序中使用注解,需要在配置文件中声明

1
2
3
4
5
6
7
8
9
10
11
<?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.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<context:annotation-config/>
</beans>

@Required

@Required 注释应用于bean属性设置方法

1
2
3
4
5
6
7
8
public class SimpleMovieLister {
private MovieFinder movieFinder;

@Required
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}

使用 @Autowired

可以将@Autowired注释应用于构造函数

1
2
3
4
5
6
7
8
public class MovieRecommender {
private final CustomerPreferenceDao customerPreferenceDao;

@Autowired
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
this.customerPreferenceDao = customerPreferenceDao;
}
}

也可以将@Autowired注释应用于setter方法

1
2
3
4
5
6
7
8
public class SimpleMovieLister {
private MovieFinder movieFinder;

@Autowired
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}

还可以将注释应用于具有任意名称和多个参数的方法

1
2
3
4
5
6
7
8
9
10
11
12
public class MovieRecommender {
private MovieCatalog movieCatalog;

private CustomerPreferenceDao customerPreferenceDao;

@Autowired
public void prepare(MovieCatalog movieCatalog,
CustomerPreferenceDao customerPreferenceDao) {
this.movieCatalog = movieCatalog;
this.customerPreferenceDao = customerPreferenceDao;
}
}

还可以将@Autowired应用于字段,甚至可以将其与构造函数混合

1
2
3
4
5
6
7
8
9
10
11
public class MovieRecommender {
private final CustomerPreferenceDao customerPreferenceDao;

@Autowired
private MovieCatalog movieCatalog;

@Autowired
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
this.customerPreferenceDao = customerPreferenceDao;
}
}

确保您的目标组件(例如MovieCatalog或CustomerPreferenceDao)是由您用于@ autowiated注释注入点的类型一致声明的。否则,注入可能会由于在运行时没有找到类型匹配而失败。

对于通过类路径扫描找到的xml定义的bean或组件类,容器通常预先知道具体类型。但是,对于@Bean工厂方法,您需要确保声明的返回类型具有足够的表达能力。对于实现多个接口的组件或可能由其实现类型引用的组件,请考虑在工厂方法上声明最特定的返回类型(至少与引用bean的注入点所需的相同)。

还可以从ApplicationContext中提供特定类型的所有bean,方法是将注释添加到需要该类型数组的字段或方法,集合

1
2
3
4
5
6
7
8
9
10
11
public class MovieRecommender {
@Autowired
private MovieCatalog[] movieCatalogs;

private Set<MovieCatalog> movieCatalogs;

@Autowired
public void setMovieCatalogs(Set<MovieCatalog> movieCatalogs) {
this.movieCatalogs = movieCatalogs;
}
}

@Qualifier

当需要对选择过程进行更多控制时,可以使用Spring的@Qualifier注释。可以将限定符值与特定参数关联,缩小类型匹配集,以便为每个参数选择特定的bean

1
2
3
4
5
6
public class MovieRecommender {

@Autowired
@Qualifier("main")
private MovieCatalog movieCatalog;
}

这是xml的配置

1
2
3
4
5
6
7
<bean class="example.SimpleMovieCatalog">
<qualifier value="main"/>
</bean>

<bean class="example.SimpleMovieCatalog">
<qualifier value="action"/>
</bean>

或者是

1
2
3
4
5
<bean id="main" class="example.SimpleMovieCatalog">
</bean>

<bean id="action" class="example.SimpleMovieCatalog">
</bean>