一、简介
Spring是一个从实际开发中抽取出来的框架,因此它完成了大量开发中的通用步骤,留给开发者的仅仅是与特定应用相关的部分,从而大大提高了企业应用的开发效率。Spring为企业应用开发提供了一个轻量级的解决方案,包括:基于依赖注入的核心机制、基于AOP的声明式事务管理、与多种持久层技术的整合,以及优秀的Web MVC框架等。Spring贯穿表现层、业务层、持久层。Spring并不取代已有的框架,而是以高度的开放性与它们无缝整合。
Spring具有如下优点:
- 低侵入式设计,代码的污染极低
- 独立于各种应用服务器,基于Spring框架的应用,可以真正实现Write Once,Run AnyWhere的承诺
- Spring的IoC容器降低了业务对象替换的复杂性,提高了组件之间的解耦
- Spring的AOP支持允许将一些通用任务如安全、事务、日志等进行集中式处理,从而提供了更好的复用
- Spring的ORM和DAO提供了与第三方持久层框架的良好整合,并简化了底层的数据库访问
- Spring的高度开放性,并不强制应用完全依赖于Spring,开发者可自由选用Spring框架的部分或全部
使用Spring框架时,必须使用Spring Core Container(即Spring容器),它代表了Spring框架的核心机制,Spring Core Container主要由rg.springframeworke.core、org.springframeworke.beans、org.springframeworke.context和org.springframeworke.expression四个包及其子包组成,主要提供Spring IOC容器支持。其中org.springframeworke.expression及其子包是Spring 3.0新增的,它提供了Spring Expression Language支持。Spring核心容器必须依赖于common-logging的JAR包。
Spring 4.0主要是为了支持最新的Java 8和Servlet 3.0 规范,也为核心IoC容器增强了一些注解。
Spring把容器中的一切对象统称为Bean,与Java Bean必须遵守一些特定的规范不同的是,Spring对Bean没有任何要求,Spring使用XML配置文件来管理容器中的Bean,同样也可使用注解的方式。同时Spring对XML配置文件的文件名没有任何要求。
官网文档可参考:III. Core Technologies —> 6. The IoC container
1.1 基本配置即使用
在Spring配置文件中配置Bean时,class属性的值必须是Bean实现类的完整类名(必须带包名),不能是接口,也不能是抽象类(除非有特殊配置),否则Spring无法使用反射创建该类的实例。配置文件中配置Bean需要使用<bean.../>元素进行配置,如下:
<bean id="id" class""></bean>
配置Bean时其还包括一个<property.../>子元素,它驱动Spring在底层以反射执行一次setter方法。其中<property.../>的name属性值决定执行那个setter方法,而value或ref属性决定执行setter方法的传入参数。
- 如果传入参数是基本类型及其包装类、String等类型,则使用value属性指定传入参数
- 如果以容器中其他Bean作为传入参数,则使用ref属性指定传入参数
注:<bean.../>元素驱动Spring调用构造器创建对象;<property.../>元素驱动Spring执行setter方法。这两步是先后执行的,中间几乎没有任何间隔。
如果已配置Bean,则可以通过Spring容器来访问容器中的Bean,ApplicationContext是Spring容器最常用的接口,该接口有如下两个实现类:
- ClassPathXMLApplicationContext:从类加载路径下搜索配置文件,并根据配置文件来创建Spring容器
- FileSystemXMLApplicationContext:从文件系统的相对路径下或绝对路径下去搜索配置文件,并根据配置文件来创建Spring容器。
注:对于Java项目而言,类加载路径总是稳定的,因此通常总是使用ClassPathXMLApplicationContext创建Spring容器。
Spring容器获取Bean对象主要有如下两个方法:
- Object getBean(String id):根据容器中Bean的id来获取指定Bean,获取Bean之后需要进行强制类型转换
- T getBean(String name,Class<T> requiredType):根据容器中Bean的di来获取指定Bean,但该方法带一个泛型参数,因此获取Bean之后无须进行强制转换
1.2 简单示例
public class Axe {
public String chop() {
return "使用斧头砍柴";
}
}
public class Person {
private Axe axe;
// 设值注入所需的setter方法
public void setAxe(Axe axe) {
this.axe = axe;
}
public void useAxe() {
System.out.println("我打算去砍点柴火!");
// 调用axe的chop()方法,
// 表明Person对象依赖于axe对象
System.out.println(axe.chop());
}
}
配置文件(beans.xml):
<?xml version="1.0" encoding="GBK"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">
<!-- 配置名为person的Bean,其实现类是org.crazyit.app.service.Person类 -->
<bean id="person" class="org.crazyit.app.service.Person">
<!-- 控制调用setAxe()方法,将容器中axe Bean作为传入参数 -->
<property name="axe" ref="axe"/>
</bean>
<!-- 配置名为axe的Bean,其实现类是org.crazyit.app.service.Axe类 -->
<bean id="axe" class="org.crazyit.app.service.Axe"/>
<!-- 配置名为win的Bean,其实现类是javax.swing.JFrame类 -->
<bean id="win" class="javax.swing.JFrame"/>
<!-- 配置名为date的Bean,其实现类是java.util.Date类 -->
<bean id="date" class="java.util.Date"/>
</beans>
测试类:
public class BeanTest {
public static void main(String[] args) throws Exception {
// 创建Spring容器
AbstractApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
// 获取id为person的Bean
Person p = ctx.getBean("person", Person.class);
// 调用useAxe()方法
p.useAxe();
ctx.close();
}
}
我们可以看到我们没有使用new的方式创建对象,也没有使用工厂创建,但是仍然可以实现正常输出。当然测试的时候也遇到了java.lang.NoClassDefFoundError: org/apache/commons/logging/LogFactory错误,解决方法可以参考:开发过程中的小错误整理
注意:上面的BeanTest会提示一个警告《Resource leak: 'ctx' is never closed》,原因是 在非Web应用中,手工加载Spring IoC容器,不能用ApplicationContext,要用AbstractApplicationContext。用完以后要记得调用ctx.close()关闭容器。如果不记得关闭容器,最典型的问题就是数据库连接不能释放
二、Spring的核心机制:依赖注入
Spring框架的核心功能有两个:
- Spring容器作为超级大工厂,负责创建、管理所有Java对象,这些Java对象被称为Bean
- Spring容器管理容器中Bean之间的依赖关系,Spring使用一种被称为“依赖注入”的方式来管理Bean之间的依赖关系
使用依赖注入,不仅可以为Bean注入普通的属性值,还可以注入其他Bean的引用。通过这种依赖注入,JavaEE应用中的各种组件不需要以硬编码方式耦合在一起,甚至无需使用工厂模式。依赖注入达到的效果,类似于“共产主义”,当某个Java实例需要其他Java实例时,系统自动提供所需要的实例,无需程序显示获取。依赖注入时一种优秀的解耦方式,依赖注入让Spring的bean以配置文件组织在一起,而不是以硬编码的方式耦合在一起。
官网文档可参考:III. Core Technologies —> 6.4. Dependencies -> 6.4.1. Dependency Injection
2.1 依赖注入简述
控制反转(Inversion of Control,IoC)
依赖注入(Dependency Injection)
这两个概念虽然名称不同,其含义是完全相同的,只是描述的角度不同而已。当Java对象(调用者)需要调用另一个Java对象(被依赖对象)的方法时,在传统模式下通常由如下两种做法:
- 原始做法:调用者主动创建被依赖对象,然后在调用被依赖对象的方法,简单来说就是new的方式。(硬编码耦合)
- 简单工厂模式:调用者先找到被依赖对象的工厂,然后主动通过工厂去获取被依赖对象,最后再调用被依赖对象的方法。(调用组件与依赖对象工厂耦合)
对于简单工厂模式:
- 调用者面向被依赖对象的接口编程
- 将被依赖对象的创建交给工厂完成
- 调用者通过工厂来获得被依赖组件。
简单工厂模式满足上面三个要点,这样可以保证调用者只需要与被依赖对象的接口耦合,这就避免了类层次的硬编码耦合。但是也有其缺点,调用组件需要主动通过工厂去获取被依赖对象,这就会带来调用组件与被依赖对象工厂的耦合。
使用Spring框架之后,调用者无须主动获取被依赖对象,调用者只要被动接受Spring容器为调用者的成员变量赋值即可(只要配置一个<property.../>子元素,Spring就会执行对应的setter方法为调用者的成员变量赋值)。
使用Spring框架之后的两个主要改变:
- 程序无须使用new调用构造器去创建对象。所有的Java对象都可交给Spring容器去创建
- 当调用者需要调用被依赖对象的方法时,调用者无须主动获取被依赖对象,只要等待Spring容器注入即可
依赖注入通常由如下两种:
- 设值注入:IoC容器使用成员变量的setter方法来注入被依赖对象
- 构造注入:IoC容器使用构造器来注入被依赖对象
2.2 设值注入
设值注入是指IoC容器通过成员变量的setter方法来注入被依赖对象。这种方式简单、直观,因而在Spring的依赖注入里大量使用。
Spring推荐面向接口编程。不管是调用者,还是被依赖对象,都应该为之定义接口,程序应该面向它们的接口,而不是面向实现类编程,这样以便程序后期的升级、维护。
下面对上面的简单示例进行改写,使之更加规范,如下:
public interface Axe {
// Axe接口里有个砍的方法
public String chop();
}
public interface Person {
// 定义一个使用斧子的方法
public void useAxe();
}
public class SteelAxe implements Axe {
public String chop() {
return "钢斧砍柴真快";
}
}
public class StoneAxe implements Axe {
public String chop() {
return "石斧砍柴好慢";
}
}
public class Chinese implements Person {
private Axe axe;
// 设值注入所需的setter方法
public void setAxe(Axe axe) {
this.axe = axe;
}
// 实现Person接口的useAxe方法
public void useAxe() {
// 调用axe的chop()方法,
// 表明Person对象依赖于axe对象
System.out.println(axe.chop());
}
}
到现在为止,程序并不知道Chinese类和哪个Axe实例耦合,Spring需要使用XML配置文件来指定实例之间的依赖关系。配置如下:
<!-- Spring配置文件的根元素,使用spring-beans-4.0.xsd语义约束 -->
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">
<!-- 配置chinese实例,其实现类是Chinese类 -->
<bean id="chinese" class="org.crazyit.app.service.impl.Chinese">
<!-- 驱动调用chinese的setAxe()方法,将容器中stoneAxe作为传入参数 -->
<property name="axe" ref="stoneAxe"/>
</bean>
<!-- 配置stoneAxe实例,其实现类是StoneAxe -->
<bean id="stoneAxe" class="org.crazyit.app.service.impl.StoneAxe"/>
<!-- 配置steelAxe实例,其实现类是SteelAxe -->
<bean id="steelAxe" class="org.crazyit.app.service.impl.SteelAxe"/>
</beans>
测试如下:
public class BeanTest {
public static void main(String[] args) throws Exception {
// 创建Spring容器
AbstractApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
// 获取chinese 实例
Person p = ctx.getBean("chinese", Person.class);
// 调用useAxe()方法
p.useAxe();
ctx.close();
}
}
在配置文件中,Spring配置Bean实例通常会指定两个属性:
- id:指定Bean的唯一标识,Spring根据id属性值来管理Bean,程序通过id属性值来访问该Bean实例
- class:指定该Bean的实现类,此处不可再使用接口,必须使用实现类,Spring容器会使用XML解析器读取该属性值,并利用反射来创建该实现类的实例。
配置Bean时其还包括一个<property.../>子元素,它驱动Spring在底层以反射执行一次setter方法。其中<property.../>的name属性值决定执行那个setter方法,而value或ref属性决定执行setter方法的传入参数。
- 如果传入参数是基本类型及其包装类、String等类型,则使用value属性指定传入参数
- 如果以容器中其他Bean作为传入参数,则使用ref属性指定传入参数
Person实例不仅不需要了解Axe实例的实现类,甚至无需了解Axe的创建过程。Spring根据配置文件的指定,创建Person实例时,不仅创建了Person对象,并为该对象注入它所依赖的Axe实例。如果有一天需要改变Axe的实现,我们只需要更改配置文件中的配置即可,如下:
添加新的Bean实例:
<!-- 配置steelAxe实例,其实现类是SteelAxe -->
<bean id="steelAxe" class="org.crazyit.app.service.impl.SteelAxe"/>
修改Chinese的配置如下:
<property name="axe" ref="steelAxe"/>
从上面的示例可以看出Chinese实例与具体的Axe实现类没有任何关系,Chinese实例仅仅与Axe接口耦合,这就保证了chinese实例与Axe实例之间的松耦合——这也是Spring面向接口编程的原因。
Bean与Bean之间的依赖关系由Spring管理,Spring采用setter方法为目标Bean注入所依赖的Bean,这种方式被称为设值注入。
Spring IoC容器的三个基本要点:
- 应用程序的各组件面向接口编程。面向接口编程可以将组件之间的耦合关系提升到接口层次,从而有利于项目后期的扩展
- 应用程序的各组件不再有程序主动创建,而是由Spring容器来负责产生并初始化
- Spring采用配置文件或注解来管理Bean的实现类、依赖关系,Spring容器则根据配置文件或注解,利用反射来创建实例,并为之注入依赖关系
2.3 构造注入
在构造实例时,已经为其完成了依赖关系的初始化。这种利用构造器来设置依赖关系的方式,被称为构造注入。通俗来说,就是驱动Spring在底层以反射方式执行带指定参数的构造器,当执行带参数的构造器时,就可利用构造器参数对成员变量执行初始化——这就是构造注入的本质。由于使用了有参书的构造器创建实例,所以当Bean实例被创建完成后,该Bean的依赖关系也已经设置完成。
Spring在配置文件的<bean.../>元素中使用<constructor-arg.../>子元素来代表一个构造器参数,<constructor-arg.../>可以出现多次,也就代表多个参数。
与设值注入区别在于创建实例中依赖的属性的时机不同——设值注入是先通过无参数的构造器创建一个Bean实例,然后调用对应的setter方法依赖注入依赖关系;而构造注入则直接调用有参数的构造器,当Bean实例创建完成后,已经完成了依赖关系的注入。
配置<constructor-arg.../>元素时可指定一个index属性,用于指定该构造参数值将作为地基和构造参数值。例如,指定index=“0”,表明该构造参数值将作为第一个构造参数值。
2.3.1 疑问解析
不知道看到这里大家有没有疑问,加入我的实例中的构造函数中的参数类型顺序有多种的情况下,Spring的运行结果会怎样?我针对这个疑问做了如下测试:
测试实例:
public class Ceshi {
String a;
int b;
public Ceshi(String a,int b) {
this.a=a;
System.out.println("第一种构造函数,其顺序为String、int");
this.b=b;
}
public Ceshi(int a,String b) {
this.a=b;
System.out.println("第二种构造函数,其顺序为int、String");
this.b=a;
}
public void display(){
System.out.println("a="+a+"\nb="+b);
}
}
<1> 不配置index和type属性的情况
1. 配置Bean采用int、String的顺序如下:
<bean id="ceshi" class="org.crazyit.app.service.impl.Ceshi">
<constructor-arg value="123456"></constructor-arg>
<constructor-arg value="字符串"></constructor-arg>
</bean>
运行结果如下:
第二种构造函数,其顺序为int、String
a=字符串
b=123456
2. 配置Bean采用String、int的顺序如下:
第一种构造函数,其顺序为String、int
a=字符串
b=123456
结论:Spring在没有指定index和type的情况下会根据字符串的实际类型,进行智能的区分并选择相应的构造器。
那么如果构造函数只有一个的情况下会正常输出吗?
答案是否定的,如果没有找到相对应顺序的构造函数,则直接报错,出错信息如下:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'ceshi' defined in class path resource [beans.xml]。。。
<2> 采用index指定顺序的情况
配置文件与上类同,就不写了,运行结果是正常输出。那么构造函数只有一个的情况下,其指定的顺序与构造器指定类型的顺序不同会怎样?答案是会报与上面相同的异常。
<3> 采用type指定类型况
显而易见,如果我们指定的类型正确,并且构造函数中可以接受,那么肯定正常输出,测试结果:指定了type的情况下,如果有多个构造函数可以匹配,其只使用第一个构造函数,顺序由Spring进行自动调节。那么指定的type与其类型不同,会怎么样呢?示例如下:
<bean id="ceshi" class="org.crazyit.app.service.impl.Ceshi">
<constructor-arg value="123456" type="String"></constructor-arg>
<constructor-arg value="字符串" index="index"></constructor-arg>
</bean>
答案是报错,报错信息如下:
org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Unexpected failure during bean definition parsing
2.4 两种注入方式的对比
这两种依赖注入方式并没有绝对的好坏,只是适应的场景有所不同。
设值注入优点:
- 与传统的JavaBean的写法更相似,程序开发人员更容易理解、接受。通过setter方法设定依赖关系显得更加直观、自然
- 对于复杂的依赖关系,如果采用构造注入,会导致构造器过于臃肿,难以阅读。Spring在创建Bean实例时,需要同时实例化其依赖的全部实例,因而导致性能下降。而设值注入,则能避免这些问题
- 尤其是在某些成员变量可选的情况下,多参数的构造器更加笨重
构造注入优点:
- 构造注入可以在构造器中决定依赖关系的注入顺序,优先依赖的优先注入。例如组件中其他依赖关系的注入,通常需要依赖于Datasource的注入。采用构造注入,可以在代码中清晰地决定注入顺序
- 对于依赖关系无须变化的Bean,构造注入更有用处。因为没有setter方法,所有的依赖关系全部在构造器内设定。因此,无须担心后续的代码对依赖关系产生破坏
- 依赖关系只能在构造器中设定,则只有组件的创建者才能改变组件的依赖关系。对组件的调用者而言,组件的内部的依赖关系完全透明,更符合高内聚的原则
建议采用以设值注入为主,构造注入为辅的注入策略。对于依赖关系无须变化的注入,尽量采用构造注入;而其他依赖关系的注入,则考虑采用设值注入。
三、使用Spring容器
Spring有两个核心接口:BeanFactory和ApplicationContext,其中ApplicationContext是BeanFactory的子接口。它们都可代表Spring容器,Spring容器是生成Bean实例的工厂,并管理容器中的Bean。Java程序面向接口编程,无须关心Bean实例的实现类;但Spring容器负责创建Bean实例,因此必须精确知道每个Bean实例的实现类,故Spring配置文件必须指定Bean实例的实现类。
官网文档可参考:III. Core Technologies —> 6. The IoC container
3.1 Spring容器
Spring容器最基本的接口就是BeanFactory。BeanFactory负责配置、创建、管理Bean,它还有一个子接口:ApplicationContext,因此也被称为Spring上下文。Spring容器还负责管理Bean与Bean之间的依赖关系。
BeanFactory接口包含如下几个基本方法:
- boolean containsBean(String name):判断Spring容器是否包含id为name的Bean实例
- <T> T getBean(Class<T> requiredType):获取Spring容器中属于requiredType类型的、唯一的Bean实例
- Object getBean(String name):返回容器id为name的Bean实例
- <T> T getBean(String name,Class requiredType):返回容器中id为name,并且类型为requiredType的Bean
- Class<?>getType(String name):返回容器中id为name的Bean实例的类型
BeanFactory常用的实现类是DefaultListableBeanFactory。
ApplicationContext是BeanFactory的子接口,对于大部分JavaEE应用而言,使用它作为Spring容器更为方便。其常用实现类是FilSystemXmlApplicationContext、ClassPathXmlApplicationContext和AnnotationConfigApplicationContext。如果在Web应用中使用Spring容器,则通常由XmlWebApplicationContext、AnnotationConfigWebApplicationContext两个实现类。
大部分JavaEE应用,可在启动Web应用时自动加载ApplicationContext实例,接受Spring管理的Bean无须知道ApplicationContext的存在,一样可以利用ApplicationContext的管理。
对于独立的应用程序,可通过如下方法来实例化BeanFactory:
// 搜索类加载路径下的beans.xml文件创建Resource对象
Resource isr = new ClassPathResource(“beans.xml”);
// 创建默认的BeanFactory容器
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// 让默认的BeanFactory容器加载isr对应的XML配置文件
new XmlBeanDefinitionReader(beanFactory).loadBeanDefinition(isr);
或者采用如下代码来创建BeanFactory:
// 搜索文件系统的当前路径下的beans.xml文件创建Resource对象
Resource isr = new FileSystemResource(“beans.xml”);
// 创建默认的BeanFactory容器
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// 让默认的BeanFactory容器加载isr对应的XML配置文件
new XmlBeanDefinitionReader(beanFactory).loadBeanDefinition(isr);
注意:Resource接口是Spring提供的资源访问接口,通过使用该接口,Spring能以简单、透明的方式访问磁盘、类路径以及网络上的资源。
如果应用需要加载多个配置文件来创建Spring容器,则应该采用BeanFactory的子接口ApplicationContext来创建BeanFactory的实例。ApplicationContext接口包含FilSystemXmlApplicationContext、ClassPathXmlApplicationContext两个常用的实现类。
如果需要同时加载多个XML配置文件来创建Spring容器,则可以采用如下方式:
// 以类加载路径下的beans.xml、service.xml文件创建ApplicationContext
ApplicationContext appContext = new ClassPathXmlApplicationContext("beans.xml","service.xml");
当然也可支持从文件系统的相对路径或绝对路径来搜索配置文件,只要使用FilSystemXmlApplicationContext即可,如下面的程序片段所示:
// 以类加载路径下的beans.xml、service.xml文件创建ApplicationContext
ApplicationContext appContext = new FilSystemXmlApplicationContext("beans.xml","service.xml");
由于ApplicationContext本身就是BeanFactory的子接口,因此ApplicationContext完全可以作为Spring容器来使用,而且功能更前。当然,如果有需要,也可以把ApplicationContext实例赋给BeanFactory变量。
3.2 使用ApplicationContext
大部分时候,都不会使用BeanFactory实例作为Spring容器,而是使用ApplicationContext实例作为容器,因此也把Spring容器称为Spring上下文。ApplicationContex是BeanFactory接口的子接口,它增强了BeanFactory的功能。
ApplicationContext允许以声明式方式操作容器,无须手动创建它。可利用如ContextLoader的支持类,在Web应用启动时自动创建ApplicationContext。当然也可采用编程方式创建ApplicationContext。
除了提供BeanFactory所支持的全部功能外,ApplicationContext还有如下额外的功能:
- ApplicationContext默认会预初始化所有的singleton Bean,也可通过配置取消预初始化
- ApplicationContext继承MessageSource接口,因此提供国际化支持
- 资源访问,比如访问URL和文件
- 事件机制
- 同时加载多个配置文件
- 以声明方式启动并创建Spring容器
当系统创建ApplicationContext包括BeanFactory容器时,默认会预初始化所有的singleton Bean。这意味着:系统前期创建ApplicationContext时将有较大的系统开销,但一旦ApplicationContext初始化完成,程序后面获取singleton Bean实例时将拥有较好的性能。
当然,我们也可以阻止Spring容器初始化容器中的singleton Bean,可以在<bean.../>元素中指定属性lazy-init="true",该属性用于阻止容器初始化该Bean,即使使用ApplicationContext作为Spring容器,Spring也不会预初始化该singleton Bean。
3.3 ApplicationContext的国际化支持
ApplicationContext接口继承了MessageSource接口,因此具有国际化功能。下面是MessageSource接口中定义的两个用于国际化的方法:
- String getMessage(String code,Object[] args,Locale loc)
- String getMessage(String code,Object[] args,String default,Local loc)
ApplicationContext正是通过这两个方法来完成国际化的,当程序创建ApplicationContext容器时,Spring自动查找配置文件中名为messageSource的Bean实例,一旦找到这个Bean实例,上述两个方法的调用就被委托给messageSource Bean。如果没有该Bean,ApplicationContext会查找其父容器中的messageSource;如果找到,它将作为messageSource Bean使用。如果无法找到messageSource Bean,系统将会创建一个空的StaticMessageSource Bean,该Bean能接受上述两个方法的调用。配置示例如下:
<?xml version="1.0" encoding="GBK"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<!-- 驱动Spring调用messageSource Bean的setBasenames()方法,
该方法需要一个数组参数,使用list元素配置多个数组元素 -->
<property name="basenames">
<list>
<value>message</value>
<!-- 如果有多个资源文件,全部列在此处 -->
</list>
</property>
</bean>
</beans>
3.4 ApplicationContext的事件机制
ApplicationContext的事件机制是观察者设计模式的实现,通过ApplicationEvent类型和ApplicationListener接口,可以实现ApplicationContext的事件处理。如果容器中有一个ApplicationListener Bean,每当ApplicationContext发布ApplicationEvent时,ApplicationListener Bean将自动被触发。
Spring的事件框架有如下两个重要成员:
- ApplicationEvent:容器事件,必须由ApplicationContext发布
- ApplicationListener:监听器,可由容器中的任何监听器Bean担任
为Spring容器注册事件监听器,只要在Spring中配置一个实现了ApplicationListener接口的Bean,Spring容器就会把这个Bean当成容器事件的事件监听器。当系统创建Spring容器、加载Spring容器时会自动触发容器事件,容器事件监听器可以监听到这些事件。除此之外,程序也可调用ApplicationContext的publishEvent()方法来主动触发容器事件,如下主程序使用ApplicationContext的publishEvent来触发事件。
注意:监听器不仅监听到程序锁触发的事件,也监听到容器内置的事件。实际上,如果开发者需要在Spring容器初始化、销毁时回调自动以方法,就可以通过上面的事件监听器。
示例代码:codes\07\7.4\EventHandler
Spring提供如下几个内置事件:
- ContextRefreshedEvent:ApplicationContext容器初始化或刷新触发该事件。此处的初始化是指,所有的Bean被成功加载,后处理的Bean被检测并激活,所有的singleton Bean被预实例化,ApplicationContext容器已就绪可用
- ContextStartedEvent:当使用ConfigurableApplicationContext(ApplicationContext的子接口)接口的start()方法启动ApplicationContext容器时触发该事件。容器管理生命周期的Bean实例将获得一个指定的启动信号,这在经常需要停止后重新启动的场合比较常见
- ContextClosedEvent:当使用ConfigurableApplicationContext(ApplicationContext的子接口)接口的close()方法关闭ApplicationContext容器时触发该事件。
- ContextStoppedEvent:当使用ConfigurableApplicationContext(ApplicationContext的子接口)接口的stop()方法使ApplicationContext停止时触发该事件。此处的“停止”意味着容器管理声明周期的Bean实例将获得一个指定的停止信号,被停止的Spring容器可再次调用start()方法重新启动
- RequestHandledEvent:Web相关的事件,只能应用于使用DispatcherServlet的Web应用中。在使用Spring作为前端的MVC控制器时,当Spring处理用户请求结束后,系统会自动触发该事件
从Spring 4.0.3开始,Spring还新增了SessionConnectedEvent、SessionContextEvent、SessionDisconnectEvent这三个事件,它们都用于为Spring新增的WebSocket功能服务。
3.5 让Bean获取Spring容器
前面示例中,我们都是手工的创建Spring容器,程序总是持有Spring容器的引用。但在Web应用中,Spring容器通常采用声明式方式配置产生:开发者只要在web.xml文件中配置一个Listener,该Listener将会负责初始化Spring容器,前端MVC框架可以直接调用Spring容器中的Bean,无须访问Spring容器本身。在这种情况下,容器中的Bean处于容器管理下,无须主动访问容器,只需接受容器的依赖注入即可。
在某些特殊情况下,Bean实现某个功能,但必须借助于Spring容器才能实现,此时就必须让该Bean先获取Spring容器,然后借助于Spring容器来实现该功能。为了让Bean获取它所在的Spring容器,可以让该Bean实现BeanFactoryAware接口,该接口只有一个方法:
- setBeanFactory(BeanFactory beanFactory):该方法有一个参数beanFactory,改参数指向创建它的BeanFactory
注意:该方法将由Spring调用,Spring调用该方法时会将Spring容器作为参数传入该方法。与该接口类似的还有BeanNameAware、ResourceLoaderAware接口,这些接口都会提供类似的setter方法,这些方法也由Spring负责调用。
与BeanFactoryAware接口类似的有ApplicationContextAware接口,实现该接口的Bean需要实现setApplicationContext(ApplicationContextapplicationContext)方法——该方法也不是由程序员负责调用的而是由Spring来调用的。当Spring容器调用该方法时,它会把自身作为参数传入该方法。
示例代码:codes\07\7.4\ApplicationContextAware
四、Spring容器中的Bean
官方文档参考:III. Core Technologies -> 6. The IoC container -> 6.3. Bean overview
对于开发者来说,开发者使用Spring框架主要是做两件事:
- 开发Bean
- 配置Bean
对于Spring框架来说,他要做的就是根据配置文件来创建Bean实例,并调用Bean实例的方法完成“依赖注入”
其实Spring框架的本质就是,通过XML配置来驱动Java代码,这样就可以把原本由Java代码管理的耦合关系,提取到XML配置文件中管理,这就实现了系统中各组件的解耦,有利于后期的升级和维护。
4.1 Bean的基本定义和Bean别名
<beans.../>元素时Spring配置文件的根元素,该元素有如下几个属性:
- default-lazy-init:指定该元素下配置的所有Bean默认的延迟初始化行为
- default-merge:指定该元素下配置的所有Bean默认的merge行为
- default-autowire:指定该元素下配置的所有Bean默认的自动装配行为
- default-autowire-candidates:指定元素下配置的所有Bean默认是否为自动装配的候选Bean
- default-init-method:指定该元素下配置的所有Bean默认的初始化方法
- default-destroy-method:指定该元素下配置的所有Bean默认的回收方法
注:<beans.../>元素下的所有属性都可以在每一个<bean.../>子元素中指定,只不过相当于一个是全局设置,一个是局部设置。
<beans.../>元素元素下可以包含多个<bean.../>元素,每个<bean.../>元素定义一个Bean,每个Bean对应Spring容器里的一个Java实例。定义Bean时有如下属性:
- id:确定该Bean的唯一标识。Bean的id属性在Spring容器中应该是唯一的
- class:指定该Bean的具体实现类,这里不能是接口
- name:用于指定Bean的别名
- scope:指定容器中Bean的作用域
注意:id属性中不能包含特殊字符,必须遵循XML文档的id属性规则,例如不能以“/”等特殊符号作为属性值。如果非要使用的话,我们可以使用name属性,用于指定Bean的别名,通过访问Bean别名也可访问Bean实例。
XML规定标识符必须由字母和数字组成,且只能以字母开头,但在一些特殊的情况下,必须为Bean指定特殊表示名,此时就必须为控制器Bean指定别名,指定别名的方式有两种:
- 通过<bean.../>元素的name属性,如果需要指定多个别名,则可以在name属性中使用逗号、冒号或者空格分隔多个别名,后面通过任一别名即可访问该Bean实例。
- 通过<alias.../>元素为已有的Bean指定别名
- name属性:指定一个Bean实例的标识名,表明将为该Bean实例指定别名
- alias属性:指定一个别名
示例:
<!-- 下面代码为该Bean指定多个别名 -->
<bean id="person" class"..." name="#abc,@123,abc*"/>
<alias name="person" alias="jack"/>
<alias name="jack" alias="jackee"/>
4.2 容器中Bean的作用域
官方文档参考:III. Core Technologies -> 6. The IoC container -> 6.5 Bean scopes
Spring通过<bean.../>的scope属性指定Bean的作用域,其包含五种:
- singleton:单例模式,在整个Spring IoC容器中,singleton作用域的Bean将只生成一个实例,配置中默认为singleton
- prototype:每次通过容器的getBean()方法获取prototype作用域的Bean时,都将产生一个新的Bean实例
- request:对于一次HTTP请求,request作用域的Bean将只生成一个实例,这意味着,在同一次HTTP请求内,程序每次请求该Bean,得到的总是同一个实例。只有在Web应用中使用Spring时,该作用域才真正有效
- session:对于一次HTTP会话,session作用域的Bean将只生成一个实例,这意味着,在同一次HTTP会话内,程序每次请求该Bean,得到的总是同一个实例。只有在Web应用中使用Spring时,该作用域才真正有效
- global session:每个全局的HTTP Session对应一个Bean实例。在典型的情况下,仅在使用portlet context的时候有效。只有在Web应用中使用Spring时,该作用域才真正有效
示例代码:codes\07\7.5
对于singleton作用域的Bean,每次请求该id的Bean,都将返回同一个共享实例,也就是每次获得的都相同;对于prototype作用域的Bean,每次获得的都是一个新实例
对于request作用域的Bean,针对每次HTTP请求都会创建一个新的实例,该实例仅在当前HTTP Request内有效,当处理请求结束后,request作用域的Bean实例将被销毁。通常情况下只会讲Web应用的控制器Bean指定成request作用域。
session作用域与request作用域完全类似,区别在于:request作用域的Bean对于每次HTTP请求有效,而session作用域的Bean则对于每次HTTP Session有效。
request和session作用域只在Web应用中才有效,并且必须在Web应用中增加额外配置才会生效,为了让request和session两个作用域生效,必须将HTTP请求对象绑定到为该请求提供服务的线程上,这使得具有request和session作用域的Bean实例能够在后面的调用链中被访问到。
对于Servlet2.4之前的规范的Web容器,不支持Listener规范,只能使用Filter配置方式,如下:
<filter>
<filter-name>requestContextFilter</filter-name>
<filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>requestContextFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
对于支持Servlet2.4及更新规范的Web容器,可以在Web应用的web.xml文件中增加如下Listener配置,该Listener负责使request作用域生效:
<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
这样Spring容器会为每次HTTP请求生成一个Person实例,当该请求响应结束时,该实例也随之消失。
4.3 配置依赖
根据注入方式不同,Bean的依赖注入通常有如下两种方式:
- 设值注入:通过<property.../>元素驱动Spring执行setter方法
- 构造注入:通过<constructor-arg.../>元素驱动Spring执行带参数的构造器
通常不建议使用配置文件配置文件管理Bean的基本类型的属性值;通常只使用配置文件管理容器中Bean与Bean之间的依赖关系。对于singleton作用域的Bean,如果没有强行取消其预初始化行为,系统会在创建Spring容器时预初始化所有的singleton Bean,与此同时,该Bean所依赖的Bean也被一起实例化。
BeanFactory与ApplicationContext实例化容器中Bean的时机不同:前者等到程序需要Bean实例时才创建Bean;而后者在容器创建ApplicationContext实例时,会预初始化容器中所有的singleton Bean。
Spring的作用就是管理JavaEE组件,Spring可以为任何Java对象注入任何类型的属性——只要该Java对象为该属性提供了对应的setter方法即可。
无论哪种注入方式,都需要为参数传入参数值,而Java类的成员变量可以是各种数据类型,除了基本类型值、字符串类型值等,还可以是其他Java实例,也可以是容器中的其他Bean实例,甚至是Java集合、数组等,所以Spring允许通过如下元素为setter方法、构造器参数指定参数值:
- value
- ref
- bean
- list、set、map及props
注意:上面的四个类型都可以使用子元素形式,或属性的方式这两种方式进行定义。而且设置注入和构造注入都类同。
4.3.1 设置普通属性值
value属性用于指定基本类型及其包装、字符串类型的参数值,Spring使用XML解析器来解析出这些数据,然后利用java.beansPropertyEditor完成类型转换:从java.lang.String类型转换为所需的参数值类型。
注:同样可以使用<value.../>这个子元素,不过推荐使用value属性。
配置如下:
<bean id="exampleBean" class="org.crazyit.app.service.ExampleBean">
<!-- 指定int型的参数值 -->
<property name="integerField" value="1"/>
<!-- 指定double型的参数值 -->
<property name="doubleField" value="2.3"/>
</bean>
4.3.2 配置合作者Bean
如果需要为Bean设置的属性值是容器中的另一个Bean实例,则应该使用<ref.../>元素,使用<ref.../>元素时可指定一个bean属性,该属性用于引用容器中其他Bean实例的id属性值,如下:
<bean id="steelAxe" class="..."/>
<bean id="chinese" class="...">
<property name="axe">
<!-- 指定使用容器中id为steelAxe的Bean作为调用setAxe()方法参数-->
<ref bean="steelAxe"/>
</property>
</bean>
注:上面使用的是<ref.../>元素,同样可以使用ref属性这样会比较简练。
4.3.3 使用自动装配注入合作者Bean
Spring能自动装配Bean与Bean之间的依赖关系,即无须使用ref显式指定依赖Bean,而是由Spring容器检查XML配置文件内容,根据某种规则,为调用者Bean注入被依赖的Bean。
Spring的自动装配可通过<beans.../>这个根元素的default-autowrie属性指定,该属性对配置文件中所有bean起作用,相当于全局性设置,当然对应的也可以使用局部设置,也就是在<bean.../>元素中指定autowire属性。当然了反之也有对应的属性分别是:<beans.../>中的default-autowrie-candidates属性和<bean.../>中的autowrie-candidate属性,我们指定autowrie-candidate=“false”,即可将该Bean排除在自动装配之外,而且default-autowrie-candidates的属性值允许使用模式字符串,如default-autowrie-candidates=“*abc”,则所有以“abc”结尾的Bean都将被排除在自动装配之外。
自动装配可以减少配置文件的工作量,但降低了依赖关系的透明性和清晰性,autowire、default-autowire属性可以接受如下值:
- no:不使用自动装配。bean依赖必须通过ref元素定义。这是默认的配置,在较大的部署环境中不鼓励改变这个配置,显式配置合作者能够得到更清晰地依赖关系
- byName:根据setter方法名进行自动装配。Spring容器查找容器中的全部Bean,找出其id与setter方法名去掉set前缀,并小写首字母后同名的Bean来完成注入。如果没有找到匹配的Bean实例,则Spring不会进行任何注入
- byType:根据setter方法的形参类型来自动装配。Spring容器查找容器中的全部Bean,如果正好有一个Bean类型与setter方法的形参类型匹配,就自动注入这个Bean;如果找到多个这样的Bean,就抛出一个异常;如果没有找到这样的Bean,则什么都不会发生,setter方法不会被调用
- constructor:与byType类似,区别是用于自动匹配构造器的参数。如果容器不能恰好找到一个与构造器参数类型匹配的Bean,则会抛出一个异常
- autodetect:Spring容器根据Bean内部结构,自行决定使用constructor或byType策略。如果找到一个默认的构造函数,那么就会应用byType策略
注:byName策略时根据setter方法的方法名与Bean的id进行匹配;byType策略是根据setter方法的参数类型与Bean的类型进行匹配。
示例代码:codes\07\7.5
注意:如果我们指定了autowire属性,但是又显示的使用ref进行指定,那么使用ref显示指定的依赖关系将覆盖自动装配的依赖关系。对于大型的应用,不鼓励使用自动装配。这样会导致依赖关系的装配依赖于源文件的属性名或属性类型,导致Bean与Bean之间的耦合降低到代码层次,不利于高层次的解耦。
4.3.4 注入嵌套Bean
如果某个Bean所依赖的Bean不想被Spring容器直接访问,则可以使用嵌套Bean。把<bean.../>配置成<property.../>或<constructor-args.../>的子元素,name该<bean.../>元素配置的Bean仅仅作为setter注入、构造注入的参数,这种Bean就是嵌套Bean。由于容器不能获取嵌套Bean,因此他不需要指定id属性。
实例如下:
<bean id="chinese" class="org.crazyit.app.service.impl.Chinese">
<!-- 驱动调用chinese的setAxe()方法,使用嵌套Bean作为参数 -->
<property name="axe">
<!-- 嵌套Bean配置的对象仅作为setter方法的参数
嵌套Bean不能被容器访问,因此无需指定id属性-->
<bean class="org.crazyit.app.service.impl.SteelAxe"/>
</property>
</bean>
注意:嵌套Bean提高了程序的内聚性,但降低了程序的灵活性。只有在完全确定无须通过Spring容器访问某个Bean实例时,才考虑使用嵌套Bean来配置该Bean。
4.3.5 注入集合值
如果需要调用形参类型为集合的setter方法,或调用形参类型为集合的构造器,则可使用集合元素<list/>, <set/>, <map/>, 和 <props/>分别来设置类型为List, Set, Map,和Properties的集合参数值。 Spring对 List集合和数组的处理都是一样的,都用<list.../>元素来配置。
示例代码:codes\07\7.5\collection
当使用<list/>, <set/>, <map/>等元素配置集合类型的参数时,还需要配置集合元素。由于集合元素又可以是基本类型值、引用容器中的而其他Bean、嵌套Bean或集合属性等,所以可以接受如下几个子元素:
- value:指定集合元素是基本数据类型值或字符串类型值
- ref:指定集合元素时容器中的另一个Bean实例
- bean:指定结合元素时一个嵌套Bean
- list、set、map及props:指定集合元素又是集合
<props.../>元素用于配置Properties类型的参数值,Properties类型是一种特殊的类型,其key和value都只能是字符串,故Spring配置Properties类型的参数值比较简单。
<map.../>元素就相对比较复杂了,因为每个元素又key、value两个部分组成,所以配置文件中的每个<entry.../>配置一组key-value对,其中<entry.../>支持如下4个属性:
- key:如果Map key是基本类型值或字符串,则可使用该属性来指定Map key
- key-ref:如果Map key是容器中的另一个Bean实例,则可使用改善寻根指定容器中其他Bean的id
- value:同理,指定Map value
- value-ref:同理指定容器中其他Bean的id
官网示例如下:
<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>
Spring还提供了一个简化语法来支持Properties形参的setter方法,如下:
<property name="health">
<value>
pressure=normal
height=175
</value>
</property>
注意:这种简化配置有一个很大的限制:属性名、属性值都只能是英文、数字!不可出现中文。
从Spring 2.0开始,Spring IoC容器将支持集合的合并,子Bean中的集合属性值可以从其父Bean的集合属性继承和覆盖而来,这与Java的继承关系类同。值得注意的是父集合需要指定abstract属性,子集合需要使用parent属性指定父集合,示例如下:
<beans>
<bean id="parent" abstract="true" class="example.ComplexObject">
<property name="adminEmails">
<props>
<prop key="administrator">administrator@example.com</prop>
<prop key="support">support@example.com</prop>
</props>
</property>
</bean>
<bean id="child" parent="parent">
<property name="adminEmails">
<!-- the merge is specified on the child collection definition -->
<props merge="true">
<prop key="sales">sales@example.com</prop>
<prop key="support">support@example.co.uk</prop>
</props>
</property>
</bean>
<beans>
4.3.6 组合属性
Spring还支持组合属性的方式,为Bean的组合属性设置参数值时,除最后一个属性之外,其他属性值都不允许为null。
示例如下:
<bean id="exampleBean" class="org.crazyit.app.service.ExampleBean">
<!-- 驱动Spring调用exampleBean的getPerson().setName()方法,
以"孙悟空"作为参数 -->
<property name="person.name" value="孙悟空"/>
</bean>
注意:person是一个实体类,其包含name属性值。
4.4 Spring的Bean和JavaBean
Spring容器对Bean没有特殊要求,甚至不要求该Bean像标准的JavaBean——必须为每个属性提供对应的getter和setter方法。Spring中的Bean是Java实例、Java组件;而传统的Java应用中JavaBean通常作为DTO(数据传输对象)用来封装值对象,在各层之间传递数据。Spring中的Bean比JavaBean的功能更加复杂,用法也更丰富。当然,传统的JavaBean也可作为Spring的bean,从而接受Spring管理。
下面示例把数据源也配置成容器中的Bean,该数据源Bean即可用于获取数据库连接:
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">
<!-- 定义数据源Bean,使用C3P0数据源实现 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<!-- 指定连接数据库的驱动 -->
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<!-- 指定连接数据库的URL -->
<property name="jdbcUrl" value="jdbc:mysql://localhost/spring"/>
<!-- 指定连接数据库的用户名 -->
<property name="user" value="root"/>
<!-- 指定连接数据库的密码 -->
<property name="password" value="32147"/>
<!-- 指定连接数据库连接池的最大连接数 -->
<property name="maxPoolSize" value="200"/>
<!-- 指定连接数据库连接池的最小连接数 -->
<property name="minPoolSize" value="2"/>
<!-- 指定连接数据库连接池的初始连接数 -->
<property name="initialPoolSize" value="2"/>
<!-- 指定连接数据库连接池的连接的最大空闲时间 -->
<property name="maxIdleTime" value="200"/>
</bean>
</beans>
主程序部分由Spring容器来获取该Bean的实例,获取实例时使用Bean的唯一标识:id属性,id属性时Bean实例在容器中的访问点。主程序代码如下:
public static void main(String[] args) throws Exception {
// 实例化Spring容器。Spring容器负责实例化Bean
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
// 获取容器中id为dataSource的Bean
DataSource ds = ctx.getBean("dataSource", DataSource.class);
// 通过DataSource来获取数据库连接
Connection conn = ds.getConnection();
// 通过数据库连接获取PreparedStatement
PreparedStatement pstmt = conn.prepareStatement("insert into news_inf values(null , ? , ?)");
pstmt.setString(1, "疯狂Java联盟成立了");
pstmt.setString(2, "疯狂Java地址:www.crazyit.org");
// 执行SQL语句
pstmt.executeUpdate();
// 清理资源,回收数据库连接资源。
if (pstmt != null)
pstmt.close();
if (conn != null)
conn.close();
}
虽然Spring对Bean没有特殊要求,但依然建议Spring中的Bean应满足如下几个原则:
- 尽量为每个Bean实现类提供无参数的构造器
- 接受构造注入的Bean,则应提供对应的、带参数的构造函数
- 接受设值注入的Bean,则应提供对应的setter方法,并不要求提供对应的getter方法。
传统的JavaBean和Spring中的Bean存在如下区别:
- 用处不同:传统的JavaBean更多是作为值对象传递参数;Spring的Bean用处几乎无所不包,任何应用组件都被称为Bean
- 写法不同:传统的JavaBean作为值对象,要求每个属性都提供getter和setter方法;但Spring的Bean只需为接受设值注入的属性提供setter方法即可
- 生命周期不同:传统的JavaBean作为值对象传递,不接受任何容器管理其生命周期;Spring中的Bean由Spring管理其生命周期行为
参考资料:
- 《轻量级JavaEE企业应用实战 第四版》
- 最后修改时间:2017年3月29日16:04:28