ApplicationContext除了作为容器,负责bean的初始化和管理,还将一些有用功能模块集成到framework里:
1、i18n风格的文本的国际化,通过MessageSource
接口;
2、容器内的事件发布(ApplicationEventPublisher
)订阅(ApplicationListener
);
3、访问url和文件资源,通过ResourceLoader接口;
4、多个context组成一个父子层级结构,将不同bean划分到不同的context,对系统进行分层;
国际化
Spring以MessageSource接口为核心,定义了一套为不同语言地区(locale)提供本地化文本消息的机制。
MessageSource接口
MessageSource的主要方法如下:
String getMessage(String code, Object[] args, String default, Locale loc)
: 最基本的提取文本消息方法,传入的参数依次为:消息编号,参数列表,默认消息(如果MessageSource没有定义该消息),语言地区;消息定义可以包含参数占位符,通过JDK的MessageFormat功能进行参数值替换;String getMessage(String code, Object[] args, Locale loc)
:和上面的类似,区别在于没有默认消息,如果MessageSource没有定义该消息,则抛出NoSuchMessageException
异常;String getMessage(MessageSourceResolvable resolvable, Locale locale)
:将1的参数封装在MessageSourceResolvable
里面;
ApplicationContext本身扩展了MessageSource,但是并不直接实现具体功能,而是委托给另外一个实现MessageSource接口的bean。
MessageSource Bean
当ApplicationContext加载的时候,会搜寻一个实现了MessageSource的Bean,而且这个Bean的名字必须叫做messageSource
(这个名字在Spring的源码里面是硬编码的)。如果找到了这个bean,那么所有对ApplicationContext相关方法的调用,都会转发给这个bean。
如果没有找到这个bean,那么就去它的父context去寻找。如果还是没找到,就创建一个DelegatingMessageSource
(空的MessageSource实现)来接受调用。
MessageSource实现
Spring提供了两个实现,ResourceBundleMessageSource
和StaticMessageSource
。其中StaticMessageSource可以通过编程方式添加消息,在实际项目中很少使用。这里主要介绍ResourceBundleMessageSource。
<beans>
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>format</value>
<value>exceptions</value>
</list>
</property>
</bean>
</beans>
ResourceBundleMessageSource从文件资源加载消息,上面引用两个消息源:format和exceptions。每个消息资源其实就是位于classpath的属性文件:
# in format.properties
message=Alligators rock!
# in exceptions.properties
argument.required=The {0} argument is required.
有了上面的配置,下面代码打印字符串Alligators rock!
:
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);
}
多语言
关于国际化文本消息的加载和解析,Spring采用与JDK ResourceBundle一致的规则。
在上面的示例中,如果添加针对British的消息源,只要添加format_en_GB.properties和exceptions_en_GB.properties即可。比如
# in exceptions_en_GB.properties
argument.required=Ebagum lad, the {0} argument is required, I say, required.
下面的代码打印的是Ebagum lad, the 'userDao' 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);
}
如果某个消息的指定语言版本没有定义,那么Spring会返回默认版本,关于默认版本的规则,文档没有写明,待验证。
事件机制
Spring的事件继承自ApplicationEvent,在ApplicationContext的生命周期内,会发布很多事件,如果客户代码想接受这些事件,实现ApplicationListener即可。
Spring内置事件
Spring的内置事件有以下这些:
Event | 描述 |
---|---|
ContextRefreshedEvent | ApplicationContext初始化完成或者刷新(调用了ConfigurableApplicationContext.refresh方法),这里的"初始化完成"意味着,所有的bean定义被加载,post-processor被激活,单例bean完成了初始化,且ApplicationContext自身可用。在Context关闭之前,refresh事件可能发生多次,因为有些context比如ApplicationContext支持hot refresh,而GenericApplicationContext 不支持 |
ContextStartedEvent | 当调用ConfigurableApplicationContext.start方法时被发布,stated意味着所有Lifecyle的实现者收到start信号,由于Context可以被反复start和stop,因此该事件也可能发生多次 |
ContextStoppedEvent | 当调用ConfigurableApplicationContext.stop方法时被发布,stop意味着所有Lifecyle的实现者收到stop信号 |
ContextClosedEvent | ApplicationContext被关闭,要么调用了ConfigurableApplicationContext.close方法,要么进程关闭且Context注册了JVM的shutdown hook;context一旦被关闭,所有的单例bean被销毁,不可能再被start或refresh |
RequestHandledEvent | 这个事件意味着一个Http请求处理完成,Spring Web MVC中使用 |
ServletRequestHandledEvent | RequestHandledEvent的子类,添加了一些Servlet上下文字段 |
自定义事件
用户代码可以创建并发布自定义事件,请看示例:
public class BlackListEvent extends ApplicationEvent {
private final String address;
private final String content;
public BlackListEvent(Object source, String address, String content) {
super(source);
this.address = address;
this.content = content;
}
}
public class EmailService implements ApplicationEventPublisherAware {
private ApplicationEventPublisher publisher;
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void sendEvent(String address, String content) {
publisher.publishEvent(new BlackListEvent(this, address, content));
}
}
创建一个自定义事件只要继承ApplicationEvent即可,要发布事件则要拿到ApplicationEventPublisher引用,通过ApplicationEventPublisherAware接口,或直接@Autowire。
监听事件
一个bean要监听Spring事件,实现ApplicationListener接口即可:
public class EventListener implements ApplicationListener<ContextRefreshedEvent> {
public void onApplicationEvent(ContextRefreshedEvent event) {
//do something
}
}
通过注解来监听
public class EventListener {
@EventListener
public void processEvent(ContextRefreshedEvent event) {
//do something
}
}
这种方式不需要实现ApplicationListener接口,事件的类型是通过方法的参数类型来指明。
如果需要监听不止一种事件,可以添加注解参数来指明要监听的事件类型,此时方法参数也可以省去:
public class EventListener {
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void processEvent() {
//do something
}
}
@EventListener注解还可以通过Spel表达式来加一些过滤条件,比如:
@EventListener(condition = "#blEvent.content == 'my-event'")
public void processEvent(BlackListEvent blEvent) {
}
condition表达的意思相当于:content属性等于’my-event’。关于Spel表达式由于实际用的很少,这块就不展开了。
监听方法返回值
事件处理方法可以返回另外一个事件,Spring会在处理完当前事件后,发布返回的事件:
@EventListener
public ListUpdateEvent processEvent(BlackListEvent event) {
}
监听方法甚至还可以返回一个事件集合,来发布多个事件。
异步监听器
Spring的event publish&listener默认是同步实现的,也即publish方法调用,要在所有的listener执行完毕后才返回,利用Spring的Async实现异步监听器。
@EventListener
@Async
public ListUpdateEvent processEvent(BlackListEvent event) {
}
Async机制本质上和Event机制没有什么关联,是两个平行的功能,但是异步方式会有以下两个影响:
- 监听方法抛出异常,不会被调用者捕获;
- 异步监听方法无法通过返回值来发布另外一个事件
监听器顺序
使用@Order注解,可以添加相对顺序
@EventListener
@Order(42)
public ListUpdateEvent processEvent(BlackListEvent event) {
}
泛型事件
比如EntityCreatedEvent事件,包含了所创建entity的类型参数,而下面的监听器只监听Person类型的创建:
@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
// ...
}
但是,由于java泛型的类型擦除机制,只有事件对象能在运行时保留类型信息时才能工作,比如class PersonCreatedEvent extends EntityCreatedEvent<Person> { … }
。因此个人觉得不要依赖这个机制为妙。
加载资源
Spring定义了一套加载底层资源的机制,核心接口包括ResourceLoader和Resource,用来取代java.net.URL。 这个机制本身比较复杂,单独用一个章节(下一章)来讲解。
父子Context
在同一个进程里面,可以创建多个ApplicationContext,每个Context包含一组bean,对系统进行横向或纵向的拆分。Spring Web MVC架构使用这种方式,将系统分成了web层和逻辑层,前者主要包含处理http请求的Controller,后者包含DAO和Service等。
Context之间可以形成父子关系,子Context访问父Context的bean,反之则不成立。
下面通过一个简单的示例来展示这种关系:有两个bean,ServiceBean和DaoBean,前者依赖后者。我们定义了两个@Configuration类,分别用于parent和child context。
@Configuration
public class ParentConfig {
@Bean
public ServiceBean serviceBean(DaoBean daoBean) {
return new ServiceBean(daoBean);
}
@Bean
public DaoBean daoBean() {
return new DaoBean();
}
@Bean
public BeanTracePostProcessor postProcessor() {
return new BeanTracePostProcessor();
}
}
@Configuration
public class ChildConfig {
@Bean
public ServiceBean serviceBean(DaoBean daoBean) {
return new ServiceBean(daoBean);
}
}
然后创建ApplicationContext如下:
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(ParentConfig.class);
AnnotationConfigApplicationContext child = new AnnotationConfigApplicationContext();
child.setParent(parent);
child.register(ChildConfig.class);
child.refresh();
parent.getBean(ServiceBean.class).work();
child.getBean(ServiceBean.class).work();
}
}
我们可以发现,child context定义的ServiceBean可以完整正常工作,使用了parent context里面的DaoBean。如果将DaoBean的定义移至ChildConfig,则parent context初始化会抛异常。
BeanPostProcessor只对所属的context有效,不会影响它的parent或child context。完整的示例代码在这里,见子工程hierarchy。