org.springframework.context包提供了更多的功能:
- 通过
MessageSource
接口实现国际化。 - 通过
ResourceLoader
接口存取像URLs、文件之类的资源。 - 通过
ApplicationEventPublisher
接口发布事件给实现ApplicationListener接口的bean。 - 通过
HierarchicalBeanFactory
接口读取多个具有层级关系的context。
使用MessageSource进行国际化:
ApplicationContext接口extends了一个名叫MessageSource的接口,以提供国际化支持。
Spring还提供了HierarchicalMessageSource接口用于解析层次的message。
这些接口内的部分方法:
String getMessage(String code, Object[] args, String default, Locale loc);
String getMessage(String code, Object[] args, Locale loc);
String getMessage(MessageSourceResolvable resolvable, Locale locale);
当ApplicationContext被装载时,它会自动搜索一个名为messageSource的MessageSource类型bean。
如果找不到,ApplicationContext就会在父级容器里寻找,如果还是找不到就生成一个空的DelegatingMessageSource实例,用于处理接口提供的方法。
Spring提供三种MessageSource实现:ResourceBundleMessageSource、ReloadableResourceBundleMessageSource和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>
上面的配置假定在classpath上有三个资源包,名字分别是format、exceptions和windows。
解析消息是用一种JDK标准的方式,通过ResourceBundle对象完成。例如:
# in format.properties
message=Alligators rock!
# in exceptions.properties
argument.required=The {0} argument is required.
由于ApplicationContext实现也实现了MessageSource,所以ApplicationContext可以转换成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);
}
例如将参数传递给message:
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.
考虑到国际化,Spring的各种MessageSource实现都和标准JDK的ResourceBundle有相同的解析和回调规则。
比如要使用英国本地化设置(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引用。
注意:Spring的MessageSource基于Java的ResourceBundle,如果basename相同,并不会自动迁移,而是使用第一个发现的bundle,剩下的忽略。
作为ResourceBundleMessageSource的替代,Spring提供了ReloadableResourceBundleMessageSource类,比标准JDK更有拓展性,允许从任意Spring资源目录读取文件,而且支持热更新。
标准/定制事件:
事件处理通过ApplicationEvent类和ApplicationListener接口实现。
如果一个bean实现了ApplicationListener接口而且被部署进容器,那么每次ApplicationEvent被发布到ApplicationContext内,这个bean都会被提醒。标准的观察者模式。
Spring内置事件:
ContextRefreshedEvent | 当ApplicationContext被初始化或者手动调用refresh()方法。 初始化意思是所有的singleten作用域bean都被装载, post-processor都被检测并装载,ApplicationContext可用。 |
ContextStartedEvent | 当ApplicationContext调用了start()方法,started意思是所有Lifecycle bean都收到明确的start信号,通常用于重启bean。 |
ContextStoppedEvent | 当ApplicationContext调用了stop(),stopped意思是所有Lifecycle bean都收到明确的stop信号,可被start()重启。 |
ContextClosedEvent | 当ApplicationContext调用了close()或者通过JVM shutdown hook。 closed意思是所有的singleton作用域bean将会被销毁,而且无法重启。 |
RequestHandledEvent | web专用事件,表示服务了一个HTTP请求,在请求完成后发布。 |
ServletRequestHandledEvent | RequestHandledEvent的子类,添加了Servlet专用上下文信息。 |
继承Spring的ApplicationEvent类可以实现自定义事件:
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;
}
// 其它内容
}
创建一个类,实现ApplicationEventPublisherAware接口,并注册为bean,调用ApplicationEventPublisher的publishEvent()方法发布定制的事件:
public class EmailService implements ApplicationEventPublisherAware {
private List<String> blockedList;
private ApplicationEventPublisher publisher;
public void setBlockedList(List<String> blockedList) {
this.blockedList = blockedList;
}
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...
}
}
在配置时,容器检测到实现ApplicationEventPublisherAware接口,会自动调用setApplicationEventPublisher()方法。在现实中,传入的参数就是容器自己。
要接收自定义事件,需要创建一个类,实现ApplicationListener接口,并注册成bean:
public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
public void onApplicationEvent(BlockedListEvent event) {
// 一顿操作...
}
}
由于ApplicationListener是泛型接口,所以需要传入类型信息,onApplicationEvent方法是类型安全的。
例子在这里。可以想注册几个事件监听器就注册几个。
默认,事件监听器以同步方式接收事件,单线程运行,所以是线程安全的,一个优势是当监听器接收到一个事件,如果事务可用,它就在发布者的transaction context内运行。
Spring的事务机制设计用于简单的容器内bean之间的通信。用于企业级应用可以考虑使用Spring Integration项目。
基于注解的事件监听
将bean定义内的方法注册成一个事件处理器,使用@EventListener注解:
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() {
// ...
}
可以通过@EventListener的condition属性声明SpEL添加一个运行时过滤器:
@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blEvent) {
// notify appropriate parties via notificationAddress...
}
上面例子的意思是当blEvent对象的content属性为my-event时才生效。
SpEL可用的元数据:
名称 | 位置 | 描述 | 例子 |
Event | root对象 | 实际的ApplicationEvent类型 | #root.event or event |
参数数组 | root对象 | 调用方法的参数 | #root.args or args args[0]访问第一个参数,以此类推 |
参数名 | 要被测试的context | 方法参数的名称 | 上面的#blEvent |
注意#root.event可以访问到底层事件,即使方法签名是别的对象的引用。
如果想在方法结束后触发一个事件,只要设置方法返回一个事件类型就行:
@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress and
// then publish a ListUpdateEvent...
}
注意,不支持asynchronous listeners。
可以通过返回Collection或者一个数组来发送多个事件。
异步监听器
添加@Async注解即可:
@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
// BlockedListEvent is processed in a separate thread
}
注意有一些限制:
- 如果异步事件监听器抛出异常,是不会传播给调用者的。
- 不能通过方法返回事件类型发布事件。
给监听器排序:
比如在一个监听器之前调用另外一个监听器,使用@Order注解:
@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
便捷访问底层资源
一个容器就是一个ResourceLoader,可以载入Resource对象。
Resource类型就是功能更丰富的JDK java.net.URL类型。
事实上也是,Resource类内部包装了一个java.net.URL实例。
Resource可以透明地访问几乎任何位置的底层资源,
像classpath、文件系统位置等可以用标准URL或它的变种表述的任何位置。
如果资源位置是一个简单的字符串,没有特殊的前缀,那么资源从哪里来取决于具体的容器类型。
bean可以通过实现ResourceLoaderAware接口,在初始化时将ApplicationContext转换成ResourceLoader并注入bean。可以将Resource属性声明为String,可以自动转换。
传递给ApplicationContext构造器的位置或者路径实际上都是路径字符串,以简单的方式被容器正确处理。
可以在路径字符串内添加特定的前缀来强制从什么地方载入定义,如classpath:。
应用启动过程跟踪
复杂的程序有复杂的启动过程,追踪启动过程可以帮助分析启动时间花在什么地方,
也可以帮助理解分析整个容器的生命周期。
AbstractApplicationContext和其子类有ApplicationStartup实例,收集启动阶段的StartupStep数据:
- 容器生命周期阶段;
- bean生命周期阶段;
- 应用事件处理;
装备在AnnotationConfigApplicationContext内的例子:
// 创建一个启动step并开始记录
StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan");
// 在当前step添加标记
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// 执行装配的实际阶段
this.scanner.scan(basePackages);
// 结束当前step
scanPackages.end();
容器已经装配了多个步骤,当开始记录时,这些步骤都可以被相应的工具收集、显示和分析。
默认的ApplicationStartup实现是一个没有操作的变种,是为了最小的开销。也就是默认不会收集启动步骤。Spring自带了一个实现:FlightRecorderApplicationStartup。要使用这个变种就要在容器创建之后尽快将它配置进容器。
ApplicationStartup仅用于应用启动阶段,并且仅用于核心容器,而不是取代别的java测量库。
要开始收集定制的StartupStep,组件可以实现ApplicationStartupAware接口直接获得ApplicationStartup实例或者在注入点请求ApplicationStartup类型。
注意不能使用”spring.*”作用域,这是Spring保留的,用于Spring内部使用。