Spring基础十一:ApplicationContext的附加功能

ApplicationContext除了作为容器,负责bean的初始化和管理,还将一些有用功能模块集成到framework里:

1、i18n风格的文本的国际化,通过MessageSource接口;
2、容器内的事件发布(ApplicationEventPublisher)订阅(ApplicationListener);
3、访问url和文件资源,通过ResourceLoader接口;
4、多个context组成一个父子层级结构,将不同bean划分到不同的context,对系统进行分层;

国际化

Spring以MessageSource接口为核心,定义了一套为不同语言地区(locale)提供本地化文本消息的机制。

MessageSource接口

MessageSource的主要方法如下:

  1. String getMessage(String code, Object[] args, String default, Locale loc): 最基本的提取文本消息方法,传入的参数依次为:消息编号,参数列表,默认消息(如果MessageSource没有定义该消息),语言地区;消息定义可以包含参数占位符,通过JDK的MessageFormat功能进行参数值替换;
  2. String getMessage(String code, Object[] args, Locale loc):和上面的类似,区别在于没有默认消息,如果MessageSource没有定义该消息,则抛出NoSuchMessageException异常;
  3. String getMessage(MessageSourceResolvable resolvable, Locale locale):将1的参数封装在MessageSourceResolvable里面;

ApplicationContext本身扩展了MessageSource,但是并不直接实现具体功能,而是委托给另外一个实现MessageSource接口的bean。

MessageSource Bean

当ApplicationContext加载的时候,会搜寻一个实现了MessageSource的Bean,而且这个Bean的名字必须叫做messageSource(这个名字在Spring的源码里面是硬编码的)。如果找到了这个bean,那么所有对ApplicationContext相关方法的调用,都会转发给这个bean。

如果没有找到这个bean,那么就去它的父context去寻找。如果还是没找到,就创建一个DelegatingMessageSource(空的MessageSource实现)来接受调用。

MessageSource实现

Spring提供了两个实现,ResourceBundleMessageSourceStaticMessageSource。其中StaticMessageSource可以通过编程方式添加消息,在实际项目中很少使用。这里主要介绍ResourceBundleMessageSource。

<beans>
    <bean id="messageSource"
            class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames">
            <list>
                <value>format</value>
                <value>exceptions</value>
            </list>
        </property>
    </bean>
</beans>

ResourceBundleMessageSource从文件资源加载消息,上面引用两个消息源:format和exceptions。每个消息资源其实就是位于classpath的属性文件:

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

有了上面的配置,下面代码打印字符串Alligators rock!

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); 
}

多语言

关于国际化文本消息的加载和解析,Spring采用与JDK ResourceBundle一致的规则。

在上面的示例中,如果添加针对British的消息源,只要添加format_en_GB.properties和exceptions_en_GB.properties即可。比如

# in exceptions_en_GB.properties
argument.required=Ebagum lad, the {0} argument is required, I say, required.

下面的代码打印的是Ebagum lad, the 'userDao' 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);
}

如果某个消息的指定语言版本没有定义,那么Spring会返回默认版本,关于默认版本的规则,文档没有写明,待验证。

事件机制

Spring的事件继承自ApplicationEvent,在ApplicationContext的生命周期内,会发布很多事件,如果客户代码想接受这些事件,实现ApplicationListener即可。

Spring内置事件

Spring的内置事件有以下这些:

Event描述
ContextRefreshedEventApplicationContext初始化完成或者刷新(调用了ConfigurableApplicationContext.refresh方法),这里的"初始化完成"意味着,所有的bean定义被加载,post-processor被激活,单例bean完成了初始化,且ApplicationContext自身可用。在Context关闭之前,refresh事件可能发生多次,因为有些context比如ApplicationContext支持hot refresh,而GenericApplicationContext 不支持
ContextStartedEvent当调用ConfigurableApplicationContext.start方法时被发布,stated意味着所有Lifecyle的实现者收到start信号,由于Context可以被反复start和stop,因此该事件也可能发生多次
ContextStoppedEvent当调用ConfigurableApplicationContext.stop方法时被发布,stop意味着所有Lifecyle的实现者收到stop信号
ContextClosedEventApplicationContext被关闭,要么调用了ConfigurableApplicationContext.close方法,要么进程关闭且Context注册了JVM的shutdown hook;context一旦被关闭,所有的单例bean被销毁,不可能再被start或refresh
RequestHandledEvent这个事件意味着一个Http请求处理完成,Spring Web MVC中使用
ServletRequestHandledEventRequestHandledEvent的子类,添加了一些Servlet上下文字段

自定义事件

用户代码可以创建并发布自定义事件,请看示例:

public class BlackListEvent extends ApplicationEvent {

    private final String address;
    private final String content;

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

public class EmailService implements ApplicationEventPublisherAware {

    private ApplicationEventPublisher publisher;

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void sendEvent(String address, String content) {
			publisher.publishEvent(new BlackListEvent(this, address, content));
    }
}

创建一个自定义事件只要继承ApplicationEvent即可,要发布事件则要拿到ApplicationEventPublisher引用,通过ApplicationEventPublisherAware接口,或直接@Autowire。

监听事件

一个bean要监听Spring事件,实现ApplicationListener接口即可:

public class EventListener implements ApplicationListener<ContextRefreshedEvent> {

    public void onApplicationEvent(ContextRefreshedEvent event) {
        //do something
    }
}

通过注解来监听

public class EventListener {

	@EventListener
    public void processEvent(ContextRefreshedEvent event) {
        //do something
    }
}

这种方式不需要实现ApplicationListener接口,事件的类型是通过方法的参数类型来指明。

如果需要监听不止一种事件,可以添加注解参数来指明要监听的事件类型,此时方法参数也可以省去:

public class EventListener {

	@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
    public void processEvent() {
        //do something
    }
}

@EventListener注解还可以通过Spel表达式来加一些过滤条件,比如:

@EventListener(condition = "#blEvent.content == 'my-event'")
public void processEvent(BlackListEvent blEvent) {
}

condition表达的意思相当于:content属性等于’my-event’。关于Spel表达式由于实际用的很少,这块就不展开了。

监听方法返回值

事件处理方法可以返回另外一个事件,Spring会在处理完当前事件后,发布返回的事件:

@EventListener
public ListUpdateEvent processEvent(BlackListEvent event) {
}

监听方法甚至还可以返回一个事件集合,来发布多个事件。

异步监听器

Spring的event publish&listener默认是同步实现的,也即publish方法调用,要在所有的listener执行完毕后才返回,利用Spring的Async实现异步监听器。

@EventListener
@Async
public ListUpdateEvent processEvent(BlackListEvent event) {
}

Async机制本质上和Event机制没有什么关联,是两个平行的功能,但是异步方式会有以下两个影响:

  1. 监听方法抛出异常,不会被调用者捕获;
  2. 异步监听方法无法通过返回值来发布另外一个事件

监听器顺序

使用@Order注解,可以添加相对顺序

@EventListener
@Order(42)
public ListUpdateEvent processEvent(BlackListEvent event) {
}

泛型事件

比如EntityCreatedEvent事件,包含了所创建entity的类型参数,而下面的监听器只监听Person类型的创建:

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

但是,由于java泛型的类型擦除机制,只有事件对象能在运行时保留类型信息时才能工作,比如class PersonCreatedEvent extends EntityCreatedEvent<Person> { …​ }。因此个人觉得不要依赖这个机制为妙。

加载资源

Spring定义了一套加载底层资源的机制,核心接口包括ResourceLoader和Resource,用来取代java.net.URL。 这个机制本身比较复杂,单独用一个章节(下一章)来讲解。

父子Context

在同一个进程里面,可以创建多个ApplicationContext,每个Context包含一组bean,对系统进行横向或纵向的拆分。Spring Web MVC架构使用这种方式,将系统分成了web层和逻辑层,前者主要包含处理http请求的Controller,后者包含DAO和Service等。

Context之间可以形成父子关系,子Context访问父Context的bean,反之则不成立。

下面通过一个简单的示例来展示这种关系:有两个bean,ServiceBean和DaoBean,前者依赖后者。我们定义了两个@Configuration类,分别用于parent和child context。

@Configuration
public class ParentConfig {
    @Bean
    public ServiceBean serviceBean(DaoBean daoBean) {
        return new ServiceBean(daoBean);
    }
    @Bean
    public DaoBean daoBean() {
        return new DaoBean();
    }
    @Bean
    public BeanTracePostProcessor postProcessor() {
        return new BeanTracePostProcessor();
    }
}
@Configuration
public class ChildConfig {
    @Bean
    public ServiceBean serviceBean(DaoBean daoBean) {
        return new ServiceBean(daoBean);
    }
}

然后创建ApplicationContext如下:

public class Main {
    public static void main(String[] args) {

        AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(ParentConfig.class);
        AnnotationConfigApplicationContext child = new AnnotationConfigApplicationContext();
        child.setParent(parent);
        child.register(ChildConfig.class);
        child.refresh();

        parent.getBean(ServiceBean.class).work();
        child.getBean(ServiceBean.class).work();
    }
}

我们可以发现,child context定义的ServiceBean可以完整正常工作,使用了parent context里面的DaoBean。如果将DaoBean的定义移至ChildConfig,则parent context初始化会抛异常。

BeanPostProcessor只对所属的context有效,不会影响它的parent或child context。完整的示例代码在这里,见子工程hierarchy。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值