3.15 ApplicationContext的额外功能

正如本章开头所讨论的那样,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 国际化

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 标准和自定义事件

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
}

当使用异步事件的时候有下面两个限制:

  1. 如果事件监听器抛出异常,则不会将其传播给调用者,查看AsyncUncaughtExceptionHandler获取详细信息。
  2. 此类事件监听器无法发送回复。如果你需要将处理结果发送给另一个时间,注入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 便捷访问低优先级的资源

为了最佳使用和理解上下文,用户应该熟悉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 应用程序

你可以通过声明式创建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文件部署

可以将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端点,这当然可能是同一台机器上的其他应用模块。