第 5 章 Spring IoC容器 ApplicationContext
作为Spring提供的较之BeanFactory更为先进的IoC容器实现,ApplicationContext除了拥有BeanFactory支持的所有功能之外,还进一步扩展了基本容器的功能,包括BeanFactoryPostProcessor、BeanPostProcessor以及其他特殊类型bean的自动识别、容器启动后bean实例的自动初始化、国际化的信息支持、容器内事件发布等
Spring为基本的BeanFactory类型容器提供了XmlBeanFactory实现。也为ApplicationContext类型容器提供了以下几个常用的实现:
①FileSystemXmlApplicationContext。
在默认情况下,从文件系统加载bean定义以及相关资源的ApplicationContext实现。
②ClassPathXmlApplicationContext
在默认情况下,从Classpath加载bean定义以及相关资源的ApplicationContext实现。
③XmlWebApplicationContext
Spring提供的用于Web应用程序的ApplicationContext实现
5.1 统一资源加载策略
URL全名是Uniform Resource Locator(统一资源定位器),Spring提出了一套基于org.springframework.core.io.Resource和org.springframework.core.io.ResourceLoader接口的资源抽象和加载策略
5.1.1 Spring中的Resource
Spring框架内部使用Resource接口作为所有资源的抽象和访问接口,我们之前在构造BeanFactory的时候已经接触过它
BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("..."));
其中ClassPathResource就是Resource的一个特定类型的实现,代表的是位于Classpath中的资源
Resource接口可以根据资源的不同类型,或者资源所处的不同场合,给出相应的具体实现:
①ByteArrayResource
将字节(byte)数组提供的数据作为一种资源进行封装,如果通过InputStream形式访问该类型的资源,该实现会根据字节数组的数据,构造相应的ByteArrayInputStream并返回。
②ClassPathResource
该实现从Java应用程序的ClassPath中加载具体资源并进行封装,可以使用指定的类加载器(ClassLoader)或者给定的类进行资源加载
③FileSystemResource
对java.io.File类型的封装,所以,我们可以以文件或者URL的形式对该类型资源进行访问,只要能跟File打的交道,基本上跟FileSystemResource也可以。
④InputStreamResource
将给定的InputStream视为一种资源的Resource实现类,较为少用。可能的情况下,以ByteArrayResource以及其他形式资源实现代之。
如果以上这些资源实现还不能满足要求,那么我们还可以根据相应场景给出自己的实现,只需实现Resource接口就是了
public interface Resource extends InputStreamSource {
boolean exists();
boolean isOpen();
URL getURL() throws IOException;
File getFile() throws IOException;
Resource createRelative(String relativePath) throws IOException;
String getFilename();
String getDescription();
}
public interface InputStreamSource {
InputStream getInputStream() throws IOException;
}
要真想实现自定义的Resource,倒是真没必要直接实现该接口,我们可以继承AbstractResource抽象类,然后根据当前具体资源特征,覆盖相应的方法就可以了。
5.1.2 ResourceLoader,“更广义的URL”
ResourceLoader接口是资源查找定位策略的统一抽象,具体的资源查找定位策略则由相应的ResourceLoader实现类给出。
public interface ResourceLoader {
String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
Resource getResource(String location);//通过它,我们就可以根据指定的资源位置,定位到具体的资源实例。
ClassLoader getClassLoader();
}
1. 可用的ResourceLoader
①DefaultResourceLoader
是ResourceLoader的一个默认实现类,该类默认的资源查找处理逻辑如下:
①首先检查资源路径是否以classpath:前缀打头,如果是,则尝试构造ClassPathResource类型资源并返回。
②否则
(a) 尝试通过URL,根据资源路径来定位资源,如果没有抛出MalformedURLException,有则会构造UrlResource类型的资源并返回;
(b)如果还是无法根据资源路径定位指定的资源,则委派getResourceByPath(String) 方法来定位, DefaultResourceLoader 的getResourceByPath(String)方法默认实现逻辑是,构造ClassPathResource类型的资源并返回。
代码清单5-2 DefaultResourceLoader使用演示
ResourceLoader resourceLoader = new DefaultResourceLoader();
Resource fakeFileResource = resourceLoader.getResource("D:/spring21site/README");
assertTrue(fakeFileResource instanceof ClassPathResource);
assertFalse(fakeFileResource.exists());
Resource urlResource1 = resourceLoader.getResource("file:D:/spring21site/README");
assertTrue(urlResource1 instanceof UrlResource);
Resource urlResource2 = resourceLoader.getResource("http://www.spring21.cn");
assertTrue(urlResource2 instanceof UrlResource);
try{ fakeFileResource.getFile();
fail("no such file with path["+fakeFileResource.getFilename()+"] exists in classpath");
}
catch(FileNotFoundException e){
//
}
try{
urlResource1.getFile();
}
catch(FileNotFoundException e){
fail();
}
尤其注意fakeFileResource资源的类型,并不是我们所预期的FileSystemResource类型,而是ClassPathResource类型,这是由DefaultResourceLoader的资源查找逻辑所决定的。如果最终没有找到符合条件的相应资源,getResourceByPath(String)方法就会构造一个实际上并不存在的资源并返回。而指定有协议前缀的资源路径,则通过URL能够定位,所以,返回的都是UrlResource类型。
②FileSystemResourceLoader
FileSystemResourceLoader,它继承自DefaultResourceLoader,但覆写了getResourceByPath(String)方法,使之从文件系统加载资源并以FileSystemResource类型返回。这样,我们就可以取得预想的资源类型。
public void testResourceTypesWithFileSystemResourceLoader()
{
ResourceLoader resourceLoader = new FileSystemResourceLoader();
Resource fileResource = resourceLoader.getResource("D:/spring21site/README");
assertTrue(fileResource instanceof FileSystemResource);
assertTrue(fileResource.exists());
Resource urlResource = resourceLoader.getResource("file:D:/spring21site/README");
assertTrue(urlResource instanceof UrlResource);
}
2. ResourcePatternResolver——批量查找的ResourceLoader
ResourcePatternResolver是ResourceLoader的扩展,ResourceLoader每次只能根据资源路径返回确定的单个Resource实例,而ResourcePatternResolver则可以根据指定的资源路径匹配模式,每次返回多个Resource实例
public interface ResourcePatternResolver extends ResourceLoader {
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
Resource[] getResources(String locationPattern) throws IOException;
}
ResourcePatternResolver在继承ResourceLoader原有定义的基础上,又引入了Resource[] getResources(String)方法定义,以支持根据路径匹配模式返回多个Resources的功能。它同时还引入了一种新的协议前缀classpath*:,针对这一点的支持,将由相应的子类实现给出。
ResourcePatternResolver最常用的一个实现是PathMatchingResourcePatternResolver,该实现类支持ResourceLoader级别的资源加载,支持基于Ant风格的路径匹配模式(类似于*/.suffix之类的路径形式),支持ResourcePatternResolver新增加的classpath*:前缀等,基本上集所有技能于一身。
在构造PathMatchingResourcePatternResolver实例的时候,可以指定一个ResourceLoader,如果不指定的话, 则PathMatchingResourcePatternResolver内部会默认构造一个DefaultResourceLoader实例。PathMatchingResourcePatternResolver内部会将匹配后确定的资源路径,委派给它的ResourceLoader来查找和定位资源。这样,如果不指定任何ResourceLoader的话,PathMatchingResourcePatternResolver在加载资源的行为上会与DefaultResourceLoader基本相同,只存在返回的Resource数量上的差异。
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
Resource fileResource = resourceResolver.getResource("D:/spring21site/README");
assertTrue(fileResource instanceof ClassPathResource);
assertFalse(fileResource.exists());
...
不过,可以通过传入其他类型的ResourceLoader来替换PathMatchingResourcePatternResolver内部默认使用的DefaultResourceLoader,从而改变其默认行为。
public void testResourceTypesWithPathMatchingResourcePatternResolver()
{
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
Resource fileResource = resourceResolver.getResource("D:/spring21site/README");
assertTrue(fileResource instanceof ClassPathResource);
assertFalse(fileResource.exists());
resourceResolver = new PathMatchingResourcePatternResolver(new FileSystemResourceLoader());
fileResource = resourceResolver.getResource("D:/spring21site/README");
assertTrue(fileResource instanceof FileSystemResource);
assertTrue(fileResource.exists());
}
现在我们应该对Spring的统一资源加载策略有了一个整体上的认识
虚线三角形是实现(implement),实现三角形是继承(extend)
5.1.3 ApplicationContext与ResourceLoader
ApplicationContext继承了ResourcePatternResolver,当然就间接实现了ResourceLoader接口。所以,任何的ApplicationContext实现都可以看作是一个ResourceLoader甚至ResourcePatternResolver。而这就是ApplicationContext支持Spring内统一资源加载策略的真相。
通常,所有的ApplicationContext实现类会直接或者间接地继承AbstractApplicationContext,AbstractApplicationContext继承了DefaultResourceLoader,那么,它的getResource(String)当然就直接用DefaultResourceLoader的了。
AbstractApplicationContext类的内部声明有一个resourcePatternResolver,类型是ResourcePatternResolver,对应的实例类型为PathMatchingResourcePatternResolver。整个ApplicationContext的实现类就完全可以支持ResourceLoader或者ResourcePatternResolver接口
图5-2给出了AbstractApplicationContext与ResourceLoader和ResourcePatternResolver之间的类层次关系
1. 扮演ResourceLoader的角色
既然ApplicationContext可以作为ResourceLoader或者ResourcePatternResolver来使用,那么我们可以通过ApplicationContext来加载任何Spring支持的Resource类型。
ResourceLoader resourceLoader = new ClassPathXmlApplicationContext("配置文件路径");
// 或者
// ResourceLoader resourceLoader = new FileSystemXmlApplicationContext("配置文件路径");
Resource fileResource = resourceLoader.getResource("D:/spring21site/README");
assertTrue(fileResource instanceof ClassPathResource);
assertFalse(fileResource.exists());
Resource urlResource2 = resourceLoader.getResource("http://www.spring21.cn");
assertTrue(urlResource2 instanceof UrlResource);
2. ResourceLoader类型的注入
假设我们有类定义如代码清单5-6所示,它依赖于ResourceLoader
public class FooBar {
private ResourceLoader resourceLoader;
public void foo(String location)
{
System.out.println(getResourceLoader().getResource(location).getClass());
}
public ResourceLoader getResourceLoader() {
return resourceLoader;
}
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}
给它注入DefaultResourceLoader
<bean id="resourceLoader" class="org.springframework.core.io.DefaultResourceLoader">
</bean>
<bean id="fooBar" class="...FooBar">
<property name="resourceLoader">
<ref bean="resourceLoader"/>
</property>
</bean>
ApplicationContext容器本身就是一个ResourceLoader,我们为了该类还需要单独提供一个resourceLoader实例就有些多余了,直接将当前的ApplicationContext容器作为ResourceLoader注入就行了
现在, 修改我们的FooBar 定义, 让其实现ResourceLoaderAware 或者ApplicationContextAware接口
实现ResourceLoaderAware
public class FooBar implements ResourceLoaderAware{
private ResourceLoader resourceLoader;
public void foo(String location)
{
System.out.println(getResourceLoader().getResource(location).getClass());
}
public ResourceLoader getResourceLoader() {
return resourceLoader;
}
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}
实现ApplicationContextAware接口
public class FooBar implements ApplicationContextAware{
private ResourceLoader resourceLoader;
public void foo(String location)
{
System.out.println(getResourceLoader().getResource(location).getClass());
}
public ResourceLoader getResourceLoader() {
return resourceLoader;
}
public void setApplicationContext(ApplicationContext ctx) throws BeansException
{
this.resourceLoader = ctx;
}
}
直接将一个FooBar配置到bean定义文件即可
<bean id="fooBar" class="...FooBar">
</bean>
容器启动的时候,就会自动将当前ApplicationContext容器本身注入到FooBar中,因为ApplicationContext类型容器可以自动识别Aware接口。
3. Resource类型的注入
默认情况下,BeanFactory容器不会为Resource类型提供相应的PropertyEditor,如果我们想注入Resource类型的bean定义,就需要注册自定义的PropertyEditor到BeanFactory容器。
不过,对于ApplicationContext来说,我们无需这么做,因为ApplicationContext容器可以正确识别Resource类型并转换后注入相关对象。
假设有一个XMailer类,它依赖于一个模板来提供邮件发送的内容,我们声明模板为Resource类型
public class XMailer {
private Resource template;
public void sendMail(Map mailCtx)
{
// String mailContext = merge(getTemplate().getInputStream(),mailCtx);
//...
}
public Resource getTemplate() {
return template;
}
public void setTemplate(Resource template) {
this.template = template;
}
}
该类定义与平常的bean定义没有什么差别,我们直接在配置文件中以String形式指定template所在位置,ApplicatonContext就可以正确地转换类型并注入依赖,配置内容如下:
<bean id="mailer" class="...XMailer">
<property name="template" value="..resources.default_template.vm"/>
...
< /bean>
ApplicationContext启动伊始,会通过一个ResourceEditorRegistrar来注册Spring提供的针对Resource类型的PropertyEditor实现到容器中,这个PropertyEditor叫做ResourceEditor。这样, ApplicationContext就可以正确地识别Resource类型的依赖了
如果应用对象需要依赖一组Resource,Spring 提供了ResourceArrayPropertyEditor实现,我们只需要通过CustomEditorConfigurar告知容器即可。
4. 在特定情况下,ApplicationContext的Resource加载行为
对于URL所接受的资源路径来说,通常开始都会有一个协议前缀,比如file:、http:、ftp:等。既然Spring使用UrlResource对URL定位查找的资源进行了抽象,那么,同样也支持这样类型的资源路径,而且,在这个基础上,Spring还扩展了协议前缀的集合。ResourceLoader中增加了一种新的资源路径协议——classpath:,ResourcePatternResolver又增加了一种——classpath*:。这样,我们就可以通过这些资源路径协议前缀,明确地告知Spring容器要从classpath中加载资源,如下所示:
// 代码中使用协议前缀
ResourceLoader resourceLoader = new
FileSystemXmlApplicationContext("classpath:conf/container-conf.xml");
// 配置中使用协议前缀
<bean id="..." class="...">
<property name="...">
<value>classpath:resource/template.vm</value>
</property>
</bean>
classpath*:与classpath:的唯一区别就在于,如果能够在classpath中找到多个指定的资源,则返回多个。我们可以通过这两个前缀改变某些ApplicationContext实现类的默认资源加载行为。
ClassPathXmlApplicationContext和FileSystemXmlApplicationContext在处理资源加载的默认行为上有所不同。
①当ClassPathXmlApplicationContext在实例化的时候,即使没有指明classpath:或者classpath*:等前缀,它会默认从classpath中加载bean定义配置文件,以下代码中演示的两种实例化方式效果是相同的:
ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");
//以及
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:conf/appContext.xml");
②FileSystemXmlApplicationContext则有些不同,如果我们像如下代码那样指定conf/appContext.xml,它会尝试从文件系统中加载bean定义文件:
ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/appContext.xml");
在资源路径之前增加classpath:前缀,明确指定FileSystemXmlApplicationContext从classpath中加载bean定义的配置文件:
ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");
它现在对应的是ClassPathResource类型的资源,而不是默认的FileSystemResource类型资源
之所以如此,是因为FileSystemXmlApplicationContext和FileSystemResourceLoader一样,也覆写了DefaultResourceLoader的getResourceByPath(String)方法,逻辑跟FileSystemResourceLoader一模一样。
当容器实例化并启动完毕,我们要用相应容器作为ResourceLoader来加载其他资源时,各种ApplicationContext容器的实现类依然会有不同的表现
①对于ClassPathXmlApplicationContext来说,如果我们不指定路径之前的前缀,它从Classpath中加载这种没有路径前缀的资源。如类似如下指定的资源路径,ClassPathXmlApplicationContext依然尝试从Classpath加载:
<bean id="..." class="...">
<property name="..." value="conf/appContext.xml"/>
</bean>
②容器类型为FileSystemXmlApplicationContext,如果不指定路径之前的前缀,将从文件系统中给我们加载该文件。也可以在这个时候用classpath:前缀强制指定FileSystemXmlApplicationContext从Classpath中加载该文件,如以下代码所示:
<bean id="..." class="...">
<property name="..." value="classpath:conf/appContext.xml"/>
</bean>
容器实例化并启动后,作为ResourceLoader来加载资源,如果不是每个地方都使用classpath: 前缀, 强制FileSystemXmlApplicationContext 从Classpath 中加载资源,FileSystemXmlApplicationContext还会默认从文件系统中加载资源。
5.2 国际化信息支持
5.2.1 Java SE提供的国际化支持
对于Java中的国际化信息处理,主要涉及两个类,即java.util.Locale和java.util.ResourceBundle
1. Locale
Locale类提供了三个构造方法,它们的定义如下:
Locale(String language)
Locale(String language, String country)
Locale(String language, String country, String variant)
这样我们就可以根据相应的语言代码和国家代码来构造相应的Locale,如以下所示:
Locale china = new Locale("zh","CN");
//相当于
Locale.CHINA
有了Locale,我们的应用程序就可以通过它来判别如何为不同的国家和地区的用户提供相应的信息。
2. ResourceBundle
ResourceBundle用来保存特定于某个Locale的信息(可以是String类型信息,也可以是任何类型的对象)。通常,ResourceBundle管理一组信息序列,所有的信息序列有统一的一个basename,然后特定的Locale的信息,可以根据basename后追加的语言或者地区代码来区分。比如,我们用一组properties文件来分别保存不同国家地区的信息,可以像下面这样来命名相应的properties文件:
messages.properties
messages_zh.properties
messages_zh_CN.properties
messages_en.properties
messages_en_US.properties
...
其中,文件名中的messages部分称作ResourceBundle将加载的资源的basename,其他语言或地区的资源在basename的基础上追加Locale特定代码。
每个资源文件中都有相同的键来标志具体资源条目,但每个资源内部对应相同键的资源条目内容,则根据Locale的不同而不同。如下代码片段演示了两个不同的资源文件内容的对比情况:
# messages_zh_CN.properties文件中
menu.file=文件({0})
menu.edit=编辑
...
# messages_en_US.properties文件中
menu.file=File({0})
menu.edit=Edit
...
有了ResourceBundle对应的资源文件之后,我们就可以通过ResourceBundle的getBundle(String baseName, Locale locale)方法取得不同Locale对应的ResourceBundle,然后根据资源的键取得相应Locale的资源条目内容。
5.2.2 MessageSource与ApplicationContext
Spring在Java SE的国际化支持的基础上,进一步抽象了国际化信息的访问接口,也就是MessageSource
public interface MessageSource {
String getMessage(String code, Object[] args, String defaultMessage, Locale locale);
String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws
NoSuchMessage zException;
}
①String getMessage(String code, Object[] args, String defaultMessage, Locale locale)
根据传入的资源条目的键(对应方法声明中的code参数)、信息参数以及Locale来查找信息,如果对应信息没有找到,则返回指定的defaultMessage
②String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;
与第一个方法相同,因为没有指定默认信息,当对应的信息找不到的情况下,将抛出NoSuchMessageException异常
③String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessage zException;
使用MessageSourceResolvable对象对资源条目的键、信息参数等进行封装,将封住了这些信息的MessageSourceResolvable对象作为查询参数来调用以上方法。如果根据MessageSourceResolvable中的信息查找不到相应条目内容,将抛出NoSuchMessageException异常。
ApplicationContext除了实现了ResourceLoader以支持统一的资源加载,它还实现了MessageSource接口,那么就跟ApplicationContext因为实现了ResourceLoader而可以当作ResourceLoader来使用一样,ApplicationContext现在也是一个MessageSource了。
在默认情况下,ApplicationContext将委派容器中一个名称为messageSource的MessageSource接口实现来完成MessageSource应该完成的职责。如果找不到这样一个名字的MessageSource实现,ApplicationContext内部会默认实例化一个不含任何内容的StaticMessageSource实例,以保证相应的方法调用。所以通常情况下,如果要提供容器内的国际化信息支持,我们会添加如代码清单5-9类似的配置信息到容器的配置文件中。
<beans>
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>messages</value>
<value>errorcodes</value>
</list>
</property>
</bean>
...
</beans>
可以通过ApplicationContext直接访问相应Locale对应的信息,如下所示:
ApplicationContext ctx = ...;
String fileMenuName = ctx.getMessage("menu.file", new Object[]{"F"}, Locale.US);
String editMenuName = ctx.getMessage("menu.file", null, Locale.US);
assertEquals("File(F)", fileMenuName);
assertEquals("Edit", editMenuName);
1. 可用的MessageSource实现
Spring提供了三种MessageSource的实现,即StaticMessageSource、ResourceBundleMessageSource和ReloadableResourceBundleMessageSource。
①StaticMessageSource
MessageSource接口的 简单实现,可以通过编程的方式添加信息条目,多用于测试,不应该用于正式的生产环境。
②ResourceBundleMessageSource
基于标准的 ResourceBundle而实现的MessageSource,对其父类AbstractMessageSource的行为进行了扩展,提供对多个ResourceBundle的缓存以提高查询速度。同时,对于参数化的信息和非参数化信息的处理进行了优化,并对用于参数化信息格式化的MessageFormat实例也进行了缓存。它是最常用的、用于正式生产环境下的MessageSource实现。
③ReloadableResourceBundleMessageSource
基于标准的ResourceBundle而构建的MessageSource实现类,通过其cacheSeconds属性可以指定时间段,以定期刷新并检查底层的properties资源文件是否有变更。对于properties资源文件的加载方式也与ResourceBundleMessageSource有所不同,可以通过ResourceLoader来加载信息资源文件。
这三种实现都可以独立于容器并在独立运行(Standalone形式)的应用程序中使用,而并非只能依托ApplicationContext才可使用。
代码清单5-10为我们演示了这三种MessageSource的简单使用
StaticMessageSource messageSource = new StaticMessageSource();
messageSource.addMessage("menu.file", Locale.US, "File");
messageSource.addMessage("menu.edit", Locale.US, "Edit");
assertEquals("File(F)", messageSource.getMessage("menu.file", new Object[]{"F"}, Locale.US));
assertEquals("Edit", messageSource.getMessage("menu.edit", null,"Edit", Locale.US));
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames(new String[]{"conf/messages"}); // 从 classpath加载资源文件
assertEquals("File(F)", messageSource.getMessage("menu.file", new Object[]{"F"}, Locale.US));
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames(new String[]{"file:conf/messages"}); // 从文件系统加载资源文件
assertEquals("File(F)", messageSource.getMessage("menu.file", new Object[]{"F"},Locale.US));
2. MessageSourceAware和MessageSource的注入
ApplicationContext启动的时候,会自动识别容器中类型为MessageSourceAware的bean定义,并将自身作为MessageSource注入相应对象实例中。
如果某个业务对象需要依赖于MessageSource的话,直接通过构造方法注入或者setter方法注入的方式声明依赖就可以了。只要配置bean定义时,将ApplicationContext容器内部的那个messageSource注入该业务对象即可。
假设我们有一个通用的Validator数据验证类,它需要通过MessageSource来返回相应的错误信息,那么可以为其声明一个MessageSource依赖,然后将ApplicationContext中的那个已经配置好的messageSource注入给它。代码清单5-11给出了该类的定义以及相关注入配置。
public class Validator
{
private MessageSource messageSource;
public ValidateResult validate(Object target)
{
// 执行相应验证逻辑
// 如果有错误,通过messageSource.getMessage(...)获取相应信息并放入验证结果对象中
// 返回验证结果(return result)
}
public MessageSource getMessageSource()
{
return messageSource;
}
public void setMessageSource(MessageSource msgSource) {
this.messageSource = msgSource;
}
// ...
}
<beans>
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>messages</value>
<value>errorcodes</value>
</list>
</property>
</bean>
<bean id="validator" class="...Validator">
<property name="messageSource" ref="messageSource"/>
</bean>
...
</beans>
5.3 容器内部事件发布
Spring的ApplicationContext容器提供的容器内事件发布功能,是通过提供一套基于Java SE标准自定义事件类而实现的。
5.3.1 自定义事件发布
Java SE提供了实现自定义事件发布(Custom Event publication)功能的基础类,即EventObject类和EventListener接口。所有的自定义事件类型可以通过扩展EventObject来实现,而事件的监听器则扩展自EventListener。
为了针对具体场景可以区分具体的事件类型,我们需要给出自己的事件类型的定义,通常做法是扩展EventObject类来实现自定义的事件类型。
public class MethodExecutionEvent extends EventObject {
private static final long serialVersionUID = -71960369269303337L;
private String methodName;
public MethodExecutionEvent(Object source) {
super(source);
}
public MethodExecutionEvent(Object source,String methodName)
{
super(source);
this.methodName = methodName;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
}
自定义的事件监听器需要在合适的时机监听自定义的事件,如刚声明的MethodExecutionEvent,我们可以在方法开始执行的时候发布该事件,也可以在方法执行即将结束之际发布该事件。相应地,自定义的事件监听器需要提供方法对这两种情况下接收到的事件进行处理。
public interface MethodExecutionEventListener extends EventListener {
// 处理方法开始执行的时候发布的MethodExecutionEvent事件
void onMethodBegin(MethodExecutionEvent evt);
//处理方法执行将结束时候发布的MethodExecutionEvent事件
void onMethodEnd(MethodExecutionEvent evt);
}
事件监听器接口定义首先继承了EventListener,然后针对不同的事件发布时机提供相应的处理方法定义,最主要的就是,这些处理方法所接受的参数就是MethodExecutionEvent类型的事件。也就是说,我们的自定义事件监听器类只负责监听其对应的自定义事件并进行处理。
自定义事件监听器具体实现类SimpleMethodExecutionEventListener的定义
public class SimpleMethodExecutionEventListener implements MethodExecutionEventListener {
public void onMethodBegin(MethodExecutionEvent evt) {
String methodName = evt.getMethodName();
System.out.println("start to execute the method["+methodName+"].");
}
public void onMethodEnd(MethodExecutionEvent evt) {
String methodName=evt.getMethodName();
System.out.println("finished to execute the method["+methodName+"].");
}
}
有了自定义事件和自定义事件监听器,剩下的就是发布事件,然后让相应的监听器监听并处理事件了。通常情况下,我们会有一个事件发布者(EventPublisher),它本身作为事件源,会在合适的时点,将相应事件发布给对应的事件监听器。
public class MethodExeuctionEventPublisher {
private List<MethodExecutionEventListener> listeners = new ArrayList<MethodExecutionEventListener>();
public void methodToMonitor()
{
MethodExecutionEvent event2Publish = new MethodExecutionEvent(this,"methodToMonitor");
publishEvent(MethodExecutionStatus.BEGIN,event2Publish);
// 执行实际的方法逻辑
// ...
publishEvent(MethodExecutionStatus.END,event2Publish);
}
protected void publishEvent(MethodExecutionStatus status,
MethodExecutionEvent methodExecutionEvent) {
List<MethodExecutionEventListener> copyListeners =
new ArrayList<MethodExecutionEventListener>(listeners);
for(MethodExecutionEventListener listener:copyListeners)
{
if(MethodExecutionStatus.BEGIN.equals(status))
listener.onMethodBegin(methodExecutionEvent);
else
listener.onMethodEnd(methodExecutionEvent);
}
}
public void addMethodExecutionEventListener(MethodExecutionEventListener listener)
{
this.listeners.add(listener);
}
public void removeListener(MethodExecutionEventListener listener)
{
if(this.listeners.contains(listener))
this.listeners.remove(listener);
}
public void removeAllListeners()
{
this.listeners.clear();
}
public static void main(String[] args) {
MethodExeuctionEventPublisher eventPublisher = new MethodExeuctionEventPublisher();
eventPublisher.addMethodExecutionEventListener(new SimpleMethodExecutionEventListener());
eventPublisher.methodToMonitor();
}
}
①具体时点上自定义事件的发布
方法methodToMonitor()是事件发布的源头,MethodExeuctionEventPublisher在该方法开始和即将结束的时候,分别针对这两个时点发布MethodExecutionEvent事件。具体实现上,每个时点发布的事件会通过MethodExecutionEventListener的相应方法传给注册的监听者并被处理掉。在实现中,需要注意到,为了避免事件处理期间事件监听器的注册或移除操作影响处理过程,我们对事件发布时点的监听器列表进行了一个安全复制(safe-copy)。另外,事件的发布是顺序执行,所以为了能够不影响处理性能,事件监听器的处理逻辑应该尽量简短。
②自定义事件监听器的管理
MethodExeuctionEventPublisher类提供了与事件监听器的注册和移除相关的方法,这样,客户端可以根据情况决定是否需要注册或者移除某个事件监听器。如果没有提供remove事件监听器的方法,那么注册的监听器实例会一直被MethodExeuctionEventPublisher引用, 即使已经过期了或者废弃不用了, 也依然存在于MethodExeuctionEventPublisher的监听器列表中。这会导致隐性的内存泄漏,在任何事件监听器的处理上都可能出现这种问题。
整个Java SE中标准的自定义事件实现就是这个样子,基本上涉及三个角色,即自定义的事件类型、
自定义的事件监听器和自定义的事件发布者,关系如图5-4所示。
5.3.2 Spring 的容器内事件发布类结构分析
Spring 的ApplicationContext 容器内部允许以ApplicationEvent的形式发布事件, 容器内注册的ApplicationListener类型的bean定义会被ApplicationContext容器自动识别,它们负责监听容器内发布的所有ApplicationEvent类型的事件。也就是说,一旦容器内发布ApplicationEvent及其子类型的事件,注册到容器的ApplicationListener就会对这些事件进行处理。
①ApplicationEvent
Spring容器内自定义事件类型,继承自EventObject,它是一个抽象类,需要根据情况提供相应子类以区分不同情况。默认情况下,Spring提供了三个实现。
(1)ContextClosedEvent:ApplicationContext容器在即将关闭的时候发布的事件类型。
(2)ContextRefreshedEvent:ApplicationContext容器在初始化或者刷新的时候发布的事件类型。
(3)RequestHandledEvent:Web请求处理后发布的事件,其有一子类ServletRequestHandledEvent提供特定于Java EE的Servlet相关事件
②ApplicationListener
ApplicationContext容器内使用的自定义事件监听器接口定义,继承自Listener。ApplicationContext容器在启动时,会自动识别并加载EventListener类型bean定义,一旦容器内有事件发布,将通知这些注册到容器的EventListener。
③ApplicationContext
除了ResourceLoader和MessageSource,ApplicationContext接口定义还继承了ApplicationEventPublisher接口,该接口提供了void publishEvent(ApplicationEvent event)方法定义。不难看出,ApplicationContext容器现在担当的就是事件发布者的角色。
ApplicationEventMulticaster接口定义了具体事件监听器的注册管理以及事件发布的方法,其有一抽象实现类AbstractApplicationEventMulticaster,它实现了事件监听器的管理功能。
出于灵活性和扩展性考虑,事件的发布功能则委托给了其子类。SimpleApplicationEventMulticaster 是Spring 提供的AbstractApplicationEventMulticaster的一个子类实现,添加了事件发布功能的实现。不过,其默认使用了SyncTaskExecutor进行事件的发布。与我们给出的样例事件发布者实现一样,事件是同步顺序发布的。为了避免这种方式可能存在的性能问题, 我们可以为其提供其他类型的TaskExecutor 实现类
ApplicationContext容器的事件发布功能全部委托给了ApplicationEventMulticaster来做,所以,容器启动伊始,就会检查容器内是否存在名称为applicationEventMulticaster的ApplicationEventMulticaster对象实例。有的话就使用提供的实现,没有则默认初始化一个SimpleApplicationEventMulticaster作为将会使用的ApplicationEventMulticaster
5.3.3 Spring 容器内事件发布的应用
Spring的ApplicationContext容器内的事件发布机制,主要用于单一容器内的简单消息通知和处理,并不适合分布式、多进程、多容器之间的事件通知。
可以通过如下两种方式为我们的业务对象注入ApplicationEventPublisher的依赖
①使用ApplicationEventPublisherAware接口
在ApplicationContext类型的容器启动时,会自动识别该类型的bean定义并将ApplicationContext容器本身作为ApplicationEventPublisher注入当前对象,而ApplicationContext容器本身就是一个ApplicationEventPublisher
②使用ApplicationContextAware接口
既然ApplicationContext本身就是一个ApplicationEventPublisher,那么通过ApplicationContextAware几乎达到第一种方式相同的效果
1. MethodExecutionEvent的改装
因为ApplicationListener只通过void onApplicationEvent(ApplicationEvent event)这一个事件处理方法来处理事件, 所以现在要在事件类中尽量保存必要的信息。改装后的MethodExecutionEvent类定义如代码清单5-16所示。
public class MethodExecutionEvent extends ApplicationEvent {
private static final long serialVersionUID = -71960369269303337L;
private String methodName;
private MethodExecutionStatus methodExecutionStatus;
public MethodExecutionEvent(Object source) {
super(source);
}
public MethodExecutionEvent(Object source,String methodName, MethodExecutionStatus methodExecutionStatus)
{
super(source);
this.methodName = methodName;
this.methodExecutionStatus = methodExecutionStatus;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public MethodExecutionStatus getMethodExecutionStatus() {
return methodExecutionStatus;
}
public void setMethodExecutionStatus(MethodExecutionStatus methodExecutionStatus)
{
this.methodExecutionStatus = methodExecutionStatus;
}
}
2. MethodExecutionEventListener
我们的MethodExecutionEventListener不再是接口,而是具体的ApplicationListener实现类。因为ApplicationListener已经取代了MethodExecutionEventListener原来的角色,所以,改装后的MethodExecutionEventListener定义如下:
public class MethodExecutionEventListener implements ApplicationListener {
public void onApplicationEvent(ApplicationEvent evt) {
if(evt instanceof MethodExecutionEvent)
{
// 执行处理逻辑
}
}
}
3. MethodExeuctionEventPublisher改造
public class MethodExeuctionEventPublisher implements ApplicationEventPublisherAware {
private ApplicationEventPublisher eventPublisher;
public void methodToMonitor()
{
MethodExecutionEvent beginEvt = new
MethodExecutionEvent(this,"methodToMonitor",MethodExecutionStatus.BEGIN);
this.eventPublisher.publishEvent(beginEvt);
// 执行实际方法逻辑
// ...
MethodExecutionEvent endEvt = new
MethodExecutionEvent(this,"methodToMonitor",MethodExecutionStatus.END);
this.eventPublisher.publishEvent(endEvt);
}
public void setApplicationEventPublisher(ApplicationEventPublisher appCtx) {
this.eventPublisher = appCtx;
}
}
直接使用注入的eventPublisher来发布事件,而不用自己实现事件发布逻辑了。需要注意的就是,我们实现了ApplicationEventPublisherAware接口(当然ApplicationContextAware也是可以的)。
4. 注册到ApplicationContext容器
最后一步工作就是将MethodExeuctionEventPublisherMethodExecutionEventListener注册到ApplicationContext容器中。当MethodExeuctionEventPublisher的methodToMonitor方法被调用时,事件即被发布。配置如下所示:
<bean id="methodExecListener" class="...MethodExecutionEventListener">
</bean>
<bean id="evtPublisher" class="...MethodExeuctionEventPublisher">
</bean>
5.4 多配置模块加载的简化
在使用Spring的IoC轻量级容器进行实际开发的过程中,为了避免出现整个团队因某个资源独占而无法并行、高效地完成工作等问题,通常会将整个系统的配置信息按照某种关注点进行分割,使得关注点逻辑良好地划分到不同的配置文件中,如按照功能模块或者按照系统划分的层次等。这样,在加载整个系统的bean定义时,就需要让容器同时读入划分到不同配置文件的信息。相对于BeanFactory来说,ApplicationContext大大简化了这种情况下的多配置文件的加载工作。
假设在文件系统中存在多个Spring的配置文件,它们所在路径如下所示:
{user.dir}/conf/dao-tier.springxml
{user.dir}/conf/view-tier.springxml
{user.dir}/conf/business-tier.springxml
...
通过ApplicationContext,我们只要以String[]形式传入这些配置文件所在的路径,即可构造并启动容器
String[] locations = new String[]{ "conf/dao-tier.springxml", "conf/view-tier.springxml", "conf/business-tier.springxml"};
ApplicationContext container = new FileSystemXmlApplicationContext(locations);
// 或者
ApplicationContext container = new ClassPathXmlApplicationContext(locations);
...
//甚至于使用通配符
ApplicationContext container = new FileSystemXmlApplicationContext("conf/**/*.springxml");
...
而使用BeanFactory来加载这些配置,则需要动用过多的代码,如以下代码所示:
BeanFactory parentFactory = new XmlBeanFactory(
new FileSystemResource("conf/dao-tier.springxml"));
BeanFactory subFactory = new XmlBeanFactory(
new FileSystemResource("conf/view-tier.springxml"),parentFactory);
BeanFactory subsubFactory = new XmlBeanFactory(
new FileSystemResource("conf/business-tier.springxml"),subFactory);
...
BeanFactory container = new XmlBeanFactory(new FileSystemResource("..."),sub...Factory);
...
除了可以批量加载配置文件之外,ClassPathXmlApplicationContext还可以通过指定Classpath中的某个类所处位置来加载相应配置文件,配置文件分布结构如下
com/
foo/
services.xml
daos.xml
MessengerService.class
ClassPathXmlApplicationContext可以通过MessengerService类在Classpath中的位置定位配置文件,而不用指定每个配置文件的完整路径名,如以下代码所示:
ApplicationContext ctx = new ClassPathXmlApplicationContext(
new String[] {"services.xml", "daos.xml"}, MessengerService.class);