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()方法的输出结果为:
TheuserDao 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端点,这当然可能是同一台机器上的其他应用模块。