Spring核心技术(十四)——ApplicationContext的额外功能

在前文的介绍中我们知道,org.springframework.beans.factory包提供了一些基本的功能来管理和控制Bean,甚至通过编程的方式来实现。org.springframework.context包添加了ApplicationContext接口,ApplicationContext接口扩展了BeanFactory接口。ApplicationContext接口扩展了其他的接口来以一种更加面向应用的方式来提供额外的功能。很多开发者喜欢以完全显式声明的方式来使用ApplicationContext,不需要通过编程的方式来创建ApplicationContext,不过需要依赖于一些支持类,比如J2EE通过ContextLoader来自动实例化ApplicationContext,启动过程也是J2EE程序的启动过程之一。

为了增强BeanFactory的功能,使之变得更加面向应用,context包中还提供如下功能:

  • 通过MessageSource接口来访问i18n的messages
  • 通过ResourceLoader接口访问资源(比如URL和文件)
  • 通过ApplicationEventPublisher接口,可以支持时间发布(对应Bean需实现ApplicationListener接口)
  • 通过HierarchicalBeanFactory接口来加载多个(层次化)context,允许每个context关注自己所在的层,比如应用的web层

通过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加载的时候,会自动搜索context中定义的MessageSource这个Bean,该Bean的名字必须为messageSource。如果这个Bean找到了,所有前面调用的方法都会代理到这个MessageSource上面。如果没有发现messageSourceApplicationContext会尝试去父节点,来查找对应的Bean。如果找到,会将Bean作为MessageSource来使用。如果ApplicationContext无法找到任何的MessageSource,那么就会使用一个空的DelegatingMessageSource来解决前面那些方法的调用。

Spring提供了两种MessageSource的实现方案,ResourceBundleMessageSource以及StaticMessageSource。都实现了HierarchicalMessageSource来支持嵌套message。StaticMessageSource很少使用,但是却让开发者可以通过编程的方式将message增加到源上。ResourceBundleMessageSource可以参考如下的例子:


    
        
            
                format
                exceptions
                windows
            
        
    

在上面的例子当中,假定开发者配置了绑定到classpath的资源分别是formatexceptions以及windows。任何来解析message的请求都会通过JDK标准的ResourceBundle来解析message的。假设上面绑定的属性如下:

# 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之中,随着classpath而加载。messageSource这个Bean通过basenames属性指向到了一些绑定的资源。那三个文件在basenames属性的中的名字需要在classpath的root下面,并且叫做format.propertiesexceptions.propertieswindows.properties才能解析。

下面的例子展示了将参数传递给message来进行属性查找,这些参数将会被转换为String,并插入到查找信息的占位符中。



    
    
        
    

    
    
        
    


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实现都遵循着相同的locale解析规则来作为JDK的ResourceBundle。简而言之,为了配合前面例子中所定义的messageSource,如果需要解析,比如说British locale(en-GB),那么开发者需要创建的properties文件为format_en_GB.properties,exceptions_en_GB.properties以及windows_en_GB.properties

默认情况下,locale的解析是由应用的运行环境所管理的。在下面的例子中,我们手动指定locale为British:

# 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接口都会被注入MessageSource

作为ResourceBundleMessageSource的另一个选择,Spring提供了一个类,名为ReloadableResourceBundleMessageSource。这个类支持类似于基于JDK的ResourceBundleMessageSource实现,但是使用起来更为弹性。尤其是,它允许从任何Spring资源位置来加载文件,而不仅仅是classpath,支持热重载属性文件。开发者可以参考ReloadableResourceBundleMessageSource的Javadoc了解更多的细节。

标准和定制事件

ApplicationContext中提供事件的处理机制,通过ApplicationEvent类和ApplicationListener接口来实现。如果Bean实现了ApplicationListener接口,那么任何时候有ApplicationEvent发布到ApplicationContext的事后,都会通知到该Bean。本质上来说,这就是一个标准的观察者模式。

在Spring 4.2的版本之后,事件的架构就已经被增强了,提供了一个基于注解模型的来发布任意的事件,这样Bean也不需要来继承ApplicationEvent。当这样的一个对象发布,我们将其包裹进PayloadApplicationEvent

下表是Spring提供的标准事件:

事件 解释
ContextRefreshedEvent ApplicationContext初始化或者刷新的时候发布,举例来说,当调用了ConfigurableApplicationContext之中的refresh()方法之后,就会产生该事件。“初始化”在这里的意思就是指当加载Bean,后置处理器Bean(post-processor),预加载的单例Bean,以及ApplicationContext对象可以使用的时候。在context没有关闭的事后,可以多次调用refresh(),一些ApplicationContext的实现都支持“热”刷新。举例来说,XmlWebApplicationContext支持热刷新,但是GenericApplicationContext不支持。
ContextStartedEvent ApplicationContext启动的事后会发布,通过ConfigurableApplicationContext接口中的start()方法来触发。“启动”在这里就意味着所有的Lifecycle的Bean都会收到明确的启动信号。通常,这个信号是用来在Bean停止之后来重启的,但是也能够用来启动没有配置自动启动的组件,比如,在初始化过程中没有启动的组件会随着start()启动。
ContextStoppedEvent ApplicationContext停止之后发布,通过ConfigurableApplicationContext之中的stop()方法来触发。“停止”意味着所有的LifecycleBean受到了明确的停止信号。停止的context可能再次通过start()来重启。
ContextClosedEvent ApplicationContext关闭的时候发布,通过ConfigurableApplicationContext之中的close()方法来触发。“关闭”意味着所有的单例Bean都会被销毁。关闭的context已经到了最后的时刻,无法刷新或者重启。
RequestHandledEvent Web特定的事件,告诉Bean一个Http请求正在服务。当请求完成之后,才发布该事件。这个事件仅仅在使用SpringDispatcherServlet的时候才可用。

开发者也可以创建和发布自己的定制事件。如下面的例子:

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 blackList;
    private ApplicationEventPublisher publisher;

    public void setBlackList(List 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 {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    public void onApplicationEvent(BlackListEvent event) {
        // notify appropriate parties via notificationAddress...
    }

}

需要注意的是ApplicationListener是一个基于参数泛型化得接口,BlackListEvent就是前面所自定义的一个ApplicationEvent,这意味着onApplicationEvent()方法仍然是类型安全的,避免了向下的类型转换。开发可以随意注册事件监听器,但是需要注意的是,默认情况下,事件监听器接受事件是同步的。这也意味着除非所有的监听器都处理完了事件,否则publishEvent()方法会一直阻塞。同步的优势在于当监听器接收到事件的时候,监听器对事件的处理都在发布者的事物内部进行处理。

下面的代码将展示如何使用前面定义的类:


    
        
            [email protected]
            [email protected]
            [email protected]
        
    



    

上面的XML代码和前面的Bean定义结合在一起的时候,当调用了emailServicesendEmail()方法,那么只要有XML种列举的黑名单,那么BlackListEvent就会发布。blackListNotifierBean作为监听器这样就会接收到BlackListEvent

Spring的事件机制的设计初衷是为了让Spring的Bean和应用上下文进行简单的交互。然而,在很多复杂的企业集成需求上,可以使用Spring Intergration,它提供了更好的支持,可以用来构建轻量级,面向模式,事件驱动的Spring编程模型。

基于注解的事件监听器

在Spring的4.2版本后,事件的监听器就可以通过在Bean的任何公开方法上面使用EventListener注解来实现了。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() {

}

当然,通过condition属性来增加运行时的过滤也是可以的。可以通过SpEL表达式来进行匹配。

参考代码如下:

@EventListener(condition = "#event.test == 'foo'")
public void processBlackListEvent(BlackListEvent event) {
    // notify appropriate parties via notificationAddress...
}

SpEL表达式会再一次检查上下文。下表列出了context可见的一些项目,来用于条件的事件处理:

Name Location Description Example
event 根对象 实际的ApplicationEvent #root.event
args 根对象 用来调用目标的参数(数组) #root.args[0]
argument name 评估上下文 任何方法的参数。在某些情况下,名字是不可见的。(比如说非debug的情况下),但是参数的名字仍然是可见的。在#a<#arg>中,#arg表示的是参数的索引(从0开始) #iban或者#a0

需要注意的是#root.event允许开发者访问潜在的事件,甚至方法会直接引用到发布的对象。

如果开发者需要将发布的事件作为结果传递给其他方法也是允许的,只需要修改方法签名,返回发布的对象即可:

@EventListener
public ListUpdateEvent handleBlackListEvent(BlackListEvent event) {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}

需要注意的是,这个特性不支持异步监听器

上面的新方法将会发布一个新的ListUpdateEvent来给处理BlackListEvent的方法。如果开发者需要发布多个方法,返回事件的集合即可。

异步监听器

如果开发者需要以异步的方式来处理事件,那么可以使用@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 event) {
    ...
}

由于函数签名对方法参数类型的敏感,上面的方法只会当事件匹配了泛型的时候才能执行,其他的类型则会过滤掉。(只有一些类型形如class PersonCreatedEvent extends EntityCreatedEvent之类的才能进行事件的处理)

在有些情况下,这一点也会变得很麻烦,比如如果所有的事件都是遵循这类结构的话,因为泛型参数无法传入所以就需要写很多的事件处理函数。在这种情况下可以通过实现ResolveableTypeProvider来令框架能够在运行环境提供处理:

public class EntityCreatedEvent
        extends ApplicationEvent implements ResolvableTypeProvider {

    public EntityCreatedEvent(T entity) {
        super(entity);
    }

    @Override
    public ResolvableType getResolvableType() {
        return ResolvableType.forClassWithGenerics(getClass(),
                ResolvableType.forInstance(getSource()));
    }
}

ResolvableType不仅仅支持ApplicationEvent,任何作为事件发送的对象都能支持。

low-level资源的便捷访问

为了优化使用和理解应用的上下文,开发者应该了解Spring针对Resource的抽象。

一个应用的上下文就是一个ResourceLoader,可以用来加载ResourceResource在本质上就是一个功能更丰富的JDK的类java.net.URL,实际上,Resource的实现之中就包含了一个java.net.URL的实例。Resource可以通过一种透明的方式获取几乎任何位置的low-level的资源,包括从classpath,文件系统地址,或者任何通过标准URL来描述的位置以及一些其他的变化等。如果资源地址仅仅是一个简单的路径二没有特殊的前缀的话,那么那些资源就是来自于特指的应用上下文的类型。

开发者可以将实现一些特殊的回调接口的Bean配置发布到到应用上下文。ResourceLoaderAware,当Bean实现了该接口,将会在初始化的时候讲应用上下文一起传递,作为ResourceLoader。开发者也可以将Resource的属性曝露出来,来访问静态资源,它们和注入的其它属性是一样的。开发者也可以特指那些Resource的属性为一些简单的字符串路径,并依赖于特殊的JavaBeanPropertyEditor,可以自动由上下文注册,然后将Resouce解析为实际的的资源对象。

ApplicationContext中所支持的路径其实就是资源字符串,在简单的模式下,其实就近乎为特定的context实现。ClassPathXmlApplicationContext将简单的路径视作classpath路径的。开发者也可以使用定位路径(资源字符串)通过一些特定的前缀来声明路径是classpath还是URL。

Web应用的ApplicationContext实例化

在Web应用中,开发者可以通过使用ContextLoader来创建加载一个ApplicationContext。当然,开发者也可以通过编程的方式,通过ApplicationContext的一些实现来创建ApplicationContext

开发者可以通过使用ContextLoaderListener来注册ApplicationContext,代码如下:


    contextConfigLocation
    /WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml



    org.springframework.web.context.ContextLoaderListener

该监听器会检查contextConfigLocation参数,如果参数不存在,监听器会使用/WEB-INF/applicationContext.xml作为默认路径。当路径存在时,监听器会通过预定义的分隔符(逗号,分号以及空格)分割路径,并将分割的路径作为实际应用检索的上下文路径。Ant风格的路径也是支持的,比如/WEB-INF/*Context.xml会匹配所有以Context.xml为结尾且在WEB-INF目录下的文件,而WEB-INF/**/*Context.xml则会匹配所有WEB-INF下的子目录。

通过J2EE RAR文件部署Spring ApplicationContext

将Spring的上下文打包成RAR文件也是可以部署的,可以将context以及所有需要的Bean的类和依赖jar包打包成J2EE RAR部署单元。这也等同于启动一个独立的ApplicationContext,只是由J2EE环境来host,ApplicationContext可以访问J2EE服务设施。RAR的部署也是部署WAR场景的一种更为自然的选择,实际上,不包含HTTP访问的WAR文件也仅仅是在J2EE环境启动Spring的ApplicationContext。

RAR的部署对于不需要HTTP的的任务是更为理想的,比如说定时任务之类的。Bean在这种上下文可以使用应用服务器的资源诸如JTA事物管理器和JNDI绑定的JDBC数据源,JMS连接工厂实例,或者注册平台的JMX服务器,这些通过Spring标准的事务管理和JNDI,JMX支持的特性即可。应用组件同样可以通过Spring的TaskExecutor抽象来进行协同工作。

可以通过查看SpringContextResourceAdapter类来了解RAR文件部署的一些详细信息。

对于简单的部署J2EE的RAR文件:将所有的应用类都打包到一个RAR文件中即可。将其依赖的jar包也放到RAR文件的root处,增加一个META-INF/ra.xml文件(参考SpringContextResourceAdapter的Javadoc)也将关联的Spring XML的Bean定义文件置于其中,然后将RAR文件放到应用服务器的发布目录即可。

这样的RAR文件的部署是自包含的,他们不会暴露组件给外部,甚至是同一个应用的其它模块都不需要引用。与基于RAR的ApplicationContext进行交互通常是通过JMS来和其它模块进行交互。基于RAR的ApplicationContext也可能做些定时任务,针对文件系统中的新文件进行处理等工作。如果需要允许支持外部的同步访问,可以接入RMI服务。

你可能感兴趣的:(Spring核心技术(十四)——ApplicationContext的额外功能)