ApplicationContext
应用级功能
org.springframework.beans.factory
包提供了管理和使用bean的基本功能, org.springframework.context
包下添加了继承BeanFactory
的ApplicationContext
接口,其提供了额外的应用级功能
ApplicationContext
提供了如下功能:
- 通过
MessageSource
接口进行国际化 - 通过
ResourceLoader
接口获取URLs或者文件等资源 - 事件发布,通过
ApplicationEventPublisher
通知实现ApplicationListener
接口的bean - 加载多个上下文环境(继承关系),通过
HierarchicalBeanFactory
使得每一个context能专注于其功能项,比如web应用的前端控制器层。
使用 MessageSource
进行国际化
ApplicationContext
接口继承了MessageSource
接口,因此ApplicationContext
能够提供国际化的功能,Spring也提供HierarchicalMessageSource
来支持分层处理国际化,接口定义的方法包括:
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
,如果找到MessageSource
bean,所有相关的方法都会被代理到这个bean实现,如果未找到,ApplicationContext
会查询其父容器中的messageSource
bean,如果未找到任何messageSource
bean,将会实例化DelegatingMessageSource
进行使用。
spring提供了2个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>
以上表示类路径下有3个资源集合: format
, exceptions
, windows
,任何请求都将通过ResourceBundle
– 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", Locale.ENGLISH);
System.out.println(message);
}
结果:
Alligators rock!
解析:上述MessageSource
定义在beans.xml中,messageSource
bean通过basenames
指定国际化资源文件。上述指定了3个文件: format.properties
, exceptions.properties
, windows.properties
MessageSource
获取使用参数:
<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.something.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", Locale.ENGLISH);
System.out.println(message);
}
}
结果如下:
The userDao argument is required.
如果想要进行英国的国际化(en-GB
),需要创建 format_en_GB.properties
, exceptions_en_GB.properties
, 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
中任何实现MessageSourceAware
接口的对象在创建和被配置的时候都将被注入MessageSource
对象。
tips:ResourceBundleMessageSource
的一个可选实现是ReloadableResourceBundleMessageSource
,ReloadableResourceBundleMessageSource
比基于JDK的ResourceBundleMessageSource
更加灵活,尤其是支持从任何spring资源加载位置读取(不仅仅是类路径)以及支持热加载, 详情:ReloadableResourceBundleMessageSource
标准和自定义事件
ApplicationContext
通过ApplicationEvent
和ApplicationListener
提供事件处理机制,如果容器中一个bean实现了ApplicationListener
接口,每当有ApplicationEvent
发布到ApplicationContext
中就会通知此bean,本质上这就是观察者模式的实现。
Spring 4.2之后,事件机制得到增强,Spring提供了注解开发模式以及事件不再必须实现ApplicationEvent
接口,当发布事件时,Spring自动将其封装为ApplicationEvent
。
Spring提供的标准事件:
Event | 解释 |
---|---|
ContextRefreshedEvent | 在初始化或刷新ApplicationContext 时发布(例如,通过使用ConfigurableApplicationContext 接口上的refresh() 方法)。在这里,“已初始化”是指所有Bean 均已加载,检测到并激活了后处理器Bean ,已预先实例化单例并且可以使用ApplicationContext 对象。只要尚未关闭上下文,只要所选的ApplicationContext 实际上支持这种“热”刷新,就可以多次触发刷新。例如,XmlWebApplicationContext 支持热刷新,但GenericApplicationContext 不支持。 |
ContextStartedEvent | 使用ConfigurableApplicationContext 接口上的start() 方法启动ApplicationContext 时发布。在这里,“已启动”是指所有Lifecycle bean都收到一个明确的启动信号。通常,此信号用于在显式停止后重新启动Bean,但也可以用于启动尚未配置为自动启动的组件(例如,尚未在初始化时启动的组件)。 |
ContextStoppedEvent | 通过使用ConfigurableApplicationContext 接口上的stop() 方法停止ApplicationContext 时发布。此处,“已停止”表示所有Lifecycle bean均收到明确的停止信号。停止的上下文可以通过start() 调用重新启动。 |
ContextClosedEvent | 通过使用ConfigurableApplicationContext 接口上的close() 方法或通过JVM关闭钩子关闭ApplicationContext 时发布。在这里,“封闭”意味着所有单例Bean将被销毁。关闭上下文后,它将达到使用寿命,无法刷新或重新启动。 |
RequestHandledEvent | 一个特定于Web的事件,告诉所有Bean HTTP请求已得到服务。请求完成后,将发布此事件。此事件仅适用于使用Spring的DispatcherServlet 的Web应用程序。 |
ServletRequestHandledEvent | RequestHandledEvent 的子类,添加了特定于Servlet 的上下文信息。 |
自定义事件案例:
public class BlockedListEvent extends ApplicationEvent {
private final String address;
private final String content;
public BlockedListEvent(Object source, String address, String content) {
super(source);
this.address = address;
this.content = content;
}
// accessor and other methods...
}
发布事件需要调用ApplicationEventPublisher
的 publishEvent()
方法,通常的做法是实现ApplicationEventPublisherAware
接口,并将其注册为spring bean:
public class EmailService implements ApplicationEventPublisherAware {
private List<String> blockedList;
private ApplicationEventPublisher publisher;
public void setBlockedList(List<String> blockedList) {
this.blockedList = blockedList;
}
// 重写 setApplicationEventPublisher方法获取ApplicationEventPublisher
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void sendEmail(String address, String content) {
if (blockedList.contains(address)) {
publisher.publishEvent(new BlockedListEvent(this, address, content));
return;
}
// send email...
}
}
在Spring容器的配置期间,检测到EmailService
实现了ApplicationEventPublisherAware
接口,会自动调用setApplicationEventPublisher()
方法,Spring会自动填充其参数,所以我们可以通过ApplicationEventPublisher
与Spring容器交互。
为了能监听ApplicationEvent
事件,需要实现ApplicationListener
并将其注册为Spring bean:
public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
public void onApplicationEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
ApplicationListener
带有范型,意味着onApplicationEvent()
方法不需要我们进行强转,我们可以注册许多监听器,但是默认情况下监听器是同步接受事件的,也就是说 publishEvent()
在直到所有监听器被执行完之前是一只阻塞的,好处就是能在同一线程中执行,并且如果存在事务环境,也能保持一致。如果需要使用其他策略,考虑 ApplicationEventMulticaster
以及实现类 SimpleApplicationEventMulticaster
注册以上的bean进行使用:
<bean id="emailService" class="example.EmailService">
<property name="blockedList">
<list>
<value>known.spammer@example.org</value>
<value>known.hacker@example.org</value>
<value>john.doe@example.org</value>
</list>
</property>
</bean>
<bean id="blockedListNotifier" class="example.BlockedListNotifier">
<property name="notificationAddress" value="blockedlist@example.org"/>
</bean>
当emailService
的 sendEmail()
被调用时,如果有黑名单中的邮件,则会发布BlockedListEvent
,blockedListNotifier
就会被通知
spring的事件机制的设计是被用来处理同一个
ApllicationContext
中的Bean进行交互的,如果需要更加复杂的交互能力需要使用Spring Integration ,能够构建轻量级、基于事件驱动的应用
基于注解的事件监听
Spring 4.2之后能够在任何Bean中的public方法上加上@EventListener
注解完成事件监听,blockedListNotifier
重新实现:
public class BlockedListNotifier {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
@EventListener
public void processBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
如果方法需要监听多个事件,并且想使用无参方法,也可以将事件声明到注解中:
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
// ...
}
还能够使用注解的condition
属性进行事件过滤,并且是使用 SpEL
expression (Spring表达式)条件配置:
@EventListener(condition = "#blEvent.content == 'my-event'") // 只有当BlockedListEvent事件中的content属性为my-event时才进行调用
public void processBlockedListEvent(BlockedListEvent blockedListEvent) {
// notify appropriate parties via notificationAddress...
}
事件上下文环境可用的元数据信息:
Name | Location | Description | Example |
---|---|---|---|
Event | root object | 实际的 ApplicationEvent . | #root.event 或者 event |
Arguments array | root object | 用于调用方法的参数(作为对象数组) | #root.args 或者 args ;使用 args[0] 获取第一个参数 |
Argument name | evaluation context | 任何方法参数的名称。如果由于某种原因这些名称不可用(例如,由于在编译的字节码中没有调试信息),则也可以使用#a <#arg> 语法(其中<#arg> 代表参数索引(从0开始)。 | #blEvent 或者 #a0 (也可以使用 #p0 或者 #p<#arg> 表示法作为别名) |
#root.event
能获取到底层的事件,即使监听方法签名参数接收的是自定义事件。
在处理完一个事件后,如果需要继续发布事件,只需要修改方法返回值即可:
@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress and
// then publish a ListUpdateEvent...
}
但是此用法不支持异步调用监听器
这个方法在美处理完一个BlockedListEvent
事件后发布ListUpdateEvent
事件,如果需要发布多个事件可以返回事件的Collection
。
异步监听器
只需在监听方法上加 @Async
注解就能 完成异步监听:
@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
// BlockedListEvent is processed in a separate thread
}
使用异步监听器的局限性:
- 如果异步监听器抛出异常,并不会被传递到调用者,详情参考
AsyncUncaughtExceptionHandler
- 异步监听器方法不支持上述发布连续的事件,如果需要进行发布连续事件,需要注入
ApplicationEventPublisher
进行手动发布
监听器的执行顺序
添加@Order
注解进行顺序编排:
@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
通用事件
在某些情况下,如果所有事件都遵循相同的结构,这可能会变得很通用性(就像前面示例中的事件一样)。 在这种情况下,可以实现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()));
}
}
使用 EntityCreatedEvent<T>
构造通用的事件,其中T
是实际的事件实体,比如:只接收EntityCreatedEvent
事件的监听器:
@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
// ...
}
这不光对
ApplicationEvent
有效,而且可以是任意事件对象
便捷的资源获取方式
Spring将应用资源抽象为Resource
接口以及通过ResourceLoader
接口进行加载得到。并且ApplicationContext
实现了ResourceLoader
接口,Resource
抽象相当于是增强版的 JDK java.net.URL
类,实际上,Resource
的实现包装了java.net.URL
的实例,Resource
获取几乎任意位置的资源,包括类路径,文件系统,以及使用标准URL描述的资源。
可以通过实现ResourceLoaderAware
接口,并注入到容器中,在初始化时期就能被回掉,获取到ResourceLoader
实例(通常就是ApplicationContext
)
ApplicationContext
的构造器通过接收资源的地址字符串,其需要依赖具体的上下文环境进行加载,比如,ClassPathXmlApplicationContext
将其识别为类路径下,也可以使用特定的前缀进行指定,强制其加载为类路径或URL,而不管是什么具体的上下环境。