Spring复杂的BeanFactory继承体系该如何理解?---下
Posted 大忽悠爱忽悠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring复杂的BeanFactory继承体系该如何理解?---下相关的知识,希望对你有一定的参考价值。
Spring复杂的BeanFactory继承体系该如何理解?---下
前文链接:
Spring复杂的BeanFactory继承体系该如何理解? ----上
Spring复杂的BeanFactory继承体系该如何理解? ----中
Spring复杂的BeanFactory继承体系该如何理解?—中下
国际化信息支持(I18n MessageSource)
全世界有很多不同的国家和地区,每个国家或者地区都使用各自的语言文字。在当今全球化的信息大潮中,要让我们的应用程序可以供全世界不同国家和地区的人们使用,应用程序就必须支持它所面向的国家和地区的语言文字,为不同的国家和地区的用户提供他们各自的语言文字信息。所以,要向全世界推广,应用程序的国际化信息支持自然是势在必行。
Java SE 提供的国际化支持
程序的国际化不是三言两语可以讲清楚的,它涉及许多的内容,如货币形式的格式化、时间的表现形式、各国家和地区的语言文字等。要全面了解Java中的I18n,建议参考O’Reilly出版的JavaInternationalization。我们这里主要是简单地介绍基本概念,以便你可以对Spring中的国际化信息支持有更好的了解。
对于Java中的国际化信息处理,主要涉及两个类,即java.util.Locale和java.util.ResourceBundle。
1. Locale
不同的Locale代表不同的国家和地区,每个国家和地区在Locale这里都有相应的简写代码表示,包括语言代码以及国家代码,这些代码是ISO标准代码。
如,Locale.CHINA代表中国,它的代码表示为zh_CN;Locale.US代表美国地区,代码表示为en_US;而美国和英国等都属于英语地区,则可以使用Locale.ENGLISH来统一表示,这时代码只有语言代码,即en。
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都提供有静态常量,不用我们自己重新构造。一些不常用的Locale的则需要根据相应的国家和地区以及语言来进行构造。有了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
...
注意 按照规定,properties文件内容是以ISO-8859-1编码的,所以,实际上message_zh_CN.properties中各个键对应的内容是不应该以中文提供的,应该使用native2ascii或者类似的相关工具进行转码,这里如此举例,只是为了更好地说明差别。
有了ResourceBundle对应的资源文件之后,我们就可以通过ResourceBundle的getBundle(String baseName, Locale locale)方法取得不同Locale对应的ResourceBundle,然后根据资源的键取得相应Locale的资源条目内容。
通过结合ResourceBundle和Locale,我们就能够实现应用程序的国际化信息支持。
MessageSource与ApplicationContext
Spring在Java SE的国际化支持的基础上,进一步抽象了国际化信息的访问接口,也就是org.springframework.context.MessageSource
,该接口定义如下:
public interface MessageSource
@Nullable
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessage zException;
通过该接口,我们统一了国际化信息的访问方式。传入相应的Locale、资源的键以及相应参数,就可以取得相应的信息,再也不用先根据Locale取得ResourceBundle,然后再从ResourceBundle查询信息了。对MessageSource所提供的三个方法的简单说明如下。
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 NoSuchMessageException
。使用MessageSourceResolvable对象对资源条目的键、信息参数等进行封装,将封住了这些信息的MessageSourceResolvable对象作为查询参数来调用以上方法。如果根据MessageSourceResolvable中的信息查找不到相应条目内容,将抛出NoSuchMessageException异常。
现在我们知道,ApplicationContext除了实现了ResourceLoader以支持统一的资源加载,它还实现了MessageSource接口,那么就跟ApplicationContext因为实现了ResourceLoader而可以当作ResourceLoader来使用一样,ApplicationContext现在也是一个MessageSource了。
在默认情况下,ApplicationContext将委派容器中一个名称为messageSource的MessageSource接口实现来完成MessageSource应该完成的职责。如果找不到这样一个名字的MessageSource实现,ApplicationContext内部会默认实例化一个不含任何内容的StaticMessageSource实例,以保证相应的方法调用。所以通常情况下,如果要提供容器内的国际化信息支持,我们会添加如下代码类似的配置信息到容器的配置文件中。
ApplicationContext容器内使用的messageSource的配置实例
<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。
org.springframework.context.support.StaticMessageSource
。MessageSource接口的简单实现,可以通过编程的方式添加信息条目,多用于测试,不应该用于正式的生产环境。
org.springframework.context.support.ResourceBundleMessageSource
。基于标准的java.util.ResourceBundle而实现的MessageSource,对其父类AbstractMessageSource的行为进行了扩展,提供对多个ResourceBundle的缓存以提高查询速度。同时,对于参数化的信息和非参数化信息的处理进行了优化,并对用于参数化信息格式化的MessageFormat实例也进行了缓存。它是最常用的、用于正式生产环境下的MessageSource实现。
org.springframework.context.support.ReloadableResourceBundleMessageSource
。同样基于标准的java.util.ResourceBundle而构建的MessageSource实现类,但通过其cacheSeconds属性可以指定时间段,以定期刷新并检查底层的properties资源文件是否有变更。
对于properties资源文件的加载方式也与ResourceBundleMessageSource有所不同,可以通过ResourceLoader来加载信息资源文件。使用ReloadableResourceBundleMessageSource时,应该避免将信息资源文件放到classpath中,因为这无助于ReloadableResourceBundleMessageSource定期加载文件变更。更多信息参照该类的Javadoc。
这三种实现都可以独立于容器并在独立运行(Standalone形式)的应用程序中使用,而并非只能依托ApplicationContext才可使用。
三种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));
之前提到,ApplicationContext需要其配置文件中有一个名称为messageSource的MessageSource实现,自然就是以上的三选一了。
至此,我们有了下图
如果说以上三种MessageSource的实现还是不能满足你的要求,那么直接继承AbstractMessageSource,然后覆写几个方法就行了,甚至可以直接实现MessageSource接口,如果你的需求真的那么特别。
2. MessageSourceAware和MessageSource的注入
ApplicationContext启动的时候,会自动识别容器中类型为MessageSourceAware的bean定义, 并将自身作为MessageSource注入相应对象实例中。
如果某个业务对象需要国际化的信息支持,那么最简单的办法就是让它实现MessageSourceAware接口,然后注册到ApplicationContext容器。不过这样一来,该业务对象对ApplicationContext容器的依赖性就太强了,显得容器具有较强的侵入性。
而实际上,如果真的某个业务对象需要依赖于MessageSource的话,直接通过构造方法注入或者setter方法注入的方式声明依赖就可以了。
只要配置bean定义时,将ApplicationContext容器内部的那个messageSource注入该业务对象即可。假设我们有一个通用的Validator数据验证类,它需要通过MessageSource来返回相应的错误信息,那么可以为其声明一个MessageSource依赖,然后将ApplicationContext中的那个已经配置好的messageSource注入给它。
依赖于MessageSource的Validator类定义以及相关注入配置
public class Validator
private MessageSource messageSource;
public ValidateResult validate(Object target)
// 执行相应验证逻辑
// 如果有错误,通过messageSource.getMessage(...)获取相应信息并放入验证结果对象中
// 返回验证结果(return result) 9
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>
与通常的依赖注入没有什么区别,不是吗?
既然MessageSource可以独立使用,那为什么还让ApplicationContext实现该接口呢?
在独立运行的应用程序(Standalone Application)中,就如我们上面这些应用场景所展示的那样,直接使用MessageSource的相应实现类就行了。
不过在Web应用程序中,通常会公开ApplicationContext给视图(View)层,这样,通过标签(tag)就可以直接访问国际化信息了。
容器内部事件发布
Spring的ApplicationContext容器提供的容器内事件发布功能,是通过提供一套基于Java SE标准自定义事件类而实现的。为了更好地了解这组自定义事件类,我们可以先从Java SE的标准自定义事件类实现的推荐流程说起。
自定义事件发布
Java SE提供了实现自定义事件发布(Custom Event publication)功能的基础类,即java.util.EventObject类和java.util.EventListener接口。所有的自定义事件类型可以通过扩展EventObject来实现,而事件的监听器则扩展自EventListener。下面让我们看一下要实现一套自定义事件发布类的架构,应该如何来做。
给出自定义事件类型(define your own event object)。为了针对具体场景可以区分具体的事件类型,我们需要给出自己的事件类型的定义,通常做法是扩展java.util.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类型,它继承自EventObject,当该类型的事件发布之后,相应的监听器即可对该类型的事件进行处理。如果需要,自定义事件类可以根据情况提供更多信息,不用担心自定义事件类的“承受力”。
实现针对自定义事件类的事件监听器接口(define custom event listener)。自定义的事件监听器需要在合适的时机监听自定义的事件,如刚声明的MethodExecutionEvent,我们可以在方法开始执行的时候发布该事件,也可以在方法执行即将结束之际发布该事件。
相应地,自定义的事件监听器需要提供方法对这两种情况下接收到的事件进行处理。MethodExecutionEvent的事件监听器接口定义。
自定义事件监听器MethodExecutionEventListener定义
public interface MethodExecutionEventListener extends EventListener
/**
* 处理方法开始执行的时候发布的MethodExecutionEvent事件
*/
void onMethodBegin(MethodExecutionEvent evt);
/**
* 处理方法执行将结束时候发布的MethodExecutionEvent事件 4
*/
void onMethodEnd(MethodExecutionEvent evt);
事件监听器接口定义首先继承了java.util.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),它本身作为事件源,会在合适的时点,将相应事件发布给对应的事件监听器。
MethodExeuctionEventPublisher时间发布者类定义
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中标准的自定义事件实现就是这个样子,基本上涉及三个角色,即自定义的事件类型、自定义的事件监听器和自定义的事件发布者。
Spring 的容器内事件发布类结构分析
Spring 的 ApplicationContext 容器内部允许以 org.springframework.context.ApplicationEvent的形式发布事件,容器内注册的org.springframework.context.ApplicationListener类型的bean定义会被ApplicationContext容器自动识别,它们负责监听容器内发布的所有
ApplicationEvent类型的事件。
也就是说,一旦容器内发布ApplicationEvent及其子类型的事件,注册到容器的ApplicationListener就会对这些事件进行处理。
我想你已经猜到是怎么回事了。
-
ApplicationEvent
Spring容器内自定义事件类型,继承自java.util.EventObject,它是一个抽象类,需要根据情况提供相应子类以区分不同情况。默认情况下,Spring提供了三个实现。
ContextClosedEvent:ApplicationContext容器在即将关闭的时候发布的事件类型。
ContextRefreshedEvent:ApplicationContext容器在初始化或者刷新的时候发布的事件类型。
RequestHandledEvent:Web请求处理后发布的事件,其有一子类ServletRequestHandledEvent提供特定于Java EE的Servlet相关事件。
-
ApplicationListener
ApplicationContext容器内使用的自定义事件监听器接口定义,继承自java.util.EventListener。ApplicationContext容器在启动时,会自动识别并加载EventListener类型bean定义,一旦容器内有事件发布,将通知这些注册到容器的EventListener。
-
ApplicationContext
还记得ApplicationContext的定义吧?
除了之前的ResourceLoader和MessageSource,ApplicationContext接口定义还继承了ApplicationEventPublisher接口,该接口提供了void publishEvent(ApplicationEvent event)方法定义。不难看出,ApplicationContext容器现在担当的就是事件发布者的角色。
虽然ApplicationContext继承了ApplicationEventPublisher接口而担当了事件发布者的角色,但是在具体实现上,与之前提到的自定义事件实现流程有些许差异,且让我一一道来……
ApplicationContext容器的具体实现类在实现事件的发布和事件监听器的注册方面,并没事必躬亲,而是把这些活儿转包给了一个称作org.springframework.context.event.ApplicationEventMulticaster
的接口。
该接口定义了具体事件监听器的注册管理以及事件发布的方法,但接口终归是接口,还得有具体实现。
ApplicationEventMulticaster
有一抽象实现类——org.spring.framework.context.event.AbstractApplicationEventMulticaster
,它实现了事件监听器的管理功能。
出于灵活性和扩展性考虑,事件的发布功能则委托给了其子类。org.springframework. context.event.SimpleApplicationEventMulticaster
是 Spring 提供的 AbstractApplicationEventMulticaster的一个子类实现,添加了事件发布功能的实现。
不过,其默认使用了SyncTaskExecutor
进行事件的发布。
与我们给出的样例事件发布者实现一样,事件是同步顺序发布的。为了避免这种方式可能存在的性能问题,我们可以为其提供其他类型的 TaskExecutor 实现类(TaskExecutor的概念将在后面详细介绍)。
因为ApplicationContext容器的事件发布功能全部委托给了ApplicationEventMulticaster来做,所以,容器启动伊始,就会检查容器内是否存在名称为applicationEventMulticaster的ApplicationEventMulticaster对象实例。
有的话就使用提供的实现,没有则默认初始化一个SimpleApplicationEventMulticaster作为将会使用的ApplicationEventMulticaster。这样,整个Spring容器内事件发布功能实现结构图就有了。
Spring 容器内事件发布的应用
Spring的ApplicationContext容器内的事件发布机制,主要用于单一容器内的简单消息通知和处理,并不适合分布式、多进程、多容器之间的事件通知。
虽然可以通过Spring的Remoting支持,“曲折一点”来实现较为复杂的需求,但是难免弊大于利,失大于得。其他消息机制处理较复杂场景或许更合适。
所以,我们应该在合适的地点、合适的需求分析的前提下,合理地使用Spring提供的ApplicationContext容器内的事件发布机制。
要让我们的业务类支持容器内的事件发布,需要它拥有ApplicationEventPublisher的事件发布支持。所以,需要为其注入ApplicationEventPublisher实例。可以通过如下两种方式为我们的业务对象注入ApplicationEventPublisher的依赖。
使用ApplicationEventPublisherAware
接口。在ApplicationContext类型的容器启动时,会自动识别该类型的bean定义并将ApplicationContext容器本身作为ApplicationEventPublisher注入当前对象,而ApplicationContext容器本身就是一个ApplicationEventPublisher。
使用ApplicationContextAware接口。既然ApplicationContext本身就是一个ApplicationEventPublisher,那么通过ApplicationContextAware几乎达到第一种方式相同的
下面,我们把之前的MethodExecutionEvent相关类改装一下,也好看看改装成使用容器内的事件发布到底是个什么样子。
1. MethodExecutionEvent的改装
因为 ApplicationListener只通过void onApplicationEvent(ApplicationEvent event)这一个事件处理方法来处理事件,所以现在要在事件类中尽量保存必要的信息。
改装后的MethodExecutionEvent类定义
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 以上是关于Spring复杂的BeanFactory继承体系该如何理解?---下的主要内容,如果未能解决你的问题,请参考以下文章
Spring复杂的BeanFactory继承体系该如何理解? ----中
Spring复杂的BeanFactory继承体系该如何理解?---下