ApplicationContext应用级功能


ApplicationContext应用级功能

org.springframework.beans.factory 包提供了管理和使用bean的基本功能, org.springframework.context 包下添加了继承BeanFactoryApplicationContext接口,其提供了额外的应用级功能

ApplicationContext提供了如下功能:

  • 通过MessageSource接口进行国际化
  • 通过ResourceLoader接口获取URLs或者文件等资源
  • 事件发布,通过ApplicationEventPublisher通知实现ApplicationListener接口的bean
  • 加载多个上下文环境(继承关系),通过HierarchicalBeanFactory使得每一个context能专注于其功能项,比如web应用的前端控制器层。

使用 MessageSource进行国际化

ApplicationContext接口继承了MessageSource接口,因此ApplicationContext能够提供国际化的功能,Spring也提供HierarchicalMessageSource来支持分层处理国际化,接口定义的方法包括:

  • 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被加载时,会自动查询容器中的MessageSourcebean,并且这个bean的名字必须为messageSource,如果找到MessageSourcebean,所有相关的方法都会被代理到这个bean实现,如果未找到,ApplicationContext会查询其父容器中的messageSourcebean,如果未找到任何messageSourcebean,将会实例化DelegatingMessageSource进行使用。

spring提供了2个MessageSource实现类: ResourceBundleMessageSourceStaticMessageSource。并且都实现了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>

以上表示类路径下有3个资源集合: format, exceptionswindows ,任何请求都将通过ResourceBundle – 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", Locale.ENGLISH);
    System.out.println(message);
}

结果:

Alligators rock!

解析:上述MessageSource定义在beans.xml中,messageSourcebean通过basenames指定国际化资源文件。上述指定了3个文件: format.properties, exceptions.properties, windows.properties

MessageSource获取使用参数:

<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.something.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", Locale.ENGLISH);
        System.out.println(message);
    }
}

结果如下:

The userDao argument is required.

如果想要进行英国的国际化(en-GB),需要创建 format_en_GB.properties, exceptions_en_GB.properties, 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中任何实现MessageSourceAware接口的对象在创建和被配置的时候都将被注入MessageSource对象。

tips:ResourceBundleMessageSource的一个可选实现是ReloadableResourceBundleMessageSourceReloadableResourceBundleMessageSource比基于JDK的ResourceBundleMessageSource更加灵活,尤其是支持从任何spring资源加载位置读取(不仅仅是类路径)以及支持热加载, 详情:ReloadableResourceBundleMessageSource

标准和自定义事件

ApplicationContext通过ApplicationEventApplicationListener提供事件处理机制,如果容器中一个bean实现了ApplicationListener接口,每当有ApplicationEvent发布到ApplicationContext中就会通知此bean,本质上这就是观察者模式的实现。

Spring 4.2之后,事件机制得到增强,Spring提供了注解开发模式以及事件不再必须实现ApplicationEvent接口,当发布事件时,Spring自动将其封装为ApplicationEvent

Spring提供的标准事件:

Event解释
ContextRefreshedEvent在初始化或刷新ApplicationContext时发布(例如,通过使用ConfigurableApplicationContext接口上的refresh()方法)。在这里,“已初始化”是指所有Bean均已加载,检测到并激活了后处理器Bean,已预先实例化单例并且可以使用ApplicationContext对象。只要尚未关闭上下文,只要所选的ApplicationContext实际上支持这种“热”刷新,就可以多次触发刷新。例如,XmlWebApplicationContext支持热刷新,但GenericApplicationContext不支持。
ContextStartedEvent使用ConfigurableApplicationContext接口上的start()方法启动ApplicationContext时发布。在这里,“已启动”是指所有Lifecycle bean都收到一个明确的启动信号。通常,此信号用于在显式停止后重新启动Bean,但也可以用于启动尚未配置为自动启动的组件(例如,尚未在初始化时启动的组件)。
ContextStoppedEvent通过使用ConfigurableApplicationContext接口上的stop()方法停止ApplicationContext时发布。此处,“已停止”表示所有Lifecycle bean均收到明确的停止信号。停止的上下文可以通过start()调用重新启动。
ContextClosedEvent通过使用ConfigurableApplicationContext接口上的close()方法或通过JVM关闭钩子关闭ApplicationContext时发布。在这里,“封闭”意味着所有单例Bean将被销毁。关闭上下文后,它将达到使用寿命,无法刷新或重新启动。
RequestHandledEvent一个特定于Web的事件,告诉所有Bean HTTP请求已得到服务。请求完成后,将发布此事件。此事件仅适用于使用Spring的DispatcherServlet的Web应用程序。
ServletRequestHandledEventRequestHandledEvent的子类,添加了特定于Servlet的上下文信息。

自定义事件案例:

public class BlockedListEvent extends ApplicationEvent {

    private final String address;
    private final String content;

    public BlockedListEvent(Object source, String address, String content) {
        super(source);
        this.address = address;
        this.content = content;
    }

    // accessor and other methods...
}

发布事件需要调用ApplicationEventPublisherpublishEvent() 方法,通常的做法是实现ApplicationEventPublisherAware接口,并将其注册为spring bean:

public class EmailService implements ApplicationEventPublisherAware {

    private List<String> blockedList;
    private ApplicationEventPublisher publisher;

    public void setBlockedList(List<String> blockedList) {
        this.blockedList = blockedList;
    }
      // 重写 setApplicationEventPublisher方法获取ApplicationEventPublisher
    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void sendEmail(String address, String content) {
        if (blockedList.contains(address)) {
            publisher.publishEvent(new BlockedListEvent(this, address, content));
            return;
        }
        // send email...
    }
}

在Spring容器的配置期间,检测到EmailService实现了ApplicationEventPublisherAware接口,会自动调用setApplicationEventPublisher()方法,Spring会自动填充其参数,所以我们可以通过ApplicationEventPublisher与Spring容器交互。

为了能监听ApplicationEvent事件,需要实现ApplicationListener并将其注册为Spring bean:

public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {

    private String notificationAddress;

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

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

ApplicationListener带有范型,意味着onApplicationEvent()方法不需要我们进行强转,我们可以注册许多监听器,但是默认情况下监听器是同步接受事件的,也就是说 publishEvent() 在直到所有监听器被执行完之前是一只阻塞的,好处就是能在同一线程中执行,并且如果存在事务环境,也能保持一致。如果需要使用其他策略,考虑 ApplicationEventMulticaster以及实现类 SimpleApplicationEventMulticaster

注册以上的bean进行使用:

<bean id="emailService" class="example.EmailService">
    <property name="blockedList">
        <list>
            <value>known.spammer@example.org</value>
            <value>known.hacker@example.org</value>
            <value>john.doe@example.org</value>
        </list>
    </property>
</bean>

<bean id="blockedListNotifier" class="example.BlockedListNotifier">
    <property name="notificationAddress" value="blockedlist@example.org"/>
</bean>

emailServicesendEmail() 被调用时,如果有黑名单中的邮件,则会发布BlockedListEventblockedListNotifier就会被通知

spring的事件机制的设计是被用来处理同一个ApllicationContext中的Bean进行交互的,如果需要更加复杂的交互能力需要使用Spring Integration ,能够构建轻量级、基于事件驱动的应用

基于注解的事件监听

Spring 4.2之后能够在任何Bean中的public方法上加上@EventListener注解完成事件监听,blockedListNotifier重新实现:

public class BlockedListNotifier {

    private String notificationAddress;

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

    @EventListener
    public void processBlockedListEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

如果方法需要监听多个事件,并且想使用无参方法,也可以将事件声明到注解中:

@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    // ...
}

还能够使用注解的condition属性进行事件过滤,并且是使用 SpEL expression (Spring表达式)条件配置:

@EventListener(condition = "#blEvent.content == 'my-event'") // 只有当BlockedListEvent事件中的content属性为my-event时才进行调用
public void processBlockedListEvent(BlockedListEvent blockedListEvent) {
    // notify appropriate parties via notificationAddress...
}

事件上下文环境可用的元数据信息:

NameLocationDescriptionExample
Eventroot object实际的 ApplicationEvent.#root.event 或者 event
Arguments arrayroot object用于调用方法的参数(作为对象数组)#root.args 或者 args;使用 args[0] 获取第一个参数
Argument nameevaluation context任何方法参数的名称。如果由于某种原因这些名称不可用(例如,由于在编译的字节码中没有调试信息),则也可以使用#a <#arg>语法(其中<#arg>代表参数索引(从0开始)。#blEvent 或者 #a0 (也可以使用 #p0 或者 #p<#arg> 表示法作为别名)

#root.event能获取到底层的事件,即使监听方法签名参数接收的是自定义事件。

在处理完一个事件后,如果需要继续发布事件,只需要修改方法返回值即可:

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

但是此用法不支持异步调用监听器

这个方法在美处理完一个BlockedListEvent事件后发布ListUpdateEvent事件,如果需要发布多个事件可以返回事件的Collection

异步监听器

只需在监听方法上加 @Async 注解就能 完成异步监听:

@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
    // BlockedListEvent is processed in a separate thread
}

使用异步监听器的局限性

  • 如果异步监听器抛出异常,并不会被传递到调用者,详情参考AsyncUncaughtExceptionHandler
  • 异步监听器方法不支持上述发布连续的事件,如果需要进行发布连续事件,需要注入 ApplicationEventPublisher 进行手动发布

监听器的执行顺序

添加@Order注解进行顺序编排:

@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress...
}

通用事件

在某些情况下,如果所有事件都遵循相同的结构,这可能会变得很通用性(就像前面示例中的事件一样)。 在这种情况下,可以实现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()));
    }
}

使用 EntityCreatedEvent<T> 构造通用的事件,其中T是实际的事件实体,比如:只接收EntityCreatedEvent事件的监听器:

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

这不光对ApplicationEvent有效,而且可以是任意事件对象

便捷的资源获取方式

Spring将应用资源抽象为Resource接口以及通过ResourceLoader接口进行加载得到。并且ApplicationContext实现了ResourceLoader接口,Resource抽象相当于是增强版的 JDK java.net.URL 类,实际上,Resource的实现包装了java.net.URL的实例,Resource获取几乎任意位置的资源,包括类路径,文件系统,以及使用标准URL描述的资源。

可以通过实现ResourceLoaderAware接口,并注入到容器中,在初始化时期就能被回掉,获取到ResourceLoader实例(通常就是ApplicationContext

ApplicationContext的构造器通过接收资源的地址字符串,其需要依赖具体的上下文环境进行加载,比如,ClassPathXmlApplicationContext将其识别为类路径下,也可以使用特定的前缀进行指定,强制其加载为类路径或URL,而不管是什么具体的上下环境。


文章作者: Ubi-potato
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Ubi-potato !
评论
  目录