Spring核心——全局事件管理

Easter79
• 阅读 500

_ApplicationContext_是一个_Context_策略(见上下文与IoC),他除了提供最基础的_IoC_容器功能,还提供了MessageSource实现的国际化、全局事件、资源层级管理等等功能。本文将详细介绍Spring核心模块的事件管理机制。

_Spring_核心模块的事件机制和常规意义上的“事件”并没有太大区别(例如浏览器上的用户操作事件)都是通过订阅/发布模式实现的。

_Spring_事件管理的内容包括标准事件、自定义事件、注解标记处理器、异步事件处理、通用实体包装。下面将通过几个例子来说明这些内容,可执行代码请到本人的gitee库下载,本文的内容在包_chkui.springcore.example.javabase.event_中。

我们都知道在订阅/发布模式中至少要涉及三个部分——发布者(_publisher_)、订阅者(_listener/subscriber_)和事件(_event_)。针对这个模型Spring也提供了对应的两个接口——_ApplicationEventPublisher、ApplicationListener_以及一个抽象类_ApplicationEvent_。基本上,要使用_Spring_事件的功能,只要_实现/继承_这这三个_接口/抽象类_并按照Spring定好的规则来使用即可。掌握这个原则那么接下来的内容就好理解了。

标准事件

_Spring_为一些比较常规的事件制定了标准的事件类型和固定的发布方法,我们只需要定制好订阅者(_listener/subscriber_)就可以监听这些事件。

先指定2个订阅者:

package chkui.springcore.example.javabase.event.standard;
public class ContextStartedListener implements ApplicationListener<ContextStartedEvent> {
    @Override
    public void onApplicationEvent(ContextStartedEvent event) {
        System.out.println("Start Listener: I am start");
    }
}

package chkui.springcore.example.javabase.event.standard;
public class ContextStopListener implements ApplicationListener<ContextStoppedEvent> {
    @Override
    public void onApplicationEvent(ContextStoppedEvent event) {
        System.out.println("Stop Listener: I am stop");
    }
}

 然后运行使用他们:

package chkui.springcore.example.javabase.event;
@Configuration
public class EventApp {

    @Bean
    ContextStopListener contextStopListener() {
        return new ContextStopListener();
    }
    
    @Bean
    ContextStartedListener contextStartedListener() {
        return new ContextStartedListener();
    }
    
    public static void main(String[] args) {
        ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(EventApp.class);
        //发布start事件
        context.start();
        //发布stop事件
        context.stop();
        //关闭容器
        context.close();
    }
}

在例子代码中,_ContextStartedListener_和_ContextStopListener_类都实现了ApplicationListener接口,然后通过_onApplicationEvent_的方法参数来指定监听的事件类型。在_ConfigurableApplicationContext_接口中已经为“start”和“stop”事件提供对应的发布方法。除了_StartedEvent_和_StoppedEvent_,_Spring_还为其他几项操作提供了标准事件:

  1. ContextRefreshedEvent:ConfigurableApplicationContext::refresh方法被调用后触发。事件发出的时机是所有的后置处理器已经执行、所有的Bean已经被加载、所有的ApplicationContext接口方法都可以提供服务。
  2. ContextStartedEvent:ConfigurableApplicationContext::start方法被调用后触发。
  3. ContextStoppedEvent:ConfigurableApplicationContext::stop方法被调用后触发。
  4. ContextClosedEvent:ConfigurableApplicationContext::close方法被调用后触发。
  5. RequestHandledEvent:这是一个用于Web容器的事件(例如启用了DispatcherServlet),当接收到前端请求时触发。

自定义事件

除了使用标准事件,我们还可以定义各种各样的事件。实现前面提到的三个接口/抽象类即可。

继承_ApplicationEvent_实现自定义事件:

package chkui.springcore.example.javabase.event.custom;
public class MyEvent extends ApplicationEvent {

    private String value = "This is my event!";
    
    public MyEvent(Object source,String value) {
        super(source);
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}

定义事件对应的_Listener_:

package chkui.springcore.example.javabase.event.custom;
public class MyEventListener implements ApplicationListener<MyEvent> {
    public void onApplicationEvent(MyEvent event) {
        System.out.println("MyEventListener :" + event.getValue());
    }
}

然后通过_ApplicationEventPublisher_接口发布事件:

package chkui.springcore.example.javabase.event.custom;
@Service
public class MyEventService implements ApplicationEventPublisherAware {
    private ApplicationEventPublisher publisher;
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        publisher = applicationEventPublisher;
    }

    public void publish(String value) {
        publisher.publishEvent(new MyEvent(this, value));
    }
}

使用@EventListener实现订阅者

在_Spring Framework4.2_之后可以直接使用_@EventListener_注解来指定事件的处理器,我们将上面的_MyEventListener_类进行简单的修改:

package chkui.springcore.example.javabase.event.custom;
public class MyEventListenerAnnotation{
    @EventListener
    public void handleMyEvent(MyEvent event) {
        System.out.println("MyEventListenerAnnotation :" + event.getValue());
    }
}

使用_@EventListener_可以不必实现_ApplicationListener_,只要添加为一个_Bean_即可。_Spring_会根据方法的参数类型订阅对应的事件。

我们也可以使用注解指定绑定的事件:

package chkui.springcore.example.javabase.event.custom;
public class MyEventListenerAnnotation{
    @EventListener(ContextStartedEvent.class})
    public void handleMyEvent() {
        //----
    }
}

 还可以指定一次性监听多个事件:

package chkui.springcore.example.javabase.event.standard;
public class MultiEventListener {
    @EventListener({ContextStartedEvent.class, ContextStoppedEvent.class})
    @Order(2)
    void contenxtStandadrEventHandle(ApplicationContextEvent event) {
        System.out.println("MultiEventListener:" + event.getClass().getSimpleName());
    }
}

注意上面代码中的_@Order注解,同一个事件可以被多个订阅者订阅。在多个定于者存在的情况下可以使用@Order_注解来指定他们的执行顺序,数值越小越优先执行。

EL表达式设定事件监听的条件

通过注解还可以使用_Spring_的_EL_表达式来更细粒度的控制监听的范围,比如下面的例子仅仅当事件的实例中MyEvent.value == "Second publish!"才触发处理器:

事件:

package chkui.springcore.example.javabase.event.custom;
public class MyEvent extends ApplicationEvent {
    private String value = "This is my event!";
    public MyEvent(Object source,String value) {
        super(source);
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}

通过EL表达式指定监听的数据:

package chkui.springcore.example.javabase.event.custom;
public class MyEventListenerElSp {
    @EventListener(condition="#p0.value == 'Second publish!'")
    public void handleMyEvent(MyEvent event) {
        System.out.println("MyEventListenerElSp :" + event.getValue());
    }
}

这样,当这个事件被发布,而且其中的成员变量value值等于"Second publish!",对应的MyEventListenerElSp::handleMyEvent方法才会被触发。EL表达式还可以使用通配符等等丰富的表现形式来设定过滤规则,后续介绍EL表达式时会详细说明。

通用包装事件

Spring还提供一个方式使用事件来包装实体类,起到传递数据但是不用重复定义多个事件的作用。看下面的例子。

我们先定义2个实体类:

package chkui.springcore.example.javabase.event.generics;
class PES {
    public String toString() {
        return "PRO EVOLUTION SOCCER";
    }
}
class WOW {
    public String toString() {
        return "World Of Warcraft";
    }
}

定义可以用于包装任何实体的事件,需要实现ResolvableTypeProvider接口:

package chkui.springcore.example.javabase.event.generics;
public class EntityWrapperEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {

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

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

}

订阅者可以根据被包裹的entity的不同来监听不同的事件:

package chkui.springcore.example.javabase.event.generics;
public class EntiryWrapperEventListener {
    @EventListener
    public void handlePES(EntityWrapperEvent<PES> evnet) {
        System.out.println("EntiryWrapper PES: " +  evnet);
    }
    @EventListener
    public void handleWOW(EntityWrapperEvent<WOW> evnet) {
        System.out.println("EntiryWrapper WOW: " +  evnet);
    }
}

上面的代码起到最用的主要是ResolvableType.forInstance(getSource())这一行代码,getSource()方法来自于EventObject类,它实际上就是返回构造方法中super(entity)设定的entity实例。

写在最后的

订阅/发布模式是几乎所有软件程序都会触及的问题,无论是浏览器前端、还是古老的winMFC程序。而在后端应用中,对于使用过MQ工具或者Vertx这种纯事件轮询驱动的框架码友,应该已经请清楚这种订阅/发布+事件驱动的价值。它除了能够降低各层的耦合度,还能更有效的利用多线程而大大的提执行效率(当然对开发人员的要求也会高不少)。

对于Spring核心框架来说,事件的订阅/发布只是IoC容器的一个附属功能,Spring的核心价值并不在这个地方。Spring的订阅发布功能在实现层面至少现在并没有使用EventLoop的方式,还是类与类之间的直接调用,所以在性能上是完全无法向Vertx看齐的。不过Spring事件的机制还是能够起到事件驱动的效果,可以用来全局控制一些状态。如果选用Spring生态中的框架(boot等)作为我们的底层框架,现阶段还是应该使用IoC的方式来组合功能,而事件的订阅/发布仅仅用于辅助。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
Easter79
Easter79
Lv1
今生可爱与温柔,每一样都不能少。
文章
2.8k
粉丝
5
获赞
1.2k