知易行难,由于之前很少写技术博客,对于写一篇“还算正式”的技术文章需要花费多少时间并不清楚。第一篇笔记整理了将近一周,这远远超出了我的预期。写完的时候,不免想起那些经常写博客的人一定花费了很多的心思在上面,因而心生佩服。很多事情都是这样,只有做过才会知道其中的不容易。
1、Spring配置概述
1.1 Spring容器高层视图
Spring容器根据各种形式的Bean配置信息在容器内部建立Bean定义注册表,然后根据注册表加载、实例化Bean,并建立Bean和Bean的依赖关系。Bean的配置信息是Bean的元数据信息,Bean的元数据信息在Spring容器中的内部对应物是由一个一个BeanDefinition形成的Bean注册表,Spring实现了Bean元数据信息内部表示和外部表示的解耦。如下图:
1.2 基于XML Schema的配置
基于XML的配置,Spring2.0以后开始使用Schema格式,相比Spring1.0的DTD格式,Schema让不同类型的配置拥有了自己的命名空间,使配置文件更具扩展性。下面看一个简单的示例:
Schema在文档根节点中通过xmlns对文档中的命名空间进行声明。在上面的代码中定义了3个命名空间:
- 默认命名空间:它没有空间名,用于Spring Bean的定义;
- xsi命名空间:这个命名空间用于为每个文档中命名空间指定相应的Schema样式文件,是标准组织定义的标准命名空间;
- aop命名空间:这个命名空间是Spring配置Aop的命名空间,是用户自定义的命名空间;
命名空间的定义分为两个步骤:第一步指定命名空间的名称,第二部指定命名空间Schema文档样式文件的位置,用空格或回车换行进行分割。在第二步中,指定命名空间的Schema文件地址有两个用途:XML解析器可以获取Schema文件并对文档进行格式合法性验证;在开发环境下,IDE可以引用Schema文件对文档编辑提供诱导功能。
2、Bean的基本配置
2.1 Bean配置的基础知识
一般情况下,Spring IoC容器中的一个Bean即对应配置文件中的一个<bean>
,其中id
为这个Bean的名称,通过容器的getBean(id
)即可获取对应的Bean,在容器中起到定位查找的作用。class
属性指定了Bean对应的实现类。
<bean id="foo" class="com.hhxs.domain.Foo">
id
在IoC的容器中必须是唯一的,此外id的命名也需要满足XML对id
的命名规范(id
是XML规定的特殊属性):必须以字母开始,后面可以是字母、数字、连字符、下划线、句号、冒号的完整结束的符号,逗号和空格这些非完整结束符是非法的。name
属性没有字符上的限制,几乎可以使用任何字符。另外id
和name
都可以指定多个名字,名字之间用逗号、分号或者空格进行分隔:
<bean name="foo,#foo,$foo" class="com.hhxs.domain.Foo">
可以使用getBean(“foo”)、getBean(“#foo”)、getBean(“$foo”)获取IoC容器中的Foo Bean。Spring配置文件中不允许出现两个相同id的<bean>
,但可以出现两个相同name的<bean>
,多个name相同的<bean>
,通过getBean(beanName)获取Bean时,将返回最后声明的那个Bean。如果id
和name
两属性都未指定,如<bean class="com.baobaotao.simple.Car">
,Spring自动将全限定类名作为Bean的名称。
2.2 注入方式
Spring支持两种依赖注入方式,分别是属性注入和构造函数注入。除此之外,Spring还支持工厂方法注入方式。
2.2.1 属性注入
属性注入要求Bean提供一个默认的构造函数,并为需要注入的属性提供对应的Setter
方法。Spring先调用Bean的默认构造函数实例化Bean对象,然后通过反射的方式调用Setter
方法注入属性值。JavaBean的属性命名规范:xxx的属性对应setXxx()方法。一般情况下,Java的属性变量名都以小写字母起头,特殊情况下,也允许大写字母起头的属性变量名,但是必须满足“变量的前两个字母要么全部大写,要么全部小写”。
下面看一个例子:
public class Car {
private String brand;
private String color;
private int maxSpeed;
public void setBrand(String brand) {
System.out.println("调用setBrand()设置属性。");
this.brand = brand;
}
public void setColor(String color) {
this.color = color;
}
public void setMaxSpeed(int maxSpeed) {
this.maxSpeed = maxSpeed;
}
......
}
// Spring配置文件中的属性注入配置
<bean id="car" class="com.hhxs.bbt.web.Car" >
<property name="brand"><value>红旗CA72</value></property>
<property name="color"><value>红色</value></property>
<property name="maxSpeed"><value>200</value></property>
</bean>
Bean的每一个属性对应一个标签,如maxSpeed对应setMaxSpeed()。
提示: 默认构造函数是不带参的构造函数。Java语言规定如果类中没有定义任何构造函数,则JVM自动为其生成一个默认的构造函数。反之,如果类中显示定义了构造函数,则JVM不会为其生成默认的构造函数。
2.2.2 构造函数注入
构造函数注入方式是除属性注入之外的另一种常用的注入方式,它保证一些必要的属性在Bean实例化时就得到设置,它保证了Bean实例在实例化后就可以使用。其中构造参数注入又分为,按类型匹配入参、按索引匹配入参、联合使用类型和索引匹配入参等。这是因为Java反射机制并不会记住构造函数的入参名,我们无法通过指定构造函数的入参名进行构造函数注入的配置,而只能通过入参类型和索引信息间接确定构造函数配置项和入参的对应关系。下面直接看一个联合使用类型和索引匹配入参的代码片段:
...
public Car(String brand, String corp, double price) {
this.brand = brand;
this.corp = corp;
this.price = price;
}
public Car(String brand, String corp, int maxSpeed) {
this.brand = brand;
this.corp = corp;
this.maxSpeed = maxSpeed;
}
...
// Spring配置文件中的配置
<!-- 对应Car(String brand, String corp, int maxSpeed)构造函数 -->
<bean id="car" class="com.hhxs.bbt.web.Car" >
<constructor-arg index="0" type="java.lang.String">
<value>红旗CA72</value>
</constructor-arg>
<constructor-arg index="1" type="java.lang.String">
<value>中国一汽</value>
</constructor-arg>
<constructor-arg index="2" type="int">
<value>200</value>
</constructor-arg>
</bean>
如果构造函数没有歧义的话,上面配置文件中的type
属性和index
属性均可去除。另外如果,Bean构造函数入参的类型是可辨别的(非基础数据类型且入参类型各异),构造函数注入的配置也是可以不提供类型和索引的信息,但是为了避免潜在配置歧义问题,如果Bean存在多个构造函数,使用显示指定index和type属性不失为一种良好的配置习惯。
2.2.3 工厂注入方法
Spring IoC容器以框架的方式提供工厂方法的功能,并以透明的方式开放给开发者,所以很少需要手工编写基于工厂方法的类。正因为此,工作方法对于实际编码的重要性大大降低,它将慢慢地淡出开发人员的视野,这里不在说明。
2.2.4 注入方式的选择
支持构造函数注入方式的理由:
- 构造函数可以保证一些重要的属性在Bean实例化时就设置好,避免因为一些重复属性没有提供,导致一个无用Bean实例的情况;
- 不需要为每个属性提供Setter方法,减少了类的方法个数;
- 可以更好地封装类变量,不需要为每个属性指定Setter方法,避免外部错误的调用。
支持属性注入方式的理由:
- 如果一个类的属性众多,构造函数的签名将变成一个庞然大物,可读性很差;
- 灵活性不够,在有些属性是可选的情况下,如果通过构造函数注入,也需要为可选的参数提供一个null值;
- 如果有多个构造函数,需要考虑配置文件和具体构造函数匹配歧义的问题,配置上相对复杂;
- 构造函数不利于类的继承和扩展,因为子类需要引用到父类复杂的构造函数;
- 构造函数注入有时会造成循环依赖的问题。
两种方式各有优缺点,生产环境中要根据实际情况使用。除了工厂方法的注入方式已不再使用,属性注入和构造函数注入方式都是比较常用的。
2.3 简化配置方式
在之前的例子里我们采用完整配置格式的配置方式,也许你已经发现这种方式显得比较拖沓。如果系统中拥有大量的类需要配置,我们一定会感到崩溃。还好Spring为我们提供了简化的配置方式。如果没有用到完整配置格式的特殊功能,我们一般都只使用简化的配置方式。
字面值属性:
配置类型 | 简化前 | 简化后 |
---|---|---|
字面值属性 | <property name="maxSpeed"> <value>好</value> </property> | <property name="maxSpeed" value="200"/> |
构造函数参数 | <constructor-arg type="java.lang.String"> <value>红旗CA72</value> </constructor-arg> | <constructor-arg type="java.lang.String" value="红旗CA72"/> |
集合元素 | <map> <entry> <key><value>AM</value></key> <value>会见客户<value> </entry> </map> | <map> <entry key="AM" value="会见客户"/> </map> |
引用对象属性:
配置类型 | 简化前 | 简化后 |
---|---|---|
字面值属性 | <property name="car"> <ref bean="car"></ref> </property> | <property name="car" ref="car"/> |
构造函数参数 | <constructor-arg> <ref bean="car"/> </constructor-arg> | <constructor-arg ref="car"/> |
集合元素 | <map> <entry> <key><ref bean="keyBean"/></key> <ref bean="valueBean"/> </entry> </map> | <map> <entry key-ref="keyBean" value-ref="valueBean"/> </map> |
使用P命名空间:
为了简化XML文件的配置,越来越多的XML文件采用属性而非子元素配置信息。Spring从2.5版本开始引入了一个新的p命名空间,可以通过<bean>
元素属性的方式配置Bean的属性。
未采用P命名空间前:
<?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-3.0.xsd">
<bean id="car" class="com.hhxs.bbt.web.Car" >
<property name="brand" value="红旗CA72" />
<property name="color" value="红色" />
<property name="maxSpeed" value="200" />
</bean>
<bean id="boss" class="com.hhxs.bbt.web.Boss">
<property name="car" ref="car"/>
</bean>
</beans>
采用P命名空间后:
<?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:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="car" class="com.hhxs.bbt.web.Car"
p:brand="红旗CA72"
p:color="红色"
p:maxSpeed="200"/>
<bean id="boss" class="com.hhxs.bbt.web.Boss"
p:car-ref="car"/>
</beans>
未采用p
命名空间前<bean>
使用<property>
子元素配置Bean的属性,采用p
命名空间后,采用<bean>
的元素属性配置Bean的属性。
- 对于字面值属性,其格式为:
p:<属性名>="xxx"
- 对于引用对象的属性,其格式为:
p:<属性名>-ref=“xxx”
由于p
命名空间中的属性名是可变的,所以p
命名空间没有对应的Scheam定义文件,也就不需要在xsi:schemaLocation中为p
命名空间指定Schema定义文件。
3. 自动装配
Spring IoC容器了解容器中所有Bean的配置信息,此外通过Java反射机制还可以获取已知实现你类的结构信息(如构造函数方法的结构、属性等信息)。掌握容器中所有Bean的这些信息后,Spring IoC容器就可以按照某种规则对容器中的Bean进行自动装配,而无须我们通过显示的方式进行配置。
<bean>
元素提供了一个指定自动个装配类型的属性:autowire=”<自动装配类型>”。Spring提供了4种自动装配类型:
自动装配类型 | 说明 |
---|---|
byName | 根据名称进行自动匹配。假设Boss有一个名为car的属性,如果容器中刚好有一个名为car的Bean,Spring就会自动将其装配给Boss的car属性。 |
byType | 根据类型进行自动匹配。假设Boss有一个car类型的属性,如果容器中刚好有一个car类型的Bean,Spring就会自动将其装配给Boss的这个属性。 |
constructor | 与ByType类似,只不过它是针对构造函数注入而言的,如果Boss有一个构造函数,构造函数包含一个Car类型的入参,如果容器中有一个Car类型的Bean,则Spring将自动把这个Bean作为Boss构造函数的入参,如果容器中没有找到和构造函数入参匹配类型的Bean,Spring将抛出异常。 |
autodetect | 根据Bean的自省机制决定采用byType还是constructor进行自动装配:如果Bean提供了默认的构造函数,则采用byType; 否则采用constructor |
<beans>
元素标签中的default-autowire
属性可以配置全局自动匹配,default-autowire
属性的默认值为no
,表示不启用自动装配,其他几个配置值为:byName、byType、constructor、autodetect,意义同上面的表格。注意:<beans>
中定义的自动装配策略可以被<bean>
的自动装配策略覆盖。
注意: 自动装配机制在减轻配置工作量的同时也会造成配置文件中Bean之间关系不清晰的问题,生产使用时要根据实际项目情况。
4. 整合多个配置文件
对于一个大型应用来说,可能有多个XML配置文件,在启动时,可以通过一个String数组指定这些配置文件。Spring还允许我们通过<import>
将多个配置文件引入到一个文件中,进行配置文件的集成。如下,beans2.xml引入了beans1.xml配置文件:
<import resource="classpath:beans1.xml"/>
<bean id="boss1" class="com.hhxs.bbt.web.Boss" p:car-ref="car1"/>
<bean id="boss2" class="com.hhxs.bbt.web.Boss" p:car-ref="car2"/>
假设我们已经在beans1.xml中配置了car1和car2的Bean,通过<import>
引入beans1.xml,beans2.xml就拥有了完整的配置信息。Spring容器仅需要通过beans2.xml就可以加载到所有的配置信息了。
5. Bean作用域
在配置文件中定义Bean时,除了可以配置Bean的属性值以及相互之间的依赖关系,还可以定义Bean的作用域。
Bean的作用域类型:
类别 | 说明 |
---|---|
singleton | 在Spring IoC容器中仅存在一个Bean实例,Bean以单实例的方式存在 。在Spring中,Bean的默认作用域为singleton。如果不希望在容器启动时提前实例化Bean,可以通过lazy-init属性进行控制。 |
prototype | 每次从容器中调用Bean时,都返回一个新的实例,即每次调用getBean()时,相当于执行new XxxBean()的操作 |
request | 每次HTTP请求都会创建一个新的Bean。该作用域仅适用于WeApplicationContext环境。 |
session | 同一个HTTP Session共享一个Bean,不同HTTP Session使用不同的Bean。该作用域仅适用于WeApplicationContext环境。 |
globalSession | 同一个全局Session共享一个Bean, 一般用于Porlet应用环境。该作用域仅适用于WeApplicationContext环境。 |
使用方式是: scope="<作用域类型>"