7.5 Spring容器中的Bean

从本质上来看, Spring容器就是一个超级大工厂, Spring容器中的Bean就是该工厂的产品。Spring容器能产生哪些产品,则完全取决于开发者在配置文件中的配置。
对于开发者来说,开发者使用Spring框架主要是做两件事:

  • 开发Bean;
  • 配置Bean

对于Spring框架来说,它要做的就是根据配置文件来创建Bean实例,并调用Bean实例的方法完成”依赖注入”——这就是所谓IoC的本质。这就要求开发者在使用Spring框架时,眼中看到的是"XML配置",心中想的是"Java代码"。本书后面介绍Spring框架时,会尽量向读者揭示”每段XML配置”在底层所对应的”Java代码调用”。
其实Spring框架的本质就是,通过XML配置来驱动Java代码,这样就可以把原本由Java代码管理的耦合关系,提取到XML配置文件中管理,这就实现了系统中各组件的解耦,有利于后期的升级和维护.

7.4.5 让Bean获取Spring容器

前面介绍的几个示例,都是程序先创建Spring容器,再调用Spring容器的getBean()方法来获取Spring容器中的Bean。在这种访问模式下,程序中总是持有Spring容器的引用。
在某些特殊的情况下,Bean需要实现某个功能(比如该Bean需要输出国际化消息,或者该Bean需要向Spring容器发布事件…),但该功能必须借助于Spring容器才能实现,此时就必须让该Bean获取它所在的Spring容器,然后借助于Spring容器来实现该功能。

如何获取Bean所在的Spring容器

为了让Bean获取它所在的Spring容器,可以让该Bean实现BeanFactoryAware接口。BeanFactoryAware接口里只有一个方法。

方法 描述
setBeanFactory(BeanFactory beanFactory) 该方法有一个参数beanFactory,该参数指向创建它的BeanFactory

大部分初学者看到这个setter方法会感到比较奇怪,因为以前定义一个setter方法之后,该setter方法通常都是由程序员来调用的, setter方法参数由程序员指定;即使使用Spring进行依赖注入时, setter方法参数值也是由程序员通过配置文件来指定的。但此处的这个setter方法比较奇怪,这个方法将由Spring调用, Spring调用该方法时会将Spring容器作为参数传入该方法。与该接口类似的还有BeanNameAwareResourceLoaderAware接口,这些接口里都会提供类似的setter方法,这些方法也由Spring负责调用。
BeanFactoryAware接口类似的有ApplicationContextAware接口,实现该接口的Bean需要实现setApplicationContext(ApplicationContext applicationContex)方法—该方法也不是由程序员负责调用的而是由Spring来调用的。当Spring容器调用该方法时,它会把自身作为参数传入该方法。

程序示例

下面示例假设Person类的sayHi()方法必须能输出国际化消息,由于国际化功能需要借助于Spring容器来实现,因此程序就需要让Person类实现Application ContextAware接口。下面是Person类的源代码。

项目结构

Person.java

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
package org.crazyit.app.service;

import java.util.Locale;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class Person implements ApplicationContextAware
{
// 用成员变量保存它所在的ApplicationContext容器
private ApplicationContext ctx;

/*
* Spring容器会检测容器中所有Bean,如果发现某个Bean实现了ApplicationContextAware接口,
* Spring容器会在创建该Bean之后,自动调用该方法,调用该方法时, 会将容器本身作为参数传给该方法。
*/
public void setApplicationContext(ApplicationContext ctx)
throws BeansException
{
this.ctx = ctx;
}
public void sayHi(String name)
{
System.out.println(ctx.getMessage("hello",
new String[]{name}, Locale.getDefault(Locale.Category.FORMAT)));
}
}

上面的Person类实现了ApplicationContextAware接口,并实现了该接口提供的setApplicationContextAware()方法.
Spring容器会检测容器中所有的Bean,如果发现某个Bean实现了ApplicationContextAware接口,Spring容器会在创建该Bean之后,自动调用该BeansetApplicationContextAware()方法,调用该方法时,会将容器本身作为参数传给该方法——该方法的实现部分将Spring传入的参数(容器本身)赋给该Person对象的ctx实例变量,因此接下来即可通过该ctx实例变量来访问容器本身。
将该Bean部署在Spring容器中,部署该Bean与部署其他Bean没有任何区别。

beans.xml

XML配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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.xsd">
<!-- 加载容器国际化所需要的语言资源文件 -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>message</value>
</list>
</property>
<!-- 设置国际化文件编码 -->
<property name="defaultEncoding" value="UTF-8" />
</bean>
<!-- Spring容器会检测容器中所有Bean,如果发现某个Bean实现了ApplicationContextAware接口,
Spring容器会在创建该Bean之后, 自动调用该Bean的setApplicationContext()方法,
调用该方法时, 会将容器本身作为参数传给该方法。 -->
<bean id="person" class="org.crazyit.app.service.Person" />
</beans>

主程序部分进行简单测试,程序先通过实例化的方法来获得ApplicationContext,然后再通过personBean来获得BeanFactory,并将二者进行比较。主程序如下。

SpringTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package lee;

import org.crazyit.app.service.Person;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringTest
{
public static void main(String[] args)throws Exception
{
@SuppressWarnings("resource")
ApplicationContext ctx = new
ClassPathXmlApplicationContext("beans.xml");
Person p = ctx.getBean("person" , Person.class);
p.sayHi("孙悟空");
}
}

Bean获取它所在容器的好处

上面程序执行Person对象的sayHi()方法时,该sayHi()方法就自动具有了国际化的功能,而这种国际化的功能实际上是由Spring容器提供的,这就是让Bean获取它所在容器的好处

7.4.4 ApplicationContext的事件机制

ApplicationContext的事件机制是观察者设计模式的实现,通过ApplicationEvent类和ApplicationListener接口,可以实现ApplicationContext的事件处理。如果容器中有一个ApplicationListener Bean每当ApplicationContext发布ApplicationEvent时, ApplicationListener Bean将自动被触发。
Spring的事件框架有如下两个重要成员。

  • ApplicationEvent:容器事件,必须由ApplicationContext发布。
  • ApplicationListener:监听器,可由容器中的任何监听器Bean担任。

实际上, Spring的事件机制与所有的事件机制都基本相似,它们都需要由事件源事件事件监听器组成。只是此处的事件源是ApplicationContext,且事件必须由Java程序显式触发。

程序示例

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\EventHandler
└─src\
├─beans.xml
├─lee\
│ └─SpringTest.java
└─org\
└─crazyit\
└─app\
├─event\
│ └─EmailEvent.java
└─listener\
└─EmailNotifier.java

下面的程序将示范Spring容器的事件机制。程序先定义了一个ApplicationEvent类,其对象就是个Spring容器事件。 ApplicationEvent类的代码如下。

beans.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.crazyit.app.event;
import org.springframework.context.ApplicationEvent;
public class EmailEvent extends ApplicationEvent
{
private static final long serialVersionUID = 1L;
private String address;
private String text;
public EmailEvent(Object source)
{
super(source);
}
// 初始化全部成员变量的构造器
public EmailEvent(Object source, String address, String text)
{
super(source);
this.address = address;
this.text = text;
}
// 此处省略getter和setter方法,请自己补上
}

上面的EmailEvent类继承了ApplicationEvent类,除此之外,它就是一个普通的Java类。

如何创建Spring容器的容器事件

只要个Java类继承了ApplicationEvent基类,那该对象就可作为Spring容器的容器事件。

容器事件的监听器

容器事件的监听器类必须实现ApplicationListener接口,实现该接口必须实现如下方法。
onApplicationEvent(ApplicationEvent event):每当容器内发生任何事件时,此方法都被触发

EmailNotifier.java

本示例所用的容器监听器类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package org.crazyit.app.listener;

import org.crazyit.app.event.EmailEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
public class EmailNotifier implements ApplicationListener
{
// 该方法会在容器发生事件时自动触发
public void onApplicationEvent(ApplicationEvent evt)
{
// 只处理EmailEvent,模拟发送email通知...
if (evt instanceof EmailEvent)
{
EmailEvent emailEvent = (EmailEvent) evt;
System.out.println("需要发送邮件的接收地址 " + emailEvent.getAddress());
System.out.println("需要发送邮件的邮件正文 " + emailEvent.getText());
} else
{
// 其他事件不作任何处理
System.out.println("其他事件:" + evt);
}
}
}

将监听器配置在容器中,配置文件如下。

beans.xml

1
2
3
4
5
6
7
8
<?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.xsd">
<!-- 配置监听器 -->
<bean class="org.crazyit.app.listener.EmailNotifier" />
</beans>

从上面的粗体字代码可以看出,为Spring容器注册事件监听器,不需要像AWT编程那样采用代码进行编程,只要进行简单配置即可。只要在Spring中配置一个实现了ApplicationListener接口的Bean,Spring容器就会把这个Bean当成容器事件的事件监听器
当系统创建Spring容器加载Spring容器时会自动触发容器事件,容器事件监听器可以监听到这些事件。除此之外,程序也可调用ApplicationContextpulishEvent()方法来主动触发容器事件。如下主程序使用ApplicationContextpublishEvent()来触发事件。

SpringTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package lee;

import org.crazyit.app.event.EmailEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringTest
{
public static void main(String[] args)
{
@SuppressWarnings("resource")
ApplicationContext ctx = new
ClassPathXmlApplicationContext("beans.xml");
// 创建一个ApplicationEvent对象
EmailEvent ele = new EmailEvent("test" ,
"spring_test@163.com" , "this is a test");
// 发布容器事件
ctx.publishEvent(ele);
}
}

上面程序中的两行粗体字代码创建了ApplicationEvent对象,并通过ApplictionContex主动触发了该事件。运行上面的程序,将看到如下执行结果:

1
2
3
其他事件:org.springframework.context.event.ContextRefreshedEvent[source=org.springframework.context.support.ClassPathXmlApplicationContext@439f5b3d, started on Sun Aug 25 23:06:44 CST 2019]
需要发送邮件的接收地址 spring_test@163.com
需要发送邮件的邮件正文 this is a test

从上面的执行结果可以看出,监听器不仅监听到程序所触发的事件,也监听到容器内置的事件。实际上,如果开发者需要在Spring容器初始化、销毁时回调自定义方法,就可以通过上面的事件监听器来实现
提示:一如果Bean希望发布容器事件,则该Bean必须先获得对Spring容器的引用。为了让Bean获得对Spring容器的引用,可让Bean类实现ApplicationContextAwareBeanFactoryAware接口。

Spring内置事件

Spring提供如下几个内置事件:
这里先省略,后续用到再更新…

7.4.3 ApplicationContext的国际化支持

ApplicationContext接口继承了MessageSource接口,因此具有国际化功能。

MessageSource接口中用于国际化的方法

下面是MessageSource接口中定义的两个用于国际化的方法。

  • String getMessage(String code,Object[] args, Locale locale)
  • String getMessage(String code,Object[] args,String default,Locale locale)

ApplicationContext正是通过这两个方法来完成国际化的,当程序创建ApplicationContext容器时Spring自动查找配置文件中名为messageSourceBean实例,

  • 一旦找到这个Bean实例,上述两个方法的调用就被委托给该messageSource Bean
  • 如果没有该Bean,ApplicationContex会查找其父容器中的messageSource Bean;如果找到,它将被作为messageSource Bean使用。
  • 如果无法找到messageSource Bean,系统将会创建一个空的StaticMessageSourceBean,该Bean能接受上述两个方法的调用。

Spring中配置messageSource bean时通常使用ResourceBundleMessageSource类。

程序示例

项目结构

1
2
3
4
5
6
7
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\I18N
└─src\
├─beans.xml
├─lee\
│ └─SpringTest.java
├─message_en_US.properties
└─message_zh_CN.properties

beans.xml

看下面的配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="GBK"?>
<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="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<!-- 驱动Spring调用messageSource Bean的setBasenames()方法, 该方法需要一个数组参数,使用list元素配置多个数组元素 -->
<property name="basenames">
<list>
<value>message</value>
<!-- 如果有多个资源文件,全部列在此处 -->
</list>
</property>
<!-- 设置国际化文件编码 -->
<property name="defaultEncoding" value="UTF-8" />
</bean>
</beans>

上面文件的粗体字代码定义了一个idmessageSourceBean,该Bean实例只指定了一份国际化资源文件,其baseNamemessage.
然后给出如下两份资源文件。

message_en_US.properties

第一份为美式英语的资源文件:

1
2
hello=welcome,{0}
now=now is :{0}

message_zh_CN.properties

第二份为简体中文的资源文件:

1
2
hello=欢迎你,{0}
now=现在时间是:{0}

由于Java9支持使用UTF-8字符集保存国际化资源文件,这种国际化资源文件可以包含非西欧字符,因此只要将这份文件以UTF-8字符集保存即可。此时,程序拥有了两份资源文件,可以自适应美式英语和简体中文的环境。主程序部分如下。

SpringTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package lee;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.*;

public class SpringTest
{
public static void main(String[] args) throws Exception
{
// 实例化ApplicationContext
@SuppressWarnings("resource")
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"beans.xml");
// 使用getMessage()方法获取本地化消息。
// Locale的getDefault方法返回计算机环境的默认Locale
String hello = ctx.getMessage("hello", new String[]{"孙悟空"},
Locale.getDefault(Locale.Category.FORMAT));
String now = ctx.getMessage("now", new Object[]{new Date()},
Locale.getDefault(Locale.Category.FORMAT));
// 打印出两条本地化消息
System.out.println(hello);
System.out.println(now);
}
}

上面的两行粗体字代码是Spring容器提供的获取国际化消息的方法,这两个方法由MessageSource接口提供。
上面程序的执行结果会随环境不同而改变,在简体中文的环境下,执行结果如下:

1
2
欢迎你,孙悟空
现在时间是:19-8-25 下午11:29

注意
Spring的国际化支持,其实是建立在Java程序国际化的基础之上的。其核心思路都是将程序中需要实现国际化的信息写入资源文件,而代码中仅仅使用相应的各信息的Key

7.4.2 使用ApplicationContext

大部分时候,都不会使用BeanFactory实例作为Spring容器,而是使用ApplicationContext实例作为容器,因此Spring容器也称为Spring上下文。 ApplicationContext作为BeanFactory的子接口,增强了BeanFactory的功能。
ApplicationContext允许以声明式方式操作容器,无须手动创建它。可利用如ContextLoader的支持类,在Web应用启动时自动创建ApplicationContext。当然也可采用编程方式创建ApplicationContext.

ApplicationContext额外功能

除了提供BeanFactory所支持的全部功能外, ApplicationContext还有如下额外的功能。

  1. ApplicationContext默认会预初始化所有的singleton Bean,也可通过配置取消预初始化ApplicationContext继承MessageSource接口,因此提供国际化支持。
  2. 资源访问,比如访问URL和文件。
  3. 事件机制。
  4. 同时加载多个配置文件。
  5. 以声明式方式启动并创建Spring容器。

ApplicationContext包括BeanFactory的全部功能,因此建议优先使用ApplicationContext。除非对于某些内存非常关键的应用,才考虑使用BeanFactory.

程序示例

项目结构

例如有如下配置。

beans.xml

1
2
3
4
5
6
7
8
9
10
11
<?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.xsd">
<!-- 如果不加任何特殊的配置,该Bean默认是singleton行为的 -->
<bean id="chinese" class="org.crazyit.app.service.Person">
<!-- 驱动Spring执行chinese Bean的setTest()方法,以"孙悟空"为传入参数 -->
<property name="test" value="孙悟空" />
</bean>
</beans>

上面粗体字代码配置了一个idchinesebean,如果没有任何特殊配置,该Bean就是singleton Bean,ApplicationContext会在容器初始化完成后,自动调用Person类的构造器创建chinese这个Bean,并以"孙悟空"这个字符串作为传入参数去调用chinese这个beansetTest方法。

Person.java

该程序用的Person类的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.crazyit.app.service;

public class Person
{
public Person()
{
System.out.println("==正在执行Person无参数的构造器==");
}
public void setTest(String name)
{
System.out.println("正在调用setName()方法,传入参数为:" + name);
}
}

测试

即使主程序只有如下一行代码:

1
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");

上面代码只是使用ApplicationContext创建了Spring容器, ApplicationContext会自动预初始化容器中的chinese这个bean包括:

  1. 调用它的无参数的构造器,
  2. 根据< property.>元素执行setter方法。

执行上面代码,可以看到如下输出:

1
2
==正在执行Person无参数的构造器==
正在调用setName()方法,传入参数为:孙悟空

如果将main方法中创建Spring容器的代码换成使用BeanFactory作为容器,例如改为如下代码:

1
2
3
4
5
6
// 搜索类加载路径下的beans.xml文件创建Resource对象
Resource isr = new ClassPathResource("beans.xml");
// 创建默认的BeanFactory容器
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// 让默认的BeanFactory容器加载默认的
new XmlBeanDefinitionReader(beanFactory).loadBeanDefinitions(isr);

上面代码以BeanFactory创建了Spring容器,但**BeanFactory不会预初始化容器中的Bean**,因此执行上面代码不会看到调用Person类的构造器、执行chinese这个beansetName方法。

如何阻止Spring容器预初始化Bean

为了阻止Spring容器预初始化容器中的singleton Bean,可以**为<bean>元素指定lazy-init="true"属性,该属性用于阻止容器预初始化该Bean**。因此,如果为上面<bean>元素指定了lazy-init="true",那么即使使用ApplicationContext作为Spring容器, Spring也不会预初始化该singleton Bean.

7.4.1 Spring容器

Spring容器最基本的接口就是BeanFactoryBeanFactory负责配置创建管理Bean,它有一个子接口:ApplictionContext,也被称为Spring上下文。 Spring容器还负责管理BeanBean之间的依赖关系。

BeanFactory接口方法

BeanFactory接口包含如下几个基本方法。

方法 描述
boolean containsBean(String name) 判断Spring容器是否包含idnameBean实例。
<T> T getBean(Class<T> requiredType) 获取Spring容器中属于requiredType类型的、唯一的Bean实例。
Object getBean(String name) 返回容器id为nameBean实例。
<T> T getBean(String name,Class requiredType) 返回容器中idname,并且类型为requiredTypeBean
Class<?> getType(String name) 返回容器中idnameBean实例的类型。

调用者只需使用getBean()方法即可获得指定Bean的引用,无须关心Bean的实例化过程。Bean实例的创建、初始化以及依赖关系的注入都由Spring容器完成
BeanFactory常用的实现类是DefaultListableBeanFactory

ApplicationContext接口

ApplicationContextBeanFactory的子接口,因此功能更强大。对于大部分Java EE应用而言,使用ApplicationContext作为Spring容器更方便

  • 其常用实现类是FileSystemXmlApplicationContextClassPathXmlApplicationContextAnnotationConfigApplicationContext
  • 如果在Web应用中使用Spring容器,则通常有XmlWebApplicationContextAnnotationConfigWebApplicationContext两个实现类。

创建Spring容器的实例时,必须提供Spring容器管理的Bean的详细配置信息。 Spring的配置信息通常采用XML配置文件来设置,因此,创建BeanFactory实例时,应该提供XML配置文件作为参数,XML配置文件通常使用Resource对象传入。

Resource接口

Resource接口是Spring提供的资源访问接口,通过使用Resource接口, Spring能以简单、透明的方式访问磁盘、类路径以及网络上的资源

大部分Java EE应用,可在启动web应用时自动加载ApplicationContext实例,接受Spring管理的Bean无须知道ApplicationContext的存在,一样可以利用ApplicationContext的管理。

实例化BeanFactory

对于独立的应用程序,可通过如下方法来实例化BeanFactory.

1
2
3
4
5
6
//搜索类加载路径下的beans.xml文件创建Resource对象
Resource isr=new ClassPathResource("beans.xml");
// 创建默认的BeanFactory容器
DefaultListableBeanFactory beanFactory=new DefaultListableBeanFactory();
// 让默认的BeanFactory容器加载isr对应的XML配置文件
new XmlBeanDefinitionReader(beanFactory).loadBeanDefinitions(isr);

或者采用如下代码来创建BeanFactory:

1
2
3
4
5
6
//搜索文件系统的当前路径下的beans.xml文件创建Resource对象
Resource isr=new FileSystemResource("beans.xml");
// 创建默认的BeanFactory容器
DefaultListableBeanFactory beanFactory=new DefaultListableBeanFactory();
// 让默认的BeanFactory容器加载isr对应的XML配置文件
new XmlBeanDefinitionReader(beanFactory).loadBeanDefinitions(isr);

加载多个配置文件来创建Spring容器

如果应用需要加载多个配置文件来创建Spring容器,则应该采用BeanFactory的子接口ApplicationContext来创建BeanFactory的实例ApplicationContext接口包含FileSystemXmlApplicationContextClassPathXmlApplicationContext两个常用的实现类。
如果需要同时加载多个XML配置文件来创建Spring容器,则可以采用如下方式:

1
2
// 以类加载路径下的beans.xml和service.xml文件来创建ApplicationContext
AplicationContext appContext=new ClassPathXmlApplicationContext("beans.xml","service.xml");

当然也可支持从文件系统的相对路径或绝对路径来搜索配置文件,只要使用FileSystemXmlApplicationContext即可,如下面的程序片段所示:

1
AplicationContext appContext=new FileSystemXmlApplicationContext("beans.xml","service.xml");

由于ApplicationContext本身就是BeanFactory的子接口,因此**ApplicationContext完全可以作为Spring容器来使用**,而且功能更强。当然,如果有需要,也可以把ApplicationContext实例赋给BeanFactory变量.

7.4 使用Spring容器

Spring有两个核心接口:BeanFactoryApplicationContext,其中ApplicationContextBeanFactory的子接口。它们都可代表Spring容器, Spring容器是生成Bean实例的工厂,并管理容器中的Bean。在基于SpringJava EE应用中,所有的组件都被当成Bean处理,包括数据源HibernateSessionFactory事务管理器等。
应用中的所有组件都处于Spring的管理下,都被SpringBean的方式管理, Spring负责创建Bean实例,并管理其生命周期。 Spring里的Bean是非常广义的概念,任何的Java对象、Java组件都被当成Bean处理。对于Spring而言,一切Java对象都是Bean
BeanSpring容器中运行,无须感受Spring容器的存在,一样可以接受Spring的依赖注入,包括Bean成员变量的注入、协作者的注入、依赖关系的注入等。
Java程序面向接口编程,无须关心Bean实例的实现类;但Spring容器负责创建Bean实例,因此必须精确知道每个Bean实例的实现类,故Spring配置文件必须指定Bean实例的实现类。

7.3.4 两种注入方式的对比

在过去的开发过程中,这两种注入方式都是非常常用的。 Spring也同时支持两种依赖注入方式:设值注入构造注入。这两种依赖注入方式并没有绝对的好坏,只是适应的场景有所不同。

设值注入的优点

相比之下,设值注入具有如下的优点。

  1. 与传统的Javabean的写法更相似,程序开发人员更容易理解、接受。通过setter方法设定依赖关系显得更加直观、自然。
  2. 对于复杂的依赖关系,如果采用构造注入,会导致构造器过于臃肿,难以阅读。 Spring在创建Bean实例时,需要同时实例化其依赖的全部实例,因而导致性能下降。而使用设值注入,则能避免这些问题。
  3. 尤其是在某些成员变量可选的情况下,多参数的构造器更加笨重。

    构造注入的优点

    构造注入也不是绝对不如设值注入,在某些特定的场景下,构造注入比设值注入更优秀。构造注入也有如下优势:
  4. 构造注入可以在构造器中决定依赖关系的注入顺序,优先依赖的优先注入。例如,组件中其他依赖关系的注入,常常需要依赖于Datasource的注入。采用构造注入,可以在代码中清晰地决定注入顺序。
  5. 对于依赖关系无须变化的Bean,构造注入更有用处。因为没有setter方法,所有的依赖关系全部在构造器内设定。因此,无须担心后续的代码对依赖关系产生破坏。
  6. 依赖关系只能在构造器中设定,则只有组件的创建者才能改变组件的依赖关系。对组件的调用者而言,组件内部的依赖关系完全透明,更符合高内聚的原则。

设值注入为主,构造注入为辅

建议采用以设值注入为主,构造注入为辅的注入策略。
对于依赖关系无须变化的注入,尽量采用构造注入;
而其他依赖关系的注入,则考虑采用设值注入。

7.3.3 构造注入

前面已经介绍过,通过setter方法为目标Bean注入依赖关系的方式被称为设值注入;另外还有种注入方式,这种方式在构造实例时,已经为其完成了依赖关系的初始化。这种利**用构造器来设置依赖关系的方式,被称为构造注入**。

构造注入的本质

通俗来说,就是驱动Spring在底层以反射方式执行带参数的构造器,当执行带参数的构造器时,就可利用构造器参数对成员变量执行初始化—这就是构造注入的本质
现在问题产生了:<bean>元素默认总是驱动Spring调用无参数的构造器来创建对象,那怎样驱动Spring调用有参数的构造器去创建对象呢?答案是使用<constructor-arg>子元素,每个<constructor-arg>子元素代表一个构造器参数,如果<bean>元素包含N<constructor-arg>子元素,就会驱动Spring调用带N个参数的构造器来创建对象.

程序示例

对前面代码中的Chinese类做简单的修改,修改后的代码如下。

Chinese.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.crazyit.app.service.impl;

import org.crazyit.app.service.*;

public class Chinese implements Person
{
private Axe axe;
// 构造注入所需的带参数的构造器
public Chinese(Axe axe)
{
this.axe = axe;
}
// 实现Person接口的useAxe()方法
public void useAxe()
{
// 调用axe的chop()方法
// 表明Person对象依赖于axe对象
System.out.println(axe.chop());
}
}

上面的Chinese类没有提供设置axe成员变量的setter法,仅仅提供了一个带Axe参数的构造器。Spring将通过该构造器为chinese注入所依赖的Bean实例。
构造注入的配置文件也需做简单的修改,为了使用构造注入(也就是驱动Spring调用有参数的构造器创建对象),还需使用<constructor-arg>元素指定构造器的参数。修改后的配置文件如下。

beans.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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.xsd">
<!-- 配置chinese实例,其实现类是Chinese -->
<!-- 1号代码 -->
<bean id="chinese" class="org.crazyit.app.service.impl.Chinese">
<!-- 下面只有一个constructor-arg子元素, 驱动Spring调用Chinese带一个参数的构造器来创建对象 -->
<constructor-arg ref="steelAxe"
type="org.crazyit.app.service.Axe" />
</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>

上面配置文件中的粗体字代码使用<constructor-arg>元素指定了一个构造器参数,该参数类型是Axe,这指定Spring调用Chinese类里带一个Axe参数的构造器来创建chinese实例。也就是说,上面代码相当于驱动Spring执行如下代码:

1
2
3
4
5
6
7
String idStr=...;//Spring解析<bean>元素得到id属性值为chinese
String refStr=...;//Spring解析<constructor-arg>元素得到ref属性值为steelAxe
Object paramBean=container.get(refStr);
//Spring会用反射方式执行下面代码,此处为了降低阅读难度,该行代码没有使用反射。
Object obj=new org.crazyit.app.service.impl.Chinese(paramBean);
// container代表Spring容器
container.put(idStr,obj);

从上面字代码可以看出,由于使用了有参数的构造器创建实例,所以当Bean实例被创建完成后,该Bean的依赖关系已经设置完成。
该示例的执行效果与设值注入steelAxe时的执行效果完全一样。区别在于:

  • 创建Person实例中Axe属性的时机不同——设值注入是先通过无参数的构造器创建一个Bean实例,然后调用对应的setter方法注入依赖关系;
  • 而构造注入则直接调用有参数的构造器,当Bean实例创建完成后,已经完成了依赖关系的注入

index属性

配置<constructor-arg>元素时可指定一个index属性,用于指定该构造参数值将作为第几个构造参数值,例如,指定index="0"表明该构造参数值将作为第一个构造参数值
希望Spring调用带几个参数的构造器,就在<bean>元素中配置几个<constructor-arg>子元素。例如如下配置代码:

1
2
3
4
5
<!--定义名为bean1的Bean,对应的实现类为lee.Test1-->
<bean id="bean1" class="lee.Test1">
<constructor-arg index="0" value="hello"/>
<constructor-arg index="1" value="23"/>
</bean>

上面的代码相当于让Spring调用如下代码( Spring底层用反射执行该代码)

1
Object bean1=new lee.Test1("hello","23");//①号代码

由于Spring本身提供了功能强大的类型转换机制,因此如果lee.Test1只包含一个Test(String,int)构造器,那么上面的配置片段相当于让Spring执行如下代码( Spring底层用反射执行该代码):

1
Object bean1=new lee.Test1("hello",23);//②号代码

这就产生一个问题:如果lee.Test1类既有Testl(String, String)构造器,又有Test1(String,int)构造器,那么上面的粗体字配置片段到底让Spring执行哪行代码呢?答案是①号代码,因为此时的配置还不够明确:对于<constructor-arg value="23>,Spring只能解析出一个"23"字符串,但它到底需要转换为哪种数据类型——从配置文件中看不出来,只能根据lee.Test1的构造器来尝试转换.

type属性

为了更明确地指定数据类型, Spring允许为<constructor-arg.>元素指定一个type属性,例如<constructor-arg value="23" type="int">,此处Spring明确知道此处配置了一个int类型的参数。与此类似的是,<value>元素也可指定type属性,用于确定该属性值的数据类型。

7.3.2 设值注入

设值注入是指IoC容器通过成员变量的setter方法来注入被依赖对象。这种注入方式简单、直观,因而在Spring的依赖注入里大量使用。
下面示例将会对前面示例进行改写,使之更加规范。 Spring推荐面向接口编程。不管是调用者,还是被依赖对象,都应该为之定义接口,程序应该面向它们的接口,而不是面向实现类编程,这样以便程序后期的升级、维护

程序示例

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\setter
└─src\
├─beans.xml
├─lee\
│ └─BeanTest.java
└─org\
└─crazyit\
└─app\
└─service\
├─Axe.java
├─impl\
│ ├─Chinese.java
│ ├─SteelAxe.java
│ └─StoneAxe.java
└─Person.java

Person.java

下面先定义一个Person接口,该接口定义了一个Person对象应遵守的规范。下面是Person接口的代码。

1
2
3
4
5
6
7
package org.crazyit.app.service;

public interface Person
{
// 定义一个使用斧子的方法
public void useAxe();
}

Axe.java

下面是Axe接口的代码。

1
2
3
4
5
6
7
package org.crazyit.app.service;

public interface Axe
{
// Axe接口里有个砍的方法
public String chop();
}

Spring推荐面向接口编程,这样可以更好地让规范和实现分离,从而提供更好的解耦。对于一个Java EE应用,不管是DAO组件,还是业务逻辑组件,都应该先定义一个接口,该接口定义了该组件应该实现的功能,但功能的实现则由其实现类提供。

Chinese.java

下面是Person实现类的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.crazyit.app.service.impl;

import org.crazyit.app.service.*;

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());
}
}

上面程序中的粗体字代码实现了Person接口的useAxe()方法,实现该方法时调用了axechop()方法,这就是典型的依赖关系。
回忆一下曾经编写的Java应用,除了最简单的Hello world之外,哪个应用不是A调用B、B调用C、C调用D…这种方式?那Spring的作用呢? Spring容器的最大作用就是以松耦合的方式来管理这种调用关系。在上面的Chinese类中, Chinese类并不知道它要调用的axe实例在哪里,也不知道axe实例是如何实现的,它只是需要调用Axe对象的方法,这个Axe实例将由Spring容器负责注入。

SteelAxe.java

1
2
3
4
5
6
7
8
9
10
11
package org.crazyit.app.service.impl;

import org.crazyit.app.service.*;

public class SteelAxe implements Axe
{
public String chop()
{
return "钢斧砍柴真快";
}
}

到现在为止,程序依然不知道Chinese类和哪个Axe实例耦合, Spring当然也不知道! Spring需要使用XML配置文件来指定实例之间的依赖关系.
Spring采用了XML配置文件,从Spring2.0开始, Spring推荐采用XML Schema来定义配置文件的语义约束。当采用XML Schema来定义配置文件的语义约束时,还可利用Spring配置文件的扩展性进一步简化Spring配置。
Spring为基于XML SchemaXML配置文件提供了一些新的标签,这些新标签使配置更简单,使用更方便。关于如何使用Spring所提供的新标签,后面会有更进一步的介绍。
不仅如此,采用基于XML SchemaXML配置文件时, Spring甚至允许程序员开发自定义的配置文件标签,让其他开发人员在Spring配置文件中使用这些标签,但这些通常应由第三方供应商来完成。对普通软件开发人员以及普通系统架构师而言,则通常无须开发自定义的Spring配置文件标签。所以本书也不打算介绍相关方面的内容。
提示:本书并不是一本完整的Spring学习手册,所以本书只会介绍Spring的核心机制,包括loCSpELAOP和资源访问等, SpringHibernate整合, SpringDAO支持和事务管理,以及SpringStruts2整合等内容,这些是Java ee开发所需要的核心知识。而Spring框架的其他方面,本书不会涉及.
下面是本应用所用的配置文件代码。

beans.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="GBK"?>
<!-- Spring配置文件的根元素,使用spring-beans.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.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>

在配置文件中, Spring配置Bean实例通常会指定两个属性。

  1. id:指定该Bean的唯一标识, Spring根据id属性值来管理Bean,程序通过id属性值来访问该Bean实例。
  2. class:指定该Bean的实现类,此处不可再用接口,必须使用实现类, Spring容器会使用XML解析器读取该属性值,并利用反射来创建该实现类的实例。

可以看到Spring管理Bean的灵巧性。BeanBean之间的依赖关系放在配置文件里组织,而不是写在代码里。通过配置文件的指定, Spring能精确地为每个Bean的成员变量注入值。

Spring会自动检测每个<bean>定义里的<property>元素定义, Spring会在调用默认的构造器创建Bean实例之后,立即调用对应的setter法为Bean的成员变量注入值。
下面是主程序的代码,该主程序只是简单地获取了Person实例,并调用该实例的useAxeo方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package lee;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.crazyit.app.service.*;

public class BeanTest
{
public static void main(String[] args) throws Exception
{
// 创建Spring容器
@SuppressWarnings("resource")
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"beans.xml");
// 从Spring容器中获取chinese实例
Person p = ctx.getBean("chinese", Person.class);
// 调用useAxe()方法
p.useAxe();
}
}

上面程序中的实现了创建Spring容器,并通过Spring容器来获取Bean实例。从上面程序中可以看出, Spring容器就是一个巨大的工厂,它可以”生产”出所有类型的Bean实例。程序获取Bean实例的方法是getBean()
一旦通过Spring容器获得了Bean实例之后,如何调用Bean实例的方法就没什么特别之处了。执行上面程序,会看到如下执行结果:

1
石斧砍柴好慢

主程序调用PersonuseAxe()方法时,该方法的方法体内需要使用Axe实例,但程序没有任何地方将特定的Person实例和Axe实例耦合在一起。或者说,程序没有为Person实例传入Axe实例,Axe实例由Spring在运行期间注入。
下面是Axe的另一个实现类:SteelAxe

SteelAxe.java

1
2
3
4
5
6
7
8
9
10
11
package org.crazyit.app.service.impl;

import org.crazyit.app.service.*;

public class SteelAxe implements Axe
{
public String chop()
{
return "钢斧砍柴真快";
}
}

将修改后的SteelAxe部署在Spring容器中,只需在Spring配置文件中增加如下一行:

1
2
<!-- 配置steelAxe实例,其实现类是SteelAxe -->
<bean id="steelAxe" class="org.crazyit.app.service.impl.SteelAxe" />

该行重新定义了一个Axe实例,它的id是steelAxe,实现类是SteelAxe。然后修改chineseBean的配置,将原来传入stoneAxe的地方改为传入steelAxe。也就是将

1
<property name="axe" ref="stoneAxe" />

改成:

1
<property name="axe" ref="steelAxe" />

此时再次执行程序,将得到如下结果:
从上面这种切换可以看出,因为chinese实例与具体的Axe实现类没有任何关系, chinese实例仅仅与Axe接口耦合,这就保证了chinese实例与Axe实例之间的松耦合—这也是Spring强调面向接口编程的原因。

设值注入

BeanBean之间的依赖关系由Spring管理,** Spring采用setter方法为目标Bean注入所依赖的Bean这种方式被称为设值注入**。
从上面示例程序中应该可以看出,依赖注入以配置文件管理Bean实例之间的耦合,让Bean实例之间的耦合从代码层次分离出来。依赖注入是一种优秀的解耦方式。

Spring IoC容器的三个基本要点

经过上面的介绍,不难发现使用Spring loC容器的三个基本要点:

  1. 应用程序的各组件面向接口编程。面向接口编程可以将组件之间的耦合关系提升到接口层次从而有利于项目后期的扩展。
  2. 应用程序的各组件不再由程序主动创建,而是由Spring容器来负责产生并初始化
  3. Spring采用配置文件注解来管理Bean的实现类、依赖关系, Spring容器则根据配置文件或注解,利用反射来创建实例,并为之注入依赖关系