3.15 ApplicationContext的额外功能 {#toc_17}
正如本章开头所讨论的那样,org.springframework.beans.factory包提供基本的功能来管理和操作bean,包括以编程的方式。The org.springframework.context包增加了ApplicationContext接口,它继承了BeanFactory接口,除了以面向应用框架的风格扩展接口来提供一些额外的功能。很多人以完全声明的方式使用ApplicationContext,甚至没有以编程的方式去创建它,而是依赖诸如ContextLoader等支持类来自动的实例化ApplicationContext,作为Java EE web应用程序正常启动的一部分。
为了增强BeanFactory在面向框架风格的功能,上下文的包还提供了以下的功能:
- 通过MessageSource接口访问i18n风格的消息
- 通过ResourceLoader接口访问类似URL和文件资源
- 通过ApplicationEventPublisher接口,即bean实现ApplicationListener接口来进行事件发布
- 通过HierarchicalBeanFactory接口实现加载多个(分层)上下文,允许每个上下文只关注特定的层,例如应用中的web层
3.15.1 使用MessageSource 国际化 {#toc_18}
ApplicationContext接口继承了一个叫做MessageSource的接口,因此它也提供了国际化(i18n)的功能。Spring也提供了HierarchicalMessageSource接口,它可以分层去解析信息。这些接口共同为Spring消息效应解析提供了基础。这些接口上定义的方法包括:
- String getMessage(String code, Object[] args, String default, Locale loc): 这个基础的方法用来从MessageSource检索消息。当指定的区域中没有发现消息时,将使用默认的。任何参数传递都将使用标准库提供的MessageFormat变成替换值。
- String getMessage(String code, Object[] args, Locale loc): 本质上和前面提供的方法相同,只有一个区别,就是当没有指定消息,又没有发现消息,将会抛出NoSuchMessageException 异常。
- String getMessage(MessageSourceResolvable resolvable, Locale locale): 所有的属性处理方法都被包装在一个名为MessageSourceResolvable的类中,你可以使用此方法。
当ApplicationContext被载入的时候,它会自动的在上下文中去搜索定义的MessageSource bean。这个bean必须有messageSource的名称。如果找到这么一个bean,所有上述方法的调用都会委托给消息源。如果没有发现消息源,ApplicationContext会尝试寻找一个同名的父消息源。如果是这样,它会将那个bean作为MessageSource。如果ApplicationContext没有找到任何的消息源,那么一个空的DelegatingMessageSource将被实例化,以便能够接受到对上述定义方法的调用。
Spring提供了ResourceBundleMessageSource和StaticMessageSource两个MessageSource实现。它们两个都实现了HierarchicalMessageSource以便处理嵌套消息。StaticMessageSource很少使用,但是它提供了通过编程的方式增加消息源。下面展示ResourceBundleMessageSource使用的例子:
<beans>
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>format</value>
<value>exceptions</value>
<value>windows</value>
</list>
</property>
</bean>
</beans>
在上面的例子中,假设在类路径下定义了format,exceptions和windows三个资源包。解析消息的任何请求都会通过ResourceBundles被JDK以标准方式处理。为了举例说明,假设上述两个资源包的文件内容是…
# in format.properties
message=Alligators rock!
# in exceptions.properties
argument.required=The {0} argument is required.
下面的实例展示了执行MessageSource功能的程序。记住所有的ApplicationContext的实现也是MessageSource的实现,而且它可以被强转为MessageSource接口。
public static void main(String[] args) {
MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
String message = resources.getMessage("message", null, "Default", null);
System.out.println(message);
}
上面的程序输出的结果为:
Alligators rock!
所以总结一下,MessageSource是定义在一个名为beans.xml,它存在类路径的跟目录下。messageSource bean定义通过basenames属性引用了很多的资源。在列表中传递给basenames属性的三个文件作为类路径下根目录中的文件存在,分别为format.properties, exceptions.properties, and windows.properties。
下一个例子展示传递给消息查找的参数;这些参数将会被转换为字符串并插入到消息查找的占位符中。
<beans>
<!-- this MessageSource is being used in a web application -->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="exceptions"/>
</bean>
<!-- lets inject the above MessageSource into this POJO -->
<bean id="example" class="com.foo.Example">
<property name="messages" ref="messageSource"/>
</bean>
</beans>
public class Example {
private MessageSource messages;
public void setMessages(MessageSource messages) {
this.messages = messages;
}
public void execute() {
String message = this.messages.getMessage("argument.required",
new Object [] {"userDao"}, "Required", null);
System.out.println(message);
}
}
调用execute()方法的输出结果为:
The userDao argument is required.
关于国际化(i18n),Spring的各种MessageSource实现遵循与标准JDK ResourceBundle相同的语言环境和回退规则。简而言之,并继续用前面messageSource 为例,如果你想根据英国(en-GB)解析消息,你可以创建这些文件format_en_GB.properties,exceptions_en_GB.properties, and windows_en_GB.properties。
通常,地域设置通过应用周围环境管理的。在此示例中,手动指定对(英国)区域消息进行解析。
# in exceptions_en_GB.properties
argument.required=Ebagum lad, the {0} argument is required, I say, required.
public static void main(final String[] args) {
MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
String message = resources.getMessage("argument.required",
new Object [] {"userDao"}, "Required", Locale.UK);
System.out.println(message);
}
上面的程序运行输出为:
Ebagum lad, the ‘userDao’ argument is required, I say, required.
你也可以使用MessageSourceAware接口来获取对已定义MessageSource的引用。任何在ApplicationContext定义的bean都会实现MessageSourceAware,当bean被创建或者配置的时候,它会在应用上下文的MessageSource中被被注入。
作为ResourceBundleMessageSource的替代方法,Spring提供了一个ReloadableResourceBundleMessageSource类。这个变体支持同样打包文件格式,但是它更灵活而不是标准JDK基于ResourceBundleMessageSource的实现。特别的,它允许从任何Spring 资源位置读取文件(不仅仅是从类路径)而且还支持属性文件热加载(同时高效缓存他们)。ReloadableResourceBundleMessageSource的详细信息参考javadocs。
3.15.2 标准和自定义事件 {#toc_19}
ApplicationEvent类和ApplicationListener接口提供了ApplicationContext中的事件处理。如果一个bean实现了ApplicationListener接口,然后它被部署到上下问中,那么每次ApplicationEvent发布到ApplicationContext中时,bean都会收到通知。本质上,这是观察者模型。
从Spring 4.2开始,事件的基础得到了重要的提升,并提供了基于注解模型及任意事件发布的能力,这个对象不一定非要继承ApplicationEvent。当这个对象被发布时,我们把他包装在事件中。
Spring提供了一下的标准事件:
表3.7 内置事件
事件 | 解释 |
---|---|
ContextRefreshedEvent | 当ApplicationContext被初始化或者被刷新的时候发布,例如,在ConfigurableApplicationContext接口上调用refresh()方法。”初始化”在这里意味着所有的bean被加载,后置处理器被检测到并且被激活,单例的预加载,以及ApplicationContext对象可以使用。只要上下文还没有被关闭,refresh就可以被触发多次,前提所选的ApplicationContext支持热刷新。例如,XmlWebApplicationContext支持热刷新,而GenericApplicationContext不支持。 |
ContextStartedEvent | 当ApplicationContext启动时发布,在ConfigurableApplicationContext接口上调用start()方法。”已启动”意味着所有bean的生命周期会接受到一个明确的启动信号。通常这个信号用来停止后的重启,但是他也可以被用来启动没有配置为自动启动的组件,例如,在初始化时还没启动的组件。 |
ContextStoppedEvent | 当ApplicationContext 停止时发布,在ConfigurableApplicationContext接口上调用stop()方法。”停止”意味这所有的bean的生命周期都会受到一个明确的停止信号。通过调用start()方法可以重启一个已经停止的上下文。 |
ContextClosedEvent | 当ApplicationContext 关闭时发布,在ConfigurableApplicationContext接口上调用close()方法。”关闭”意味着所有的单例bean都会被销毁。关闭的上下文就是它生命周期的末尾。它不能刷新或者重启。 |
RequestHandledEvent | 接受一个HTTP请求的时候,一个特定的web时间会通知所有的bean。这个时间的发布是在请求完成。此事件仅适用于使用Spring的DispatcherServlet的Web应用程序。 |
你可以创建并发布自己的自定义事件。这个例子演示了一个继承Spring ApplicationEvent的简单类:
public class BlackListEvent extends ApplicationEvent {
private final String address;
private final String test;
public BlackListEvent(Object source, String address, String test) {
super(source);
this.address = address;
this.test = test;
}
// accessor and other methods...
}
为了发布一个自定义的ApplicationEvent,在ApplicationEventPublisher中调用publishEvent()方法。通常在实现了ApplicationEventPublisherAware接口并把它注册为一个Spring bean的时候它就完成了。下面的例子展示了这么一个类:
public class EmailService implements ApplicationEventPublisherAware {
private List<String> blackList;
private ApplicationEventPublisher publisher;
public void setBlackList(List<String> blackList) {
this.blackList = blackList;
}
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void sendEmail(String address, String text) {
if (blackList.contains(address)) {
BlackListEvent event = new BlackListEvent(this, address, text);
publisher.publishEvent(event);
return;
}
// send email...
}
}
在配置时,Spring容器将检测到EmailService实现了ApplicationEventPublisherAware,并将自动调用setApplicationEventPublisher()方法。实际上,传入的参数将是Spring容器本身;您只需通过ApplicationEventPublisher接口与应用程序上下文进行交互。
为了自定义ApplicationEvent,创建一个试下了ApplicationListener的类并把他注册为一个Spring bean。下面例子展示这样一个类:
public class BlackListNotifier implements ApplicationListener<BlackListEvent> {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
public void onApplicationEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
请注意,ApplicationListener通常用你自定义的事件BlackListEvent类型参数化的。这意味着onApplicationEvent()方法可以保持类型安全,避免向下转型的需要。您可以根据需要注册许多的事件侦听器,但请注意,默认情况下,事件侦听器将同步接收事件。这意味着publishEvent()方法会阻塞直到所有的监听者都处理完。这种同步和单线程方法的一个优点是,如果事务上下文可用,它就会在发布者的事务上下文中处理。如果必须需要其他的时间发布策略,请参考javadoc的 Spring ApplicationEventMulticaster 接口。
下面例子展示了使用配置和注册上述每个类的bean定义:
<bean id="emailService" class="example.EmailService">
<property name="blackList">
<list>
<value>known.spammer@example.org</value>
<value>known.hacker@example.org</value>
<value>john.doe@example.org</value>
</list>
</property>
</bean>
<bean id="blackListNotifier" class="example.BlackListNotifier">
<property name="notificationAddress" value="blacklist@example.org"/>
</bean>
把他们放在一起,当调用emailService的sendEmail()方法时,如果有任何应该被列入黑名单的邮件,那么自定义的BlackListEvent事件会被发布。blackListNotifier 会被注册为一个ApplicationListener,从而接受BlackListEvent,届时通知适当的参与者。
Spring 的事件机制的设计是用在Spring bean和相同应用上下文的简单通讯。然而,对于更复杂的企业集成需求,单独维护Spring Integration工程对构建著名的Spring编程模型轻量级,面向模式,事件驱动架构提供了完整的支持。
基于注解的事件监听器
从Spring 4.2开始,一个事件监听器可以通过EventListener注解注册在任何managed bean的公共方法上。BlackListNotifier可以重写如下:
public class BlackListNotifier {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
@EventListener
public void processBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
如上所示,方法签名实际上会推断出它监听的是哪一个类型的事件。这也适用于泛型嵌套,只要你在过滤的时候可以根据泛型参数解析出实际的事件。
如果你的方法需要监听好几个事件或根本没有参数定义它,事件类型也可以用注解本身指明:
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
}
对特殊的时间调用方法,根据定义的SpEL表达式来匹配实际情况,通过条件属性注解,
也可以通过condition注解来添加额外的运行过滤,它对一个特殊事件的方法实际调用是根据它是否匹配condition注解所定义的SpEL表达式。
例如,只要事件的测试属性等于foo,notifier可以被重写为只被调用:
@EventListener(condition = "#blEvent.test == 'foo'")
public void processBlackListEvent(BlackListEvent blEvent) {
// notify appropriate parties via notificationAddress...
}
每个SpEL表达式在此评估专用的上下文。下表列出的条目存在上下文中可用,所以可以调用他们处理conditional事件:
表 3.8. 存在元数据中的Event SpEL 表达式
名字 | 位置 | 描述 | 例子 |
---|---|---|---|
事件 | 根路径 | 实际的ApplicationEvent | #root.event |
参数数组 | 根路径 | 参数(数组) 目标调用 | #root.args[0] |
参数名字 | 上下文 | 任何的方法参数名称。如果由于某些名称的原因而不可用(例如:没有调试信息),参数名称也会存在#a<#arg>, #arg代表参数索引开始的地方(从0开始) | #blEvent 或 #a0(也可以使用#p0 or #p<#arg> 作为别名) |
注意,#root.event允许你访问底层的时间,即使你的方法签名实际上是指已发布的任意对象。
如果您需要发布一个事件作为处理另一个事件的结果,只需更改方法签名来返回应该被发布的事件,如下所示:
@EventListener
public ListUpdateEvent handleBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress and
// then publish a ListUpdateEvent...
}
异步监听器不支持这个特性
这个新方法将对上述方法处理的每个BlackListEvent都会发布一个新的ListUpdateEvent。如果需要发布多个时间,只需要返回事件集合即可。
异步监听器
如果你希望一个特定的监听器去异步处理事件,只需要重新使用常规的@Async支持:
@EventListener
@Async
public void processBlackListEvent(BlackListEvent event) {
// BlackListEvent is processed in a separate thread
}
当使用异步事件的时候有下面两个限制:
- 如果事件监听器抛出异常,则不会将其传播给调用者,查看AsyncUncaughtExceptionHandler获取详细信息。
- 此类事件监听器无法发送回复。如果你需要将处理结果发送给另一个时间,注入ApplicationEventPublisher里面手动发送事件。
顺序的监听器
如果你需要一个监听器在另一个监听器调用前被调用,只需要在方法声明上添加@Order注解:
@EventListener
@Order(42)
public void processBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
泛型事件
你可以使用泛型来进一步的定义事件的结构。考虑EntityCreatedEvent,T的类型就是你要创建的真实类型。你可以创建下面的监听器定义,它只接受Person类型的EntityCreatedEvent:
@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
...
}
触发了事件解析泛型参数,
由于类型擦除,只有在触发了事件解析事件监听过器滤的泛型参数(类似于PersonCreatedEvent继承了EntityCreatedEvent { … }),此操作才会起作用。
在某些情况下,如果所有的时间都遵循相同的结果(上述事件应该是这样),这可能有点冗余。在这种情况下,你可以实现ResolvableTypeProvider来引导超出框架运行是环境提供的范围:
public class EntityCreatedEvent<T>
extends ApplicationEvent implements ResolvableTypeProvider {
public EntityCreatedEvent(T entity) {
super(entity);
}
@Override
public ResolvableType getResolvableType() {
return ResolvableType.forClassWithGenerics(getClass(),
ResolvableType.forInstance(getSource()));
}
}
这不仅适用于ApplicationEvent,还可以作为时间发送的任意对象。
3.15.3 便捷访问低优先级的资源 {#toc_20}
为了最佳使用和理解上下文,用户应该熟悉Spring资源抽象,如章节:章节4,资源。
一个应用上下文就是一个ResourceLoader,它可以用来载入资源。一个资源本质上讲就是JDK类java.net.URL功能更丰富的版本。实际上,Resource的实现在合适的地方包装了一个java.net.URL实例。资源可以以透明的方式从任何位置获得获取低优先级的资源,包括一个标准的URL,本地文件系统,任何描述标准URL的地方,其他的一些扩展。如果资源位置的字符串是一个没有任何特殊字符前缀的简单路径,那么这些资源就来自特定的并适合实际应用程序上下文类型。
你可以将bean的配置部署到一个实现了应用上下文的特殊回调接口ResourceLoaderAware中,以便在初始化的时候应用上下文把自己作为ResourceLoader传递进去可以自动调用。你也可以暴露资源的属性类型,用于访问静态资源;它们像其他属性一样会被注入。你可以像字符串路径一样指定这些资源的属性,并依赖自动注入上下文的特殊JavaBean的属性编辑器,以便在部署bean时将这些文本字符串转换为真实的对象。
提供给ApplicationContext 构造器的位置路径或路径都是真实的资源字符串,并以简单的形式对特定的上下文进行了适当的处理。ClassPathXmlApplicationContext将简单的路径作为类路径的位置。你也可以使用特殊前缀的位置路径(资源字符串)强制从类路径或者URL加载定义信息,而不管实际的上下文类型。
3.15.4 便捷的ApplicationContext 实例化web 应用程序 {#toc_21}
你可以通过声明式创建ApplicationContext的实例,例如,ContextLoader。当然你也可以通过使用一个ApplicationContext的实现用编程的方式创建ApplicationContext的实例。
你可以像下面一样通过ContextLoaderListener来注册一个ApplicationContext:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
监听器会检查contextConfigLocation的参数。如果参数不存在,监听器默认会使用/WEB-INF/applicationContext.xml。当参数存在的是,监听器会通过预定义的分隔符来(逗号,分号和空格)来分隔字符串,并将其作为应用程序上下文的搜索位置。它也支持Ant风格的路径模式。例如:/WEB-INF/*Context.xml,在WEB-INF目录下,/WEB-INF/**/*Context.xml,WEB-INF子目录的所有这样类似的文件都会被发现。
3.15.5 Spring ApplicationContext 作为Java EE RAR文件部署 {#toc_22}
可以将Spring ApplicationContext部署为RAR文件,将上下文及其所有必需的bean类和库JAR封装在Java EE RAR部署单元中。这相当于引导一个独立的ApplicationContext,只是托管在Java EE环境中,能够访问Java EE服务器设施。 在部署无头WAR文件(实际上,没有任何HTTP入口点,仅用于在Java EE环境中引导Spring ApplicationContext的WAR文件)的情况下RAR部署是更自然的替代方案。
RAR部署非常适合不需要HTTP入口点但仅由消息端点和调度作业组成的应用程序上下文。在这种情况下,Bean可以使用应用程序服务器资源,例如JTA事务管理器和JNDI绑定的JDBC DataSources和JMS ConnectionFactory实例,并且还可以通过Spring的标准事务管理和JNDI和JMX支持设施向平台的JMX服务器注册。应用程序组件还可以通过Spring的TaskExecutor抽象实现与应用程序服务器的JCA WorkManager交互。
通过查看 SpringContextResourceAdapter类的JavaDoc,可以知道用于RAR部署中涉及的配置详细信息。
对于Spring ApplicationContext作为Java EE RAR文件的简单部署:将所有应用程序类打包到RAR文件中,这是具有不同文件扩展名的标准JAR文件。将所有必需的库JAR添加到RAR归档的根目录中。添加一个“META-INF / ra.xml”部署描述符(如SpringContextResourceAdapter的JavaDoc中所示)和相应的Spring XML bean定义文件(通常为“META-INF / applicationContext.xml”),导致RAR文件进入应用程序服务器的部署目录。
[Note]
这种RAR部署单元通常是独立的; 它们不会将组件暴露给外界,甚至不会暴露给同一应用程序的其他模块。 与基于RAR的ApplicationContext的交互通常通过发生在与其他模块共享的JMS目标的情况下。 基于RAR的ApplicationContext还会在其他情况下使用,例如调度一些作业,对文件系统中的新文件(等等)作出反应。 如果需要允许从外部同步访问,它可以做到如导出RMI端点,然后很自然的可以由同一机器上的其他应用模块使用
可以将Spring ApplicationContext部署为RAR文件,将上下文和所有他所需的bean的类和JAR库封装在Java EE RAR部署单元中。这相当于独立启动一个ApplicationContext,它在Java EE环境中可以访问Java EE服务资源。RAR部署在一些没用头信息的war文件中更自然的选择,实际上,一个war文件在没有http入口的时候,那么它就仅仅是用来在Java EE环境中启动Spring ApplicationContext。
对于不需要HTTP入口点的应用上下文来说RAR部署是一种理想的方式,而不仅是一些消息端点和计划的任务。Bean在这样的上下文中可以使用应用服务器的资源,例如:JTA事务管理器、JNDI-bound JDBC DataSources 和JMS连接工厂实例,也可以通过Spring标准事务管理器、JNDI和JMX支持来注册平台的JMX服务。应用组件也可以通过Spring 抽象TaskExecutor来和应用服务器的JCA WorkManager来进行交互。
有关RAR部署中涉及的配置详细信息请查看 JavaDoc中的SpringContextResourceAdapter。
Spring ApplicationContext 作为Java EE RAR文件的简单部署:将所有的应用类打包进一个RAR文件,它和标准的JAR文件有不同的文件扩展名。将所有需要的库JAR文件添加到RAR归档的根目录中。添加一个”META-INF/ra.xml”部署描述文件(如SpringContextResourceAdapters JavaDoc所示),并更改Spring XML中bean的定义文件(通常为“META-INF / applicationContext.xml”),将生成的rar文件放到音符服务器的部署目录。
这种RAR部署单元通常是独立的;它们不会将组建暴露给外界,甚至是同一个应用的其他模块。同基于RAR的ApplicationContext交互通常是通过模块共享的JMS来实现的。一个基于RAR的应用上下文,例如:某些调度任务,文件系统对新文件产生的响应等。如果要允许外界的同步访问,则可以导出RMI端点,这当然可能是同一台机器上的其他应用模块。