IoC容器15——ApplicationContext的附加功能

原创
2017/07/26 11:43
阅读数 50

“第2节 注解的监听器“中关于SpEL表达式作为过滤条件的部分需要重看

正如之前章节介绍的,org.springframework.beans.factory包提供了管理和操作bean的基础功能,包括编程的方式。org.springframework.context包添加了ApplicationContext接口,它扩展了BeanFactory接口,同时也扩展了其它接口以更多的应用程序框架导向的风格来提供附加的功能(???)。许多人以完全声明的风格使用ApplicationContext,甚至不用编程的方式创建它;作为替代,依赖于像COntextLoader这样类的支持自动实例化ApplicationContext,作为Java EE web应用程序正常启动过程的一部分。

为了以更加面向框架的风格来增强BeanFactory功能,context包也提供了下面的功能:

  • 通过MessageSource接口,访问国际化风格的消息;
  • 通过ResourceLoader接口,访问资源,例如URL和文件;
  • 通过使用ApplicationEventPublisher接口,将事件发布到实现ApplicationListener接口的bean;
  • 通过HierarchicalBeanFactory接口加载多个(结构层次化的)上下文,允许每个关注一层,例如应用程序的web层。

1 使用MessageSource进行国际化

ApplicationContext接口扩展了MessageSource接口,因此提供了国际化的功能。Spring也提供了HierarchicalMessageSource接口,它可以层次化的解析消息。这些接口一起为Spring消息解析提供基础。定义在这些接口中的方法包括:

  • String getMessage(String code, Object[] args, String default, Locale loc):用于从MessageSource取得消息的基础方法。当没有找到用于特定locale的消息,将会使用默认的消息。任何传入的参数会变为替代值,使用标准库提供的MessageFormat功能;

  • String getMessage(String code, Objectp[] args, Locale loc):本质上与上面的方法相同,不同点在于:没有指定默认消息,如果不能找到消息会抛出NoSuchMessageException;

  • String getMessage(MessageSourceResolvable resolvable, Locale locale):上面方法中使用的所用属性都被包装到MessageSourceResolvable类中。

当一个ApplicationContext被加载时,它会自动搜索在上下文中定义的MessageSource bean。这个bean必须名为messageSource。如果找到这样一个bean,所有对于上述方法的调用被派发给这个消息源。如果没有找到消息源,ApplicationContext将尝试在父容器中寻找相同名字的bean。如果找到,它使用这个bean作为MessageSource。如果ApplicationContext不能找到任何消息源,一个空的DelegatingMessageSource会被实例化用于能够接受上述方法的调用。

Spring提供两个MessageSource的实现,ResourceBundleMessageSource和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>

在该示例中,假设在类路径中定义了三个资源束名为format、exceptions和windows。任何解析消息的请求都会以JDK标准方式通过ResourceBundle来处理。为了示例的目的,假设其中两个资源束文件的内容是:

# 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和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,如果想要使用英国的语言环境解析消息,需要分别创建名为format_en_GB.properties、exceptions_en_GB.properties和windows_en_GB.properties的文件。

典型的,语言环境解析被应用程序运行的环境管理。在这个例子中,使用哪个(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中定义的实现了MessageSourceAware接口的bean在创建时都会被注入应用上下的MessageSource。

作为ResourceBundleMessageSource的替代,Spring提供了ReloadableResourceBundleMessageSource类。这个类支持相同的束文件但是比基于JDK的标准ResourceBundleMessageSource实现更加灵活。尤其是他允许从任何Spring资源的位置读取文件(不仅仅从classpath)并且支持束配置文件的热重载(同时有效的缓存它们)。详细信息可查看查看ReloadableResourceBundleMessageSource javadoc。

2 标准和自定义事件

ApplicationContext中的事件处理通过ApplicationEvent类和ApplicationListener接口提供。如果一个实现了ApplicationListener接口的bean被部署到上下文中,每次一个ApplicationEvent发布到ApplicationContext时,这个bean会被通知。实际上,这是标准的观察者设计模式。

从Spring 4.2开始,事件基础设施被明显的提升并且提供了基于注解的模型和发布任意事件,即对象不需要继承ApplicationEvent。当一个对象被发布,Spring将它包裹到一个事件中。

Spring提供了以下标准事件:

事件 说明
ContextRefreshedEvent 当ApplicationContext被初始化或刷新时发布,例如使用ConfigurableApplicationContext接口的refresh()方法。“初始化”在这里的意思是所用bean被加载,后处理器bean被发现和激活,单例被预初始化,并且ApplicationContext对象可以使用。只要上下文没有被关闭,刷新可被多次出发,只要所选的ApplicationContext支持这种“热”刷新。例如,XmlWebApplicationContext支持热刷新,但是GenericApplicationContext不支持。
ContextStartedEvent 当ApplicationContext开始的时候发布,例如使用ConfigurableApplicationContext接口的start()方法。”开始“在这里的意思是所有LifeCycle bean接受到一个明确的开始信号。典型的,这个信号被用于在明确的停止之后重启bean,但也可以用于开启没有被配置为自动开启的组件,例如在初始化中没有被开启的组件。
ContextStoppedEvent 当ApplicationContext被停止时发布,例如使用ConfigurableApplicationContext接口的stop()方法。“停止”在这里的意思是所有生命周期bean接受到一个明确的停止信号。一个已停止的上下文可以通过调用start()方法重启。
ContextClosedEvent 当ApplicationContext被关闭时发布,例如使用ConfigurableApplicationContext接口的close()方法。“关闭”在这里的意思是所有的单例bean被销毁。已关闭的上下文到达了生命周期的最后,它不能被刷新和重启。
RequestHandleEvent 一个基于web的事件,通知所有的bean一个HTTP请求已被处理。这个事件在请求完成时被发布。这个事件仅仅适用于使用Spring的DispatcherServlet的web应用程序。

也可以创建和发布自定义事件。这个例子展示了一个继承了Spring的ApplcationEvent基类的简单类:

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...

}

为了发布一个自定义事件,调用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使用自定义的事件被范型参数化。这意味着onApplicationEvent()方法可以保持类型安全,避免任何向下转型的需要。可以根据需要注册多个事件监听器,但是请注意,默认的事件监听器同步的接收事件。这意味着publishEvent()方法会被阻塞直到所用监听器都完成了对这个事件的处理。这种阻塞和单线程处理的一个好处是当一个监听器接收到事件,如果一个事务上下文是可用的它就在发布者的事务上下文中操作。如果需要另外的事件发布策略,可以参考Spring的ApplicationEventMulticaster接口的javadoc。

下面的例子展示了用于注册和配置上面的类的XML 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>

把它们放在一起,当emailServie bean的sendEmail()方法被调用,如果有任何邮箱应被列入黑名单,一个类型为BlackListEvent的自定义事件被发布。blackListNotifier bean被注册为一个ApplicationListener,因此接收到BlackListEvent,此时它可以通知适当的部分。

Spring的事件机制被设计为同一个应用上下文中的Spring bean之间的简单通讯。然而,对于更加复杂的企业级集成需求,分开维护的Spring Integration项目提供了对于基于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表达式来添加额外的运行时过滤。

例如,接收者可以被重写为仅当事件的test属性等于foo时被调用:

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

每个SpEL表达式由专用的上下文评估。下面的表格列出了可用于上下文的项目,因此可以将它们用于条件事件处理:

名字 位置 描述 例子
事件 根对象 实际的ApplicationEvent #root.event
参数数组 根对象 用于调用对象的参数(数组的形式) #root.args[0]
参数名 评估上下文 任何方法参数的名字。如果由于一些原因名字不可用(例如没有debug信息),参数名在#a<#arg>仍然可用,其中#arg代表参数的下标(从0开始)。 #blEvent 或者 #a0(也可以使用#p0 或 #p<#arg>符号作为别名

请注意,#root.event允许获取底层事件,甚至是方法签名实际引用任意一个被发布的对象。

如果需要发布一个事件同时处理另一个,可以修改方法签名,返回需要发布的事件,例如:

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

这个特性在异步监听器中不支持。

这个新方法会在每次处理完BlackListEvent后发布一个新的ListUpdateEvent。如果想发布多个事件,返回事件的Collection即可。

异步监听器

如果需要一个特定的监听器来异步的处理事件,只需要重用标准的@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>,其中T是被创建的实体类型。可以创建如下的监听器定义用于仅接收Person的EntityCreatedEvent:

@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
    ...
}

由于类型擦出,只有在触发的事件解析了事件监听过滤器的范型参数时才会其作用(像这样的形式class PersonCreatedEvent extends EntityCreatedEvent<Person>{...})。

在某些情况下,这也许变得很乏味如果所有事件遵循相同的结构(上面的事件应该是这种情况)。在这种情况下,可以实现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 方便的访问低级资源

为了更好的使用和理解应用上下文,用户应该属性Spring的资源抽象。

一个应用上下问是一个ResourceLoader,可以用于加载Resource。一个Resource实质上JDK类java.net.URL的功能更丰富的版本,实际上,Resource的实现在适当的地方包裹了java.net.URL的一个实例。一个Resource可以从几乎任何位置以透明的方式获取低级资源,包括从类路径、文件系统位置、使用标准URL描述的任何位置,或其它地方。如果一个资源定位字符串是一个简单的路径而没有指定前缀,则他们从何处而来取决于实际的应用上下文类型。

可以配置一个bean部署到应用上下文来实现指定的回调接口,ResourceLoaderAware,在初始化时应用上下文自身所谓ResourceLoader传入时被自动回调。可以暴露Resource类型的属性,用于访问静态资源;它们可以像其它属性一样被注入。可以指定这些Resource属性为简单的字符串路径,依靠被上下文自动注册的、类型为PropertyEditor的特定JavaBean,当bean被部署时将这些文本字符串转换为实际的Resource对象。

提供给ApplicationContext构造函数的位置路径实际上是资源字符串,并且以简单的形式根据具体的上下文实现进行恰当的处理。ClassPathXmlApplicationContext将一个简单的定位路径视为类路径位置。也可以使用带特定前缀的位置路径强制指定从类路径或URL加载定义,而不考虑实际上下文类型。

4 方便的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文件夹中以Context.xml结尾的文件,同时/WEB-INF/**/*Context.xml匹配在WEB-INF任何子文件夹中的这样的文件。

5 将Spring应用上下文部署为Java EE RAR文件

可以将Spring应用上下文部署为RAR文件,将上下文、所有需要的bean类、库JAR文件封装进一个Java EE RAR部署单元。这等价于引导一个托管在Java EE环境中的独立的应用上下文,它可以访问Jave EE服务器设施。RAR部署是更自然的方案用于替代无头的WAR文件,实际上,没有任何HTTP入口点的WAR文件仅用于在Java EE环境中引导Spring的应用上下文。

RAR部署是不需要HTTP入口点而只需要消息端点和计划任务的应用上下文的理想选择。在这种上下文中的bean可以使用应用服务器资源例如JTA事务管理器、JNDI绑定的JDBC数据源、JMS连接工厂实例和任何通过Spring标准事务管理器和JNDI、JMX支持工具向JMX服务器平台注册的组件。应用程序组件也可以通过Spring的TaskExecutor抽象与应用程序服务器的JCA工作管理器进行交互。

查看SpringContextResourceAdapter类的javadoc来获取RAR部署的配置细节。

对于将Spring应用上下文作为Java EE RAR文件的简单部署:将所有所用应用类打包成一个RAR文件,这是一个具有不同扩展名的标准JAR文件。将所有必需的库JAR添加到RAR存档的根目录中。添加META-INF/ra.xml部署描述符(注入SpringContextResourceAdapter的javadoc中展示的那样)和所有相关的Spring XML bean定义文件(一般是META-INF/applicationContext.xml),将得到的RAR文件放入应用服务器的部署文件夹。

这样的RAR部署单元一般是自持的;它们不对外暴露组件,甚至相同应用的其它组件。与基于RAR的ApplicationContext交互一般通过与其它模块共享JMS目的地的方式。基于RAR的ApplicationContext也周期执行任务、对文件系统中的新文件进行响应(或类似物)。如果需要允许外部的同步访问,可以暴露RMI端点,它当然也可以用于相同机器上的其它应用模块。

展开阅读全文
加载中

作者的其它热门文章

打赏
0
0 收藏
分享
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部