3.15 ApplicationContext的附加功能
正如在介绍一章中所讨论的,org.springframework.beans.factory包提供了管理和操作bean的基本功能,包括编程方式。org.springframework.context包添加了ApplicationContext接口,它扩展了BeanFactory接口,此外还扩展了其他接口,以以更面向应用程序框架的风格提供附加功能。许多人以完全声明式的方式使用ApplicationContext,甚至不以编程方式创建它,而是依赖于支持类(如ContextLoader)自动实例化ApplicationContext,将其作为Java EE web应用程序正常启动过程的一部分。
为了以更面向框架的方式增强BeanFactory功能,上下文包还提供了以下功能:
- 通过MessageSource接口访问i18n样式的消息。
- 通过ResourceLoader接口访问资源,例如url和文件。
- 事件发布,即通过使用ApplicationEventPublisher接口实现ApplicationListener接口的bean。
- 加载多个(层次结构)上下文,允许每个上下文通过HierarchicalBeanFactory接口集中于一个特定的层,例如应用程序的web层。
3.15.1 使用MessageSource国际化
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的父bean。如果是,则使用该bean作为消息源。如果ApplicationContext找不到消息的任何源,则实例化一个空的DelegatingMessageSource,以便能够接受对上面定义的方法的调用。
Spring提供了两种MessageSource实现,ResourceBundleMessageSource和StaticMessageSource。它们都实现了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。任何解析消息的请求都将以JDK通过ResourceBundles解析消息的标准方式处理。对于本例,假设上面两个资源包文件的内容是……
# 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和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和windows_en_GBproperties。
通常,区域设置解析由应用程序的周围环境管理。在本例中,将手动指定解析(英国)消息的区域设置。
# 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的引用。在创建和配置bean时,在实现MessageSourceAware接口的ApplicationContext中定义的任何bean都将被应用程序上下文的MessageSource注入。
注意:作为ResourceBundleMessageSource的替代品,Spring提供了ReloadableResourceBundleMessageSource类。此变体支持相同的包文件格式,但比基于JDK的标准ResourceBundleMessageSource实现更加灵活。特别是,它允许从任何Spring资源位置读取文件(不只是从类路径),并支持热重载bundle属性文件(同时有效地在两者之间缓存它们)。有关详细信息,请查看ReloadableResourceBundleMessageSource javadocs。
3.15.2 标准和自定义事件
ApplicationContext中的事件处理是通过ApplicationEvent类和ApplicationListener接口提供的。如果将实现ApplicationListener接口的bean部署到上下文中,每当将ApplicationEvent发布到ApplicationContext时,就会通知该bean。本质上,这是标准的观察者设计模式。
注意:从Spring 4.2开始,事件基础设施得到了显著的改进,并提供了基于注解的模型,以及发布任意事件的能力,即不一定要扩展自ApplicationEvent的对象。当这样一个对象发布时,我们将它封装在一个事件中。
Spring提供了以下标准事件:
表3.7 内置的事件
事件 | 解释 |
---|---|
ContextRefreshedEvent | 在初始化或刷新ApplicationContext时发布,例如,使用ConfigurableApplicationContext接口上的refresh()方法。这里的“初始化”意味着加载所有bean,检测并激活后处理器bean,预实例化单例,并准备使用ApplicationContext对象。只要上下文没有关闭,就可以多次触发刷新,前提是所选的ApplicationContext实际上支持这种“热”刷新。例如,XmlWebApplicationContext支持热刷新,但是GenericApplicationContext不支持。 |
ContextStartedEvent | 在启动ApplicationContext时,使用ConfigurableApplicationContext接口上的start()方法发布。这里的“Started”意味着所有生命周期bean都接收一个显式的start信号。通常,此信号用于在显式停止之后重新启动bean,但也可以用于启动未为autostart配置的组件,例如,在初始化时尚未启动的组件。 |
ContextStoppedEvent | 在停止ApplicationContext时,使用ConfigurableApplicationContext接口上的stop()方法发布。这里的“停止”意味着所有生命周期bean都接收一个显式的停止信号。可以通过start()调用重新启动已停止的上下文。 |
ContextClosedEvent | 在关闭ApplicationContext时,使用ConfigurableApplicationContext接口上的close()方法发布。这里的“关闭”意味着销毁所有单例bean。封闭的环境到了生命的尽头;无法刷新或重新启动。 |
RequestHandledEvent | 一个特定于web的事件,通知所有bean HTTP请求已得到服务。此事件在请求完成后发布。此事件仅适用于使用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 bean的sendEmail()方法时,如果有任何电子邮件应该被列入黑名单,则会发布BlackListEvent类型的自定义事件。blackListNotifier bean注册为ApplicationListener,从而接收BlackListEvent,此时它可以通知适当的方。
注意:Spring的事件机制是为同一应用程序上下文中Spring bean之间的简单通信而设计的。然而,对于更复杂的企业集成需求,独立维护的Spring integration项目提供了对构建轻量级、面向模式、事件驱动的体系结构的完整支持,这些体系结构构建在众所周知的Spring编程模型之上。
基于注解的事件监听器
从Spring 4.2开始,事件监听器可以通过EventListener注解注册到托管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() {
}
还可以通过注解的condition属性添加额外的运行时筛选,该注解定义了一个SpEL表达式,该表达式应该与实际调用特定事件的方法相匹配。
例如,我们的通知程序可以重写为只有当事件的test属性等于foo时才会被调用:
@EventListener(condition = "#blEvent.test == 'foo'")
public void processBlackListEvent(BlackListEvent blEvent) {
// notify appropriate parties via notificationAddress...
}
每个SpEL表达式再次计算一个专用上下文。下表列出了对上下文可用的项,以便可以将它们用于条件事件处理:
表3.8 事件SpEL可用元数据
名字 | 位置 | 描述 | 实例 |
---|---|---|---|
Event | root object | 实际ApplicationEvent | #root.event |
Arguments array | root object | 用于调用目标的参数(作为数组) | #root.args[0] |
Argument name | evaluation context | 任何方法参数的名称。如果由于某种原因名称不可用(例如没有调试信息),参数名称也可以在#a<#arg>下使用,其中#arg代表参数索引(从0开始)。 | #blEvent or #a0(one can also use#p0 or #p<#arg>notation as an alias). |
请注意,#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>,其中T是创建的实际实体的类型。你可以创建以下侦听器定义,以仅接收一个人的EntityCreatedEvent:
@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
...
}
由于类型擦除,只有当触发的事件解析事件侦听器筛选的泛型参数(类似于class PersonCreatedEvent extends EntityCreatedEvent< Person> { … })时,这才会工作。
在某些情况下,如果所有事件都遵循相同的结构(对于上面的事件应该是这样),这可能会变得相当乏味。在这种情况下,你可以实现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 方便地访问底层资源
为了最佳地使用和理解应用程序上下文,用户通常应该熟悉Spring的Resource 抽象,如第4章“资源”中所述。
应用程序上下文是ResourceLoader,可用于加载资源。资源本质上是JDK类java.net.URL的功能更丰富的版本,实际上,Resource的实现在适当的地方封装了java.net.URL的实例。Resource可以以透明的方式从几乎任何位置获得底层资源,包括类路径、文件系统位置、任何可以用标准URL描述的位置,以及其他一些变体。如果资源位置字符串是一个没有任何特殊前缀的简单路径,那么这些资源的来源是特定的,并且适合于实际的应用程序上下文类型。
你可以配置部署到应用程序上下文中的bean,以实现特殊的回调接口ResourceLoaderAware,以便在初始化时自动回调,并将应用程序上下文本身作为ResourceLoader传递进来。还可以公开Resource类型的属性,用于访问静态资源;它们会像其他属性一样被注入其中。你可以将这些资源属性指定为简单的字符串路径,并依赖于由上下文自动注册的特殊JavaBean PropertyEditor,以便在部署bean时将这些文本字符串转换为实际的资源对象。
提供给ApplicationContext构造函数的位置路径实际上是资源字符串,在简单的形式中,它们被适当地处理为特定上下文实现。ClassPathXmlApplicationContext将简单的位置路径视为类路径位置。你还可以使用带有特殊前缀的位置路径(资源字符串)来强制从类路径或URL加载定义,而不管实际的上下文类型如何。
3.15.4 方便的web应用程序的ApplicationContext实例化
你可以通过使用ContextLoader(例如)声明式地创建ApplicationContext实例。当然,你也可以通过使用ApplicationContext实现之一以编程方式创建ApplicationContext实例。
你可以使用ContextLoaderListener注册一个应用程序上下文,如下所示:
<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,用于所有名称以“Context”结尾的文件。,位于“WEB-INF”目录和/WEB-INF/**/*Context.xml,用于“WEB-INF”任何子目录中的所有此类文件。
3.15.5 将Spring ApplicationContext部署为Java EE RAR文件
可以将Spring ApplicationContext部署为RAR文件,将上下文及其所需的所有bean类和库jar封装在Java EE RAR部署单元中。这相当于引导一个独立的ApplicationContext(仅托管在Java EE环境中),从而能够访问Java EE服务器设施。RAR部署比部署无头WAR文件的场景更自然,实际上,没有任何HTTP入口点的WAR文件只用于在Java EE环境中引导Spring ApplicationContext。
RAR部署非常适合不需要HTTP入口点,而只由消息端点和计划的作业组成的应用程序上下文。在这种上下文中,bean可以使用应用服务器资源,比如JTA事务管理器和JNDI绑定的JDBC数据源以及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文件放入应用服务器的部署目录中。
注意:这种RAR部署单元通常是自包含的;它们不向外部公开组件,甚至不向同一应用程序的其他模块公开组件。与基于rar的ApplicationContext的交互通常通过与其他模块共享的JMS目的地进行。例如,基于rar的ApplicationContext还可以调度一些作业,以响应文件系统中的新文件(或类似的东西)。如果需要允许从外部进行同步访问,则可以导出RMI端点,当然,同一台机器上的其他应用程序模块也可以使用这些端点。