Spring框架(一):IOC容器

IOC容器和Bean

通常一个对象的初始化和依赖注入过程由自身完成,通俗来说,对象实例的生成需要程序员通过new来进行,其内部的一些必要字段需要通过set方法进行注入。这样会导致多个模块之间耦合在一起,例如某个组件需要被多个其它组件使用时,可能难以实现共享。

Spring框架提供了一种较好的实现模块复用和解耦的方式,即控制反转(Inversion of Control, IOC)。IOC原则将对象的初始化、依赖注入、装配和生命周期管理交给IOC容器来处理,即将控制权交由容器。容器根据用户的配置规则来管理容器中的对象,这个配置规则表现为xml配置文件、注解或java代码。

被容器管理的所有对象都被称为Bean。

初始化和使用容器

容器需要根据用户的配置来对容器中的Bean进行初始化,以使用xml配置文件为例,这个配置文件具有如下形式:

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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="dog1" class="com.pojo.Dog">
<!-- collaborators and configuration for this bean go here -->
</bean>
<bean id="..." class="...">
<!-- collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions go here -->
</beans>

配置文件的由一对<beans></beans>标签包含若干<bean>标签,一对<bean>标签就配置了一个对象的生命周期和依赖情况。具体配置方法将在后面介绍。

Spring中通常使用以下代码来获取和使用Spring容器:

1
2
3
4
// 通过xml配置文件获取容器
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 通过id获取容器中的Bean
Dog dog = context.getBean("dog1", Dog.class)

其中applicationContext.xml文件为上面提到的配置文件,使用容器对象的getBean方法可以获取容器中的对象,其中第一个参数为Bean的idname,第二个参数为Bean所属的class,第二个参数是可选的,若不给则需要对返回的对象进行转型。

实际上,Spring还提供一种容器为BeanFactory,它为ApplicationContext的父接口:

1
2
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));
Dog dog = context.getBean("dog1", Dog.class)

BeanFactory接口更为简单,提供的功能也更少,通常情况下使用ApplicationContext接口就行了。

Bean配置

Spring容器管理着众多的Bean,这些Bean通过配置文件来指定容器对它们的管理方式,正如上面所说的在一对<bean>标签间进行定义,Bean可以定义的属性值见下表:

属性 描述
Class Bean所属类的全路径类名,这是属性是必须的
Name 用来标识Bean,也可以使用id属性来标识,区别是id属性是唯一的,name可以有多个
Scope 类的作用域,常用的有单例(singleton)和原型(prototype),默认为单例模式
Constructor arguments 用于依赖注入,通过类的构造器来注入依赖
Properties 用于依赖注入,通过类定义的Setter来注入依赖
Autowiring mode 用于依赖注入,使用自动装配来注入依赖
Lazy initialization mode 延迟初始化,可以控制bean在容器启动时初始化或被请求时初始化
Initialization method 用于配置bean必要属性被设置完后执行的回调
Destruction method 用于配置容器销毁时的回调

依赖注入(DI)

通常,一个项目有若干个对象组成,这些对象之间具有依赖关系,依赖注入(Dependency Injection)是一种设计模式。指对象只通过构造器和Setter来设置本身的依赖。

构造器依赖注入

构造器依赖注入是指容器调用对象定义的有参构造器来实现依赖注入。

1
2
3
4
5
6
package x.y;
public class ThingOne {
public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}
1
2
3
4
5
6
7
8
9
10
<beans>
<!-- 调用有参构造器实现依赖注入,复杂类型使用ref,简单类型使用value -->
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg ref="beanTwo"/>
<constructor-arg ref="beanThree"/>
</bean>

<bean id="beanTwo" class="x.y.ThingTwo"/>
<bean id="beanThree" class="x.y.ThingThree"/>
</beans>

为指定构造器参数和配置文件中参数的对应关系,可以用以下三种方式来配置:

1
2
3
4
5
6
7
8
9
10
11
12
package examples;
public class ExampleBean {
// Number of years to calculate the Ultimate Answer
private final int years;
// The Answer to Life, the Universe, and Everything
private final String ultimateAnswer;

public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
  1. 使用参数类型来实现匹配:
1
2
3
4
5
<bean id="exampleBean" class="examples.ExampleBean">
<!-- 使用type属性来指定,匹配参数类型 -->
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>
  1. 当构造器有多个同类型的参数时,上述方法将失效;故可以使用参数顺序来指定:
1
2
3
4
5
<bean id="exampleBean" class="examples.ExampleBean">
<!-- 使用index属性,匹配参数顺序,注意index从0开始 -->
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>
  1. 使用参数名称来实现匹配:
1
2
3
4
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>

实际注入时,我个人喜欢使用第三种方式,能够避免参数匹配问题,规范命名后能够清除看出参数注入情况。

Setter依赖注入

Setter依赖注入通过容器调用对象定义的Setter方法来实现,这个过程发生在对象实例初始化之后,意味着Setter依赖注入方法可能会覆盖通过构造器注入的结果。

1
2
3
4
5
6
7
8
9
10
11
12
<bean id="exampleBean" class="examples.ExampleBean">
<!-- setter injection using the nested ref element -->
<property name="beanOne">
<ref bean="anotherExampleBean"/>
</property>
<!-- setter injection using the neater ref attribute -->
<property name="beanTwo" ref="yetAnotherBean"/>
<property name="integerProperty" value="1"/>
</bean>

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

一些细节

  1. 参数解析问题。

依赖注入通过<property>construct-arg>标签来实现,当注入的参数为简单类型(如int,long,boolean,String等)时,使用value指定注入值,Spring解析时会将其转化为相应的实际类型;例如指定<property name="integerProperty" value="1"/>,实际上"1"被注入为整数1。当注入参数为复杂类型(指非基本类型)时,通过ref指定注入值。

  1. 集合类型注入。

为了实现对Java中的List,Set,Map以及Properties等集合类型的依赖注入,Spring提供了<list/>,<set/>,<map/>,<props/>等标签:

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">
<!-- results in a setAdminEmails(java.util.Properties) call -->
<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>
<!-- results in a setSomeList(java.util.List) call -->
<property name="someList">
<list>
<value>a list element followed by a reference</value>
<ref bean="myDataSource" />
</list>
</property>
<!-- results in a setSomeMap(java.util.Map) call -->
<property name="someMap">
<map>
<entry key="an entry" value="just some string"/>
<entry key="a ref" value-ref="myDataSource"/>
</map>
</property>
<!-- results in a setSomeSet(java.util.Set) call -->
<property name="someSet">
<set>
<value>just some string</value>
<ref bean="myDataSource" />
</set>
</property>
</bean>
  1. 关于空字符串和Null类型。

Spring将空参数值视为空字符:

1
2
3
4
<bean class="ExampleBean">
<property name="email" value=""/>
<!-- 等同于exampleBean.setEmail(""); -->
</bean>

若想注入null值需要使用特殊的<null/>标签:

1
2
3
4
5
6
<bean class="ExampleBean">
<property name="email">
<null/>
</property>
<!-- 等同于exampleBean.setEmail(null); -->
</bean>
  1. 依赖注入的简写形式。

可以使用p-namespacec-namespace<property>construct-arg>标签进行简写,但需要在头部引入相应的名字空间。

以下为使用常规方式和p-namespace方式进行依赖注入的对比,它们效果一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean name="classic" class="com.example.ExampleBean">
<property name="email" value="someone@somewhere.com"/>
<!-- 常规方式注入email属性 -->
</bean>

<bean name="p-namespace" class="com.example.ExampleBean"
p:email="someone@somewhere.com"/>
<!-- 使用p-namespace方式,需要在头部引入名字空间 -->
</beans>

以下为使用常规方式和c-namespace方式进行依赖注入的对比,它们效果一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="beanTwo" class="x.y.ThingTwo"/>
<bean id="beanThree" class="x.y.ThingThree"/>
<!-- traditional declaration with optional argument names -->
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg name="thingTwo" ref="beanTwo"/>
<constructor-arg name="thingThree" ref="beanThree"/>
<constructor-arg name="email" value="something@somewhere.com"/>
</bean>

<!-- c-namespace declaration with argument names -->
<bean id="beanOne" class="x.y.ThingOne"
c:thingTwo-ref="beanTwo"
c:thingThree-ref="beanThree"
c:email="something@somewhere.com"/>
</beans>

自动装配(Autowire)

通过配置Bean中的autowire属性,能够指定该Bean依赖注入方式为自动装配。能够配置的方式如下表:

模式 描述
no (默认配置),不采用自动装配,Bean中的引用依赖需要自己手动的通过ref来显式配置
byName 通过属性名称来自动装配,配置为此选项时,Spring会寻找和属性名相同名称的bean进行装配。例如,已经在容器注册了一个名为dogDog类型的bean,而另一个名为person的bean含有一个Dog类型的属性(含有setDog()方法),这时候Spring将dog装配给person。 需要注意的是Spring根据set方法名称和bean的名称进行匹配。
byType 通过属性类型来自动装配,配置为此选项时,Spring会寻找和属性类型相同类型的bean进行装配,当存在多个同类型的bean可选时,会抛出致命错误;当没有匹配的bean时,依赖不会被注入(什么也不发生)。
                constructor                   类似于byType,但应用于构造函数的参数,如果没有构造函数参数类型的bean被找到,将抛出错误。

总结

本篇文章主要是我根据官方手册进行的总结,由于手册编写十分细致,故本文很多例子都是官方手册中截取的,部分表格也是我翻译官方手册的。本文主要是介绍了Spring框架中的IOC容器相关的概念,包括容器创建使用、Bean的概念以及依赖注入的概念和配置方法。

参考文献

  1. https://docs.spring.io/spring-framework/docs/current/reference/html/core.html