ApplicationContext的额外功能

ApplicationContext就是核心容器,它有许多额外功能。

org.springframework.context包提供了更多的功能:

  • 通过MessageSource接口实现国际化。
  • 通过ResourceLoader接口存取像URLs、文件之类的资源。
  • 通过ApplicationEventPublisher接口发布事件给实现ApplicationListener接口的bean。
  • 通过HierarchicalBeanFactory接口读取多个具有层级关系的context。

使用MessageSource进行国际化:

ApplicationContext接口extends了一个名叫MessageSource的接口,以提供国际化支持。
Spring还提供了HierarchicalMessageSource接口用于解析层次的message。

这些接口内的部分方法:

String getMessage(String code, Object[] args, String default, Locale loc);
String getMessage(String code, Object[] args, Locale loc);
String getMessage(MessageSourceResolvable resolvable, Locale locale);

当ApplicationContext被装载时,它会自动搜索一个名为messageSource的MessageSource类型bean。
如果找不到,ApplicationContext就会在父级容器里寻找,如果还是找不到就生成一个空的DelegatingMessageSource实例,用于处理接口提供的方法。

Spring提供三种MessageSource实现:ResourceBundleMessageSource、ReloadableResourceBundleMessageSource和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>

上面的配置假定在classpath上有三个资源包,名字分别是format、exceptions和windows。
解析消息是用一种JDK标准的方式,通过ResourceBundle对象完成。例如:

# in format.properties
message=Alligators rock!
# in exceptions.properties
argument.required=The {0} argument is required.

由于ApplicationContext实现也实现了MessageSource,所以ApplicationContext可以转换成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);
}

例如将参数传递给message:

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.

考虑到国际化,Spring的各种MessageSource实现都和标准JDK的ResourceBundle有相同的解析和回调规则。

比如要使用英国本地化设置(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引用。

注意:Spring的MessageSource基于Java的ResourceBundle,如果basename相同,并不会自动迁移,而是使用第一个发现的bundle,剩下的忽略。

作为ResourceBundleMessageSource的替代,Spring提供了ReloadableResourceBundleMessageSource类,比标准JDK更有拓展性,允许从任意Spring资源目录读取文件,而且支持热更新。

标准/定制事件:

事件处理通过ApplicationEvent类和ApplicationListener接口实现。
如果一个bean实现了ApplicationListener接口而且被部署进容器,那么每次ApplicationEvent被发布到ApplicationContext内,这个bean都会被提醒。标准的观察者模式。

Spring内置事件:

ContextRefreshedEvent当ApplicationContext被初始化或者手动调用refresh()方法。
初始化意思是所有的singleten作用域bean都被装载,
post-processor都被检测并装载,ApplicationContext可用。
ContextStartedEvent当ApplicationContext调用了start()方法,started意思是所有
Lifecycle bean都收到明确的start信号,通常用于重启bean。
ContextStoppedEvent当ApplicationContext调用了stop(),stopped意思是所有
Lifecycle bean都收到明确的stop信号,可被start()重启。
ContextClosedEvent当ApplicationContext调用了close()或者通过JVM shutdown hook。
closed意思是所有的singleton作用域bean将会被销毁,而且无法重启。
RequestHandledEventweb专用事件,表示服务了一个HTTP请求,在请求完成后发布。
ServletRequestHandledEventRequestHandledEvent的子类,添加了Servlet专用上下文信息。

继承Spring的ApplicationEvent类可以实现自定义事件:

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;
    }
    // 其它内容
}

创建一个类,实现ApplicationEventPublisherAware接口,并注册为bean,调用ApplicationEventPublisher的publishEvent()方法发布定制的事件:

public class EmailService implements ApplicationEventPublisherAware {

    private List<String> blockedList;
    private ApplicationEventPublisher publisher;

    public void setBlockedList(List<String> blockedList) {
        this.blockedList = blockedList;
    }

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

在配置时,容器检测到实现ApplicationEventPublisherAware接口,会自动调用setApplicationEventPublisher()方法。在现实中,传入的参数就是容器自己。

要接收自定义事件,需要创建一个类,实现ApplicationListener接口,并注册成bean:

public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {

    private String notificationAddress;

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

    public void onApplicationEvent(BlockedListEvent event) {
        // 一顿操作...
    }
}

由于ApplicationListener是泛型接口,所以需要传入类型信息,onApplicationEvent方法是类型安全的。

例子在这里。可以想注册几个事件监听器就注册几个。

默认,事件监听器以同步方式接收事件,单线程运行,所以是线程安全的,一个优势是当监听器接收到一个事件,如果事务可用,它就在发布者的transaction context内运行。

Spring的事务机制设计用于简单的容器内bean之间的通信。用于企业级应用可以考虑使用Spring Integration项目。

基于注解的事件监听

将bean定义内的方法注册成一个事件处理器,使用@EventListener注解:

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() {
    // ...
}

可以通过@EventListener的condition属性声明SpEL添加一个运行时过滤器:

@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blEvent) {
    // notify appropriate parties via notificationAddress...
}

上面例子的意思是当blEvent对象的content属性为my-event时才生效。

SpEL可用的元数据:

名称位置描述例子
Eventroot对象实际的ApplicationEvent类型#root.event or event
参数数组root对象调用方法的参数#root.args or args
args[0]访问第一个参数,以此类推
参数名要被测试的context方法参数的名称上面的#blEvent

注意#root.event可以访问到底层事件,即使方法签名是别的对象的引用。

如果想在方法结束后触发一个事件,只要设置方法返回一个事件类型就行:

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

注意,不支持asynchronous listeners。

可以通过返回Collection或者一个数组来发送多个事件。

异步监听器

添加@Async注解即可:

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

注意有一些限制:

  • 如果异步事件监听器抛出异常,是不会传播给调用者的。
  • 不能通过方法返回事件类型发布事件。

给监听器排序:

比如在一个监听器之前调用另外一个监听器,使用@Order注解:

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

便捷访问底层资源

一个容器就是一个ResourceLoader,可以载入Resource对象。
Resource类型就是功能更丰富的JDK java.net.URL类型。
事实上也是,Resource类内部包装了一个java.net.URL实例。

Resource可以透明地访问几乎任何位置的底层资源,
像classpath、文件系统位置等可以用标准URL或它的变种表述的任何位置。
如果资源位置是一个简单的字符串,没有特殊的前缀,那么资源从哪里来取决于具体的容器类型。

bean可以通过实现ResourceLoaderAware接口,在初始化时将ApplicationContext转换成ResourceLoader并注入bean。可以将Resource属性声明为String,可以自动转换。

传递给ApplicationContext构造器的位置或者路径实际上都是路径字符串,以简单的方式被容器正确处理。

可以在路径字符串内添加特定的前缀来强制从什么地方载入定义,如classpath:。

应用启动过程跟踪

复杂的程序有复杂的启动过程,追踪启动过程可以帮助分析启动时间花在什么地方,
也可以帮助理解分析整个容器的生命周期。

AbstractApplicationContext和其子类有ApplicationStartup实例,收集启动阶段的StartupStep数据:

  • 容器生命周期阶段;
  • bean生命周期阶段;
  • 应用事件处理;

装备在AnnotationConfigApplicationContext内的例子:

// 创建一个启动step并开始记录
StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan");
// 在当前step添加标记
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// 执行装配的实际阶段
this.scanner.scan(basePackages);
// 结束当前step
scanPackages.end();

容器已经装配了多个步骤,当开始记录时,这些步骤都可以被相应的工具收集、显示和分析。

默认的ApplicationStartup实现是一个没有操作的变种,是为了最小的开销。也就是默认不会收集启动步骤。Spring自带了一个实现:FlightRecorderApplicationStartup。要使用这个变种就要在容器创建之后尽快将它配置进容器。

ApplicationStartup仅用于应用启动阶段,并且仅用于核心容器,而不是取代别的java测量库。

要开始收集定制的StartupStep,组件可以实现ApplicationStartupAware接口直接获得ApplicationStartup实例或者在注入点请求ApplicationStartup类型。

注意不能使用”spring.*”作用域,这是Spring保留的,用于Spring内部使用。

留下评论

您的邮箱地址不会被公开。 必填项已用 * 标注