事件驱动-观察者/发布订阅模式到Listner的实现

一、事件驱动

1.1 概念和介绍

事件驱动的概念

事件驱动:事件驱动是指,某件事物跟随当前事件的时间点发动,或者说,是当前时间点发生的某件事物触发了另外一件事物的发生,从而调动可用的资源,执行相关任务,防止事物堆积。

例如:当你订购了火车票,手机接收到短信提示;你点击页面按钮,触发相关的内容页面改变等等。

ps: 事件驱动模型只是个抽象模型,具体的实现驱动系统由外部系统根据具体的业务场景进行建模设计或者具象,例如接下来要讲的设计模式或Spring中的事件监听机制等。


1.2 实现思路

事件驱动有很多的实现形式,如简单的 事件触发机制单线程异步任务多线程异步任务 ,复杂的有各种 消息中间件 等等。各种技术实现事件驱动的思路基本相同,事件驱动四个基本要素:事件生产方、事件、事件消费方、事件管理器

在这里插入图片描述

  • 事件生产方(事件源Source):顾名思义,就是事件的生产者。任何一个事件都有事件源,比如前段点击按钮,事件源就是input框或者button按钮;spring中请求处理完成的事件源就是DispatcherServlet;Spring容器刷新完的事件源就是ApplicationContext。

  • 事件(Event)具体发生的事情,比如Spring容器刷新完后;点击按钮后等等。

  • 事件消费方(事件监听器 Listener): 在事件源发生事件后,监听该行为的一方.可以在该处做相关的逻辑处理,比如当容器刷新后,做什么业务处理呀,当按钮点击后,弹出什么弹框内容或者填充什么数据之类。

  • 事件管理器(事件广播器 Pushlish): 主要的作用是派发事件。是沟通事件Event发生后和事件消费方Listener的桥梁。其也可以在事件源内部实现手动去回调Listener方法,所以可有可无。

1.3 解决的问题

基于事件驱动模型可以 实时观察所关心事件的状态变化,从而做出相关的响应行为。当然这只是事件驱动模型最基础的作用,在一些复杂的系统中,事件驱动还能发挥以下的作用:

1.实现异步任务

在一些业务场景中,同步、顺序阻塞的执行会造成系统耗时较大,如账号注册成功后给邮箱和手机号发送成功通知,本身发邮件和发短信的顺序不会影响到整个业务。如果能够异步,关注注册成功这个事件(Event),让发送邮件和短信来消费这个事件,耗时会大大的减少。因此遇到 耗时较大,且没有同步执行要求的操作时,可以考虑该模型

2 跟踪状态的变化

在一些业务场景中,某些业务存储实体的状态变化是我们所关心的, 比如报销单的单据状态,是自由还是已结算,如果是已结算,那么就推送生成对应的应收单和应付单。但是我们没办法从系统全局去控制所有人在改变报销单状态的时候,去生成对应的应收单和应付单。这时事件驱动模型就很好的为我们解决了这类业务,我们 只需要提供一个改变报销单状态的接口, 所有人去调用该接口改变报销单状态,只要调用了这个接口,我们就可以触发一个自定义事件(Event),并把整个报销单当做数据源传递给监听该事件的监听者,去做相应的业务处理。

当然啦,你也许会说直接在改变状态后直接去生成应收应付单不也可以吗?如果你非要这么做,也是可以实现的,只是前者写成事件驱动Listener和event形式,可以实现可插拔,易更改,松耦合。后者从代码设计的优美度和系统耦合度上面就有些差劲人意了。

3 实现组件的松耦合,解耦

在复杂的业务系统中,各个组件之间肯定存在着一些交互,比如采购模块,新加一张采购单,当采购单生效后,入库模块应该新增一条入库记录,这时 我们可以将这两个模块组件之间的依赖关系,抽象成事件,从而达到组件模块的松耦合,解耦

介绍完了事件驱动模型,下面会为为大家介绍一下这种模型在设计模式中对应的具体实践,以及在我们的 Java Web中和成熟的Spring框架中都有那些实际的应用从而可以加深大家的印象,以及对这种模型的认知。

二 观察者模式和发布订阅模式

前文有介绍了事件驱动模型,👇🏻 为大家介绍一下观察者模式和发布订阅模式,这两种模式都是基于事件驱动模型的实践

2.1 模式介绍

观察者模式

观察者(observer)是 **直接监听(listener)事件源(source),在事件源内部往往会记录 下观察者列表 通过观察者列表去显示调用观察者回调方法

一般的实现形式是在事件源内部,自身的某些需要被监听的方法,执行完后,生成对应的 Event 事件 。自己遍历对应的监听列表回调观察者方法(例如后文说到的 java-listener种 event和listener的实现),或者通过广播器去触发事件(例如 后文说到的spring-listener 中 event和listener的实现),从而实现只要触发了subject内指定的 event,就回调观察者方法。

在这里插入图片描述

发布订阅模式

订阅者(Subscriber)将自己的订阅信息(Subscribe)告诉调度中心(Topic), 在调度中心内部记录订阅者列表, 发布者(Publisher)将要发布的信息(Message) 发布到调度中心,然后在调度中心内部确认该信息确定要发布后,遍历订阅者列表,触发订阅者的回调事件。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ukaqweee-1669629400461)(/Users/tinghuiliu/Library/Application Support/typora-user-images/image-20221125235528860.png)]

从以上描述你应该可以看出这两种设计模式的差异了吧, 观察者模式较于观察者模式,多了一个 调度中心(Topic) 这个”中间商“发布者和订阅者都是通过这个 调度中心来完成消息通知的。且观察者模式是知道有观察者存在的,而发布订阅者模式,发布者不知道订阅者的存在

2.2 使用场景的区别

观察者模式发布订阅模式
应用场景多用于单个应用内部,是一种松耦合关系更多用于跨应用的模式,比如消息中间件,是一种完全解耦的关系
触发模式大多数时候是同步的,例如当事件触发,去调用观察者方法大多数时候是异步的,例如spring中的监听模式,也是通过线程池异步去通知监听事件触发,也比如消息中间件使用消息队列等
生产方和消费方的关系事件源知道观察者的存在发布者不知道订阅者的存在

尽管二者存在差异,但是它们要解决的问题是相同的,即在同一时间,某件事件发生时,另外的一端能够监听到,及时的做出相应的业务处理。所以也🙅🏻‍♀️太过纠结。

2.3 Observable 和 Observer

java.util包为我们提供了Observable类 Observer接口 方便我们实现简单的观察者和发布订阅模式。

如果我们用观察者模式的话:

被观察者只需要实现Observable类,观察者继承 Observer接口 即可

如果我们用发布订阅模式的话:

调度中心Topic则需实现 Observable类承担对观察者的一些操作,订阅者继承Observer接口 ,然后我们再自己写一个发布者用来给Topic发送消息

在Observable类中提供了以下方法

在这里插入图片描述

Observable类中包含了添加观察者(addObserver)、删除观察者(deleteObserver)、通知观察者 (notifyObservers),保存改变 setChanged等基本操作,并且其内部还包含了一个观察者集合。

Observable 源码如下:

    private Vector<Observer> obs;  //观察者集合

    /** Construct an Observable with zero Observers. */

    public Observable() {
        obs = new Vector<>(); //构造函数初始化时候,新建该集合
    }

	 //可以看到源码中添加观察者对象就是同步的状态下往集合中加值
    public synchronized void addObserver(Observer o) {
        if (o == null)
            throw new NullPointerException();
        if (!obs.contains(o)) {
            obs.addElement(o);
        }
    }

2.3.1 观察者模式示例

观察者模式示例代码:FatherObserver和MotherObserver观察BobySubject状态

定义一个BodySubject类

package com.springlistener.observer;

import lombok.Data;

import java.util.List;
import java.util.Observable;
import java.util.Observer;

/**
 * @Description 宝宝类,观察者father和mather观察宝宝状态,状态变更通知观察者
 * @Author CV大虾
 * @Date 2022/11/20 14:39
 */
@Data
public class BobySubject extends Observable {
    //宝宝状态
    private String state;


   //给boby添加father和mather的观察者对象
    public BobySubject(){
        this.addObserver(new FatherObserver());
        this.addObserver(new MatherObserver());
    }


    public void setState(String state) {
        this.state = state;
      	//调用Observable的改变方法 将该事件状态设置为已改变
        setChanged();
      //通知观察者(ps:可以去看看源码,其实就是调用了观察者的update方法,并且将change状态设置回了false)
        notifyObservers();
    }
}

观察者 FatherObserver和MatherObser

package com.springlistener.observer;

import java.util.Observable;
import java.util.Observer;

/**
 * @Description
 * @Author CV大虾
 * @Date 2022/11/20 14:38
 */
public class FatherObserver implements Observer {

  //重写update方法
    @Override
    public void update(Observable o, Object arg) {
        System.out.println("Father observable 收到宝宝的状态:" + ((BobySubject)o).getState());
    }
}

package com.springlistener.observer;

import java.util.Observable;
import java.util.Observer;

/**
 * @Description
 * @Author CV大虾
 * @Date 2022/11/20 14:38
 */
public class MatherObserver implements Observer {

    @Override
    public void update(Observable o, Object arg) {
        System.out.println("Mother observable 收到宝宝的状态:" +  ((BobySubject)o).getState());
    }
}

测试调用

class ObserverTest {

   @Test
   void testObserver(){
       BobySubject bobySubject = new BobySubject();

       bobySubject.setState("cry");
       bobySubject.setState2("happy");

   }
 }

/***输出结果为:
      Mother observable 收到宝宝的状态:cry
      Father observable 收到宝宝的状态:cry
*/

ps:思考一下,发布订阅模式发布者不知道订阅者的存在,应该怎么做呢?

提示一下,可以给MatherObserver和FatherObserver类新加一个有参构造方法,传入一个Observable作为调度中心,然后在方法内调用addObserver,把自己加入待通知列表中。


2.4 EventObject 和 EventListener

java 关于监听事件也为我们提供了** EventObject类 ** 和 EventListener 接口 ,我们同样可以实现观察者模式。

被观察者只需要继承 EventObject 类,观察者实现 EventListener 接口即可。

2.4.1 观察者模式示例

仍然以上文的业务场景为例,MatherListener和FatherListener 作为观察者,观察Boby对象的 BodyEvnet 事件

自定义接口 EventListener 继承 EventListener 接口

package com.lthcode.jdk8.jdklistener;

import java.util.EventListener;

public interface MyListener<T> extends EventListener {
    //自定义监听回调一个
      void   callBack(T event);
}

观察者 FatherListener

package com.lthcode.jdk8.jdklistener;

public class FatherListener implements MyListener<BobyEvent> {
    @Override
    public void callBack(BobyEvent event) {
        System.out.println("Father 收到通知, 宝宝状态为:"+ event.getState());
    }
}

观察者 MatherListener

package com.lthcode.jdk8.jdklistener;

public class MatherListener implements MyListener<BobyEvent> {
 @Override
 public void callBack(BobyEvent event) {
     System.out.println("Mather 收到通知,宝宝状态为: "+ event.getState());
 }
}

Boby

package com.lthcode.jdk8.jdklistener;

public class Body {
    //宝宝的状态
    private Object state;

    public Object getState() {
        return state;
    }

    public void setState(Object state) {
        this.state = state;
        //当宝宝状态改变时,生成一个事件,并且通知其观察者
        new BobyEvent(state).handle();
    }
}

Boby的事件

package com.lthcode.jdk8.jdklistener;

import java.util.ArrayList;
import java.util.EventObject;
import java.util.List;

public class BobyEvent extends EventObject {


    private List<MyListener> listener = new ArrayList<>();


    public BobyEvent(Object source) {
        super(source);
    }


    public void registerEventListener(MyListener eventListener){
        if (eventListener != null){
            listener.add(eventListener);
        }
    }

    public void handle(){
        //循环调用其内部的观察者列表的 callback 方法
        listener.stream().forEach(e->e.callBack(this));

    }
}

测试

public class ListnerDemo {

    @Test
    public void testEvent(){
        BobyEvent event = new BobyEvent(event);
        //添加观察者
        event.registerEventListener(new MatherListener());
        event.registerEventListener(new FatherListener());
        event.setState("cry");
    }
}

输出

Mather 收到通知,宝宝状态为: cry
Father 收到通知, 宝宝状态为:cry

2.5 发布订阅模式示例

下面自定义接口实现一下发布订阅模式,让大家体会一下发布订阅和观察者模式的区别吧

IPublisher 发布者接口

package com.executor.executor.mypush;

/**
 * @Description 发布者接口 (泛型为指定发布什么类型的消息)
 * @Author CV大虾
 * @Date 2022/11/25 11:14
 */
public interface IPublisher <P>{

    /**
     * @Description
     * @param topicBus 发布中心
     * @param message  要发布的消息
     * @param isSende  是否立即发送
     * @return
     */
    public void  publish(TopicBus topicBus,P message,boolean isSende);


}

ISubcriber 订阅者接口

package com.executor.executor.mypush;

/**
 * @Description 订阅者接口 (泛型为指定接收什么类型的消息)
 * @Author CV大虾
 * @Date 2022/11/25 11:15
 */
public interface ISubcriber<R> {

    /**
     * @Description
     * @param topicBus 发布中心
     * @return
     */
    public void subscribe(TopicBus topicBus);


    public void callBack(R message);

}

Publisher 发布者

package com.executor.executor.mypush;

/**
 * @Description  发布者 类似于观察者模式中的 事件源
 * @Author CV大虾
 * @Date 2022/11/25 11:51
 */
public class Publisher implements IPublisher<Message> {

    //发布者名字
    String name;

    Publisher(String name) {
        this.name  =name;
    }

    @Override
    public void publish(TopicBus topicBus, Message message,boolean issend) {
                topicBus.publish(name,message,issend);
    }
}

订阅者

package com.executor.executor.mypush;

/**
 * @Description 订阅者,类似于 观察者模式中的监听器
 * @Author CV大虾
 * @Date 2022/11/25 11:38
 */
public class Subcriber implements ISubcriber<Message> {

    //订阅者姓名
    private final String name;

    public Subcriber(String name){
        this.name = name;
    }

    @Override
    public void subscribe(TopicBus topicBus) {
            topicBus.addSubscriber(this);
    }

    /**
     * @Description  订阅者的回调方法
     * @param
     * @return
     */
    @Override
    public void callBack(Message message) {
        System.out.println(this.name +"  收到消息: "+ message.getPushName() +" 发布  "+ message.getMessage());

    }
}

TopicBus 发布中心

package com.executor.executor.mypush;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 * @Description 发布中心
 * @Author CV大虾
 * @Date 2022/11/25 11:19
 */
public class TopicBus {

    private  String name;

    public TopicBus(String name){
        this.name =name;
    }
    List<ISubcriber> subcriberList=new ArrayList<>();

    /**
     * @Description 添加订阅者
     * @param subcriber
     * @return
     */
    void addSubscriber(ISubcriber subcriber){
            subcriberList.add(subcriber);
    }

    void publish(String pulisherName,Object message,boolean isPublish){
        if(isPublish){       
           /**
             这里可以优化
             1 .将订阅者根据订阅消息的不同分组,然后将相同类型的消息 发送给相同类型的订阅者
             2. 这里可以用线程池实现异步 回调订阅者的回调方法
             **/
            if (message instanceof Message){ //判断消息类型
                //将发布者的名称和发布信息合并为创建一个消息
                Optional.ofNullable(subcriberList).ifPresent(e->
                        e.stream()
                                .forEach(sub->sub.callBack(new Message(pulisherName,((Message) message).getMessage())))
                );
            }
      
        }
    }
}

测试

package com.executor.executor.mypush;
import java.util.Optional;
/**
 * @Description 发布订阅测试类
 *            1. 发布订阅模式,发布者不知道订阅者的存在
 *            2. 发布者和订阅者基于发布器进行消息传递
 *            3. 发布者一旦发布消息,其订阅者都可以收到订阅消息
 * @Author CV大虾
 * @Date 2022/11/25 10:11
 */
public class PushSubcriTest {
    public static void main(String[] args) {
        TopicBus subscribePublish = new TopicBus("订阅器");
        IPublisher publisher1 = new Publisher("发布者1");
        ISubcriber subcriber1 = new Subcriber("订阅者1");
        ISubcriber subcriber2 = new Subcriber("订阅者2");
        subcriber1.subscribe(subscribePublish);
        subcriber2.subscribe(subscribePublish);
        publisher1.publish(subscribePublish, new Message("welcome"), true);
        publisher1.publish(subscribePublish,  new Message("to"), true);
        publisher1.publish(subscribePublish, new Message("hel"), false);
        publisher1.publish(subscribePublish, "22", false);
    }
}

三 Java-Web中Listener的使用

JavaWeb里面的listener就是通过观察者设计模式进行实现的。

有些同学可能会觉得都已经用Spring 和 SpringBoot 快速搭建web 应用了,为什么还要了解 Java-web 原生的Listener,我个人觉得,所有的知识点都是相辅相成的,只是现在一些成熟的框架,帮我们把一些原生的东西扩展,封装了,方便我们使用,但是为了我们的知识体系更加的完善,我个人觉得还是有必要了解这些。

3.1 Java-Web三大组件的介绍

Java Web三大组件分别是:Servelet、Filter、Listemer

Java-Web三大组件

组件作用
Listener事件监听机制:可以用于监听三大域对象
Filter一般用于完成通用的操作。如:登录验证、同意编码处理,敏感字符的过滤等
Servelet使用Java语言编写的服务端程序,可以用于生成动态的WEB内容

三大组件的初始化顺序

Listener > Filter > Servelet

三大组件的销毁顺序

Servlet > Filter > Listener

注意:这里说的初始化顺序,是在Listener组件监控的域对象是为ServletContext时,如果Listener组件监控的为HttpServletRequest对象,如ServeletRequestListener,那么初始化顺序,则不一定在Servlet前。

3.2 Java-Listener的监控对象

监听是编程语言中非常重要的一个环节,例如前端JavaScript的事件:onclick、onblur、onchange都是基于监听用户行为,触发的事件

Listener表示服务器的事件监听器,其作用用于监听三个域对象的状态(对象、对象的属性) 变化。

三个域对象分别是:

  • ServeletContext
  • HttpSession
  • HttpServletRequest

Java Web服务器端的事件监听器一共分为三大类,八个小类

ServeletContext监听HttpSession监听HttpServletRequest监听
ServletContextListenerHttpSessionListenerServeletRequestListener
ServletContextAttributeListnerHttpSessionActivationListenerServeletRequestAttributeListener
HttpSessionBindingListener
HttpSessionAtrributeListener

3.3 示例分析

以下我们就拿Listener监听ServletContext来举例分析,上面展示了,listenner是通过ServletContextListener和ServletContextAttributeListner 两个接口来监控ServletContext的,也可以说 servlet容器是通过Listener接口类将事件暴露给应用程序的。

先新建一个Web项目,然后再Web项目中新建Listener分别实现ServletContextListener 和 ServletContextAttributeListner

TestServletContextListner

package com.example.servlet01.listener;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

/**
 * @Description 用于监听ServletContext创建和销毁
 * @Author CV大虾
 * @Date 2022/11/20 21:02
 */
public class TestServletContextListner implements ServletContextListener {

   //监听ServletCntext创建
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("在ServletContext初始化时 调用了 TestServletContextListner " + sce);
    }
	
    //监听ServletContext销毁
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("在ServletContext 销毁时 调用了 TestServletContextListner " + sce);
    }
}

TestServletAttributeListner

package com.example.servlet01.listener;

import javax.servlet.ServletContextAttributeEvent;
import javax.servlet.ServletContextAttributeListener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

/**
 * @Description 监听ServletContext对象中属性的变化
 * @Author CV大虾
 * @Date 2022/11/20 21:02
 */
public class TestServletAttributeListner implements ServletContextAttributeListener {

    @Override
    public void attributeAdded(ServletContextAttributeEvent event) {
        System.out.println("TestServletAttributeListner 监控到 ServletContext 添加了属性 name :"+event.getName()
                +"  value :"+event.getValue());
    }

    @Override
    public void attributeRemoved(ServletContextAttributeEvent event) {
        System.out.println("TestServletAttributeListner 监控到 ServletContext 移除了属性 name :"+event.getName()
                +"  value :"+event.getValue());    }

    @Override
    public void attributeReplaced(ServletContextAttributeEvent event) {
      /**
          这里有个小坑,如果直接用event.getValue() 获取到的是改变前的值如果要获取到改变的值,
          如果要获取到改变后的值要重新从ServletContextevent中获取
      */
      System.out.println("TestServletAttributeListner 监控到 ServletContext 替换了属性 name :"+event.getName()
                +"  value :"+ event.getServletContext().getAttribute(event.getName()));

    }
}

Web.xml 中注册 listener

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

	  <!-- 当然你也可以使用@@WebListener 在监听类上完成注册W-->
    <listener>
        <listener-class>com.example.servlet01.listener.TestServletContextListner</listener-class>
    </listener>
    <listener>
        <listener-class>com.example.servlet01.listener.TestServletAttributeListner</listener-class>
    </listener>
</web-app>

自定义一个Servlet

package com.example.servlet01;

import java.io.*;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import javax.servlet.annotation.*;

//@WebServlet完成Servlet注册,当访问 /hellO-servlet 时会跳到该service
@WebServlet(name = "helloServlet", value = "/hello-servlet")
public class HelloServlet extends HttpServlet {
    private String message;


    public void init() {
        message = "Hello World!";
    }

    @Override
    public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //给servletContext 填充属性,看看时候能被listnner监控捕获到
        getServletContext().setAttribute("message",message);
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        out.println("<html><body>");
        out.println("<h1>" + message + "</h1>");
        out.println("</body></html>");
        //修改原有的servletContext属性值,看看时候能被listnner监控捕获到
        getServletContext().setAttribute("message","更改值messgae");


    }

    public void destroy() {
    }
}

当web项目启动后,可以我们的 contextInitialized 和 attributeAdded监控方法被调用

当我们访问 自定义 HelloServlet 这个servlet 时

在这里插入图片描述

当我们停下整个web项目时

以上就是 Java-web中 ServletContext 域对象通过 listener 将自身不同阶段暴露给应用程序的方法感兴趣的可以再试试SessionContext和 RequestContext 不同的监听接口,通过这种方式我们可以实现诸如 实时统计在线人数,限流等各种业务需求

四 Spring中Listener的使用

接下来要说的就是重头戏了,spring中的listener。

spring和springboot 中的事件监听机制都是通过 发布-订阅实现的,主要包括以下四部分:

  • 事件ApplicationEvent,继承 JDK 的 EventObject,可自定义事件。
  • 事件监听者,继承 JDK 的 EventListener,负责监听指定事件。
  • 调度中心 ApplicationContext,也可以用它的父类ApplicationEventPublisher,负责事件发布和回调监听
  • 事件发布者:多为自己定义的业务逻辑类,可通过ApplicationContext提供的push方法发布事件

4.1 对spring原生事件监听

ApplicationListenerDemo1 自定义监听

package com.executor.executor.springlistener.demo1;


import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
 * @Description 自定义监听类
 * @Author CV大虾
 * @Date 2022/11/25 16:12
 */
@Component
public class ApplicationListenerDemo1 implements ApplicationListener {

    @Override
   public void onApplicationEvent(ApplicationEvent event) {
       System.out.println("ApplicationListenerDemo1 监控到事件:"+event.toString());
    }
}

PublisherDemo1 自定义业务处理类

package com.executor.executor.springlistener.demo1;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.stereotype.Component;

/**
 * @Description 自己的业务处理类,事件发布者,相当于事件源,在这里发布事件
 * @Author CV大虾
 * @Date 2022/11/26 15:49
 */

//这里实现ApplicationRunner接口,当spring容器启动后,就会运行run方法,方便测试
@Component
public class PublisherDemo1 implements ApplicationRunner {

    @Autowired
    ApplicationContext applicationContext;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        //添加一个Spring 原生 ApplicationEvent 事件,并在触发事件时,将PublisherDemo1 作为数据源传递
        applicationContext.publishEvent(new ApplicationEvent(this) {});
    }
}

运行结果如下

在这里插入图片描述

可以看到除了监控到了我们的自定义业务处理类(数据源)PublisherDemo1,还将Spring容器启动后一些自带的ApplicationEvent 事件也监控到了


4.2 对自定义事件的监听

ApplicationEventDemo2 自定义事件

package com.executor.executor.springlistener.demo2;

import org.springframework.context.ApplicationEvent;
import org.springframework.stereotype.Component;

/**
 * @Description 自定义事件ApplicationEventDemo2
 * @Author CV大虾
 * @Date 2022/11/25 16:12
 */

public class ApplicationEventDemo2 extends ApplicationEvent {
    public ApplicationEventDemo2(Object source) {
        super(source);
    }
}

ApplicationListenerDemo2 自定义监听

package com.executor.executor.springlistener.demo2;


import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
 * @Description 自定义监听,要加入spring容器中,指定泛型,只针对ApplicationEventDemo2事件进行监听
 * @Author CV大虾
 * @Date 2022/11/25 16:12
 */
//这里要将监听加入spring容器当中,才能在后续触发
@Component
public class ApplicationListenerDemo2 implements ApplicationListener<ApplicationEventDemo2> {

    @Override
    public void onApplicationEvent(ApplicationEventDemo2 event) {
        System.out.println("ApplicationListenerDemo1 监控到事件:"+event.toString());
    }

}

PublisherDemo2 自定义也业务处理类

package com.executor.executor.springlistener.demo2;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

/**
 * @Description 事件源、监听器、
 * @Author CV大虾
 * @Date 2022/11/26 15:49
 */
@Component
public class PublisherDemo2 implements ApplicationRunner {

    @Autowired
    ApplicationContext applicationContext;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        //这里发布的事件为自己自定义的事件ApplicationEventDemo2 
        applicationContext.publishEvent(new ApplicationEventDemo2(this));
    }
}

运行结果如下

在这里插入图片描述

这里可以看到Spring容器启动时,我们自定义的监听ApplicationListenerDemo2 只监听到了我们自定义的事件。

4.3 使用@EventListener 增加监听的扩展性

Spring 提供了 @EventListener注解,支持我们使用注解的形式注册 listener。

@EventListener注解支持在方法上使用,并且可以支持方法监听一个或多个监听,且支持同步异步。


4.3.1 注解注册单个监听

ApplicationEventDemo3 自定义事件

package com.executor.executor.springlistener.demo3;

import org.springframework.context.ApplicationEvent;
import org.springframework.stereotype.Component;

/**
 * @Description 自定义事件ApplicationEventDemo2
 * @Author CV大虾
 * @Date 2022/11/25 16:12
 */

public class ApplicationEventDemo3 extends ApplicationEvent {
    public ApplicationEventDemo3(Object source) {
        super(source);
    }
}

ApplicationListenerDemo3 自定义监听

package com.executor.executor.springlistener.demo3;


import com.executor.executor.springlistener.demo2.ApplicationEventDemo2;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

/**
 * @Description 自定义监听
 * @Author CV大虾
 * @Date 2022/11/25 16:12
 */
//这里要将监听加入spring容器当中,才能在后续触发,原因在后文源码分析会有讲到
@Slf4j
@Component
public class ApplicationListenerDemo3 {
	
  
    /**
     * @Description 入参对象为ApplicationEventDemo2 表示监控的对象为 ApplicationEventDemo3
     * @param applicationEventDemo2
     * @return
     */
    @EventListener
    public void listener002(ApplicationEventDemo2 applicationEventDemo2){
        log.info("listener002 监控到事件EventDemo2: {}",applicationEventDemo2);
    }

    /**
     * @Description 入参对象为ApplicationEventDemo3 表示监控的对象为 ApplicationEventDemo3
     * @param applicationEventDemo3
     * @return
     */
    @EventListener
    public void listener003(ApplicationEventDemo3 applicationEventDemo3){
        log.info("listener003 监控到事件EventDemo3: {}",applicationEventDemo3);
    }



}

PublisherDemo3 自定义也业务处理类

package com.executor.executor.springlistener.demo3;

import com.executor.executor.springlistener.demo2.ApplicationEventDemo2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

/**
 * @Description 事件源、监听器、
 * @Author CV大虾
 * @Date 2022/11/26 15:49
 */
@Component
public class PublisherDemo3 implements ApplicationRunner {

    @Autowired
    ApplicationContext applicationContext;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        //这里发布两个自定义事件方便测试
        applicationContext.publishEvent(new ApplicationEventDemo2(this));
        applicationContext.publishEvent(new ApplicationEventDemo3(this));

    }
}

运行结果如下

在这里插入图片描述

可以看到加了 @EventListener 注解的两个方法,入参分别是不同的事件对象,则会监听到该对象的事件。


4.3.2 注解注册多个监听

当想用 @EventListener 在同一个方法上监听到多个事件时,该注解也支持传入一个数组,但是注意入参对象只允许有一个 ,所以我们可以把ApplicationEvent 这个接口作为入参对象

ApplicationListenerDemo3 自定义监听: 一个方法监控多个事件

    /**
     * @Description  这里注意入参对象只允许有一个,所以这里可以传入ApplicationEvent 再做事件的类型判断
     * @param event
     * @return
     */
    @EventListener({ApplicationEventDemo2.class,ApplicationEventDemo3.class})
    public void listener004(ApplicationEvent event){
       //这里做事件类型判断
        if(event instanceof  ApplicationEventDemo2){
            log.info("listener004 监控到事件EventDemo2: {}",event);
        }
        if(event instanceof  ApplicationEventDemo3){
            log.info("listener004 监控到事件EventDemo3: {}",event);
        }
    }

4.3.5 注解指定注册监听的条件

在开发的过程中,如果我们在多个业务节点发布了相同的事件,但是我们只想监听有指定内容的事件。比如在库存节点,发布了Order事件,里面包含了库存数目。在订单节点,也发布了同样的事件类型,但是只有订单数目,没有库存数目,这时如果我们想 定义一个监听只监控我们想要的条件,@EventListener直接加condition属性就搞定。

Order 订单类

package com.executor.executor.springlistener.demo3;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * @Description
 * @Author CV大虾
 * @Date 2022/11/28 14:56
 */
@Data
@AllArgsConstructor
public class Order {
    private String id;
    private int num;
    private String message;
    
}

ApplicationEventDemo3

package com.executor.executor.springlistener.demo3;

import lombok.Getter;
import org.springframework.context.ApplicationEvent;
import org.springframework.stereotype.Component;

/**
 * @Description 自定义事件ApplicationEventDemo3
 * @Author CV大虾
 * @Date 2022/11/25 16:12
 */

public class ApplicationEventDemo3 extends ApplicationEvent {

    private Order order;

    public ApplicationEventDemo3(Order order) {
        super(order);
        this.order=order;
    }

    public Order getOrder() {
        return order;
    }

    public void setOrder(Order order) {
        this.order = order;
    }
}

PublisherDemo3 发布事件

    @Override
    public void run(ApplicationArguments args) throws Exception {
        //这里发布我们的自定义事件,并将一个订单作为数据源传递下去(实际开发中你可以传递你自己需要监控的数据)
        applicationContext.publishEvent(new ApplicationEventDemo3(new Order("001",21,"这是订单001")));

    }

ApplicationListenerDemo3 自定义监听

 /***
     * 这里 #root.args[0] 表示我们的自定义事件也就是我们的方法入参对象
     *   order则是我们上文在 ApplicationEventDemo3 中定义的属性 order
     *   id 就是order属性中的id
     *
     *   合起来也就是只监控ApplicationEventDemo3 事件中数据源的id位 001 的事件
     * @param applicationEventDemo3
     */
		
    @EventListener(condition = "#root.args[0].order.id=='001'")
    public void listener003(ApplicationEventDemo3 applicationEventDemo3){
        log.info("listener002 监控到事件EventDemo3: {}",applicationEventDemo3.getOrder());
    }



可以试一下,如果将事件发布时 order的id属性改为 002,我们的监控就监控不到了。


4.3.3 @Order指定注册监听顺序

@Order属性可以指定不同监听器对同一事件监控的执行顺序 例如 4.3.1 中,我们再加上一个监听,同样监控 ApplicationEventDemo2 看看执行顺序会有什么变化

@Order(2)
@EventListener
public void listener002(ApplicationEventDemo2 applicationEventDemo2){
    log.info("listener002 监控到事件EventDemo2: {}",applicationEventDemo2);
}

@Order(1)
@EventListener
public void listener004(ApplicationEventDemo2 applicationEventDemo2){
    log.info("listener004 监控到事件EventDemo2: {}",applicationEventDemo2);
}

执行结果如下

在这里插入图片描述

可以看到加了 @Order(1) 先执行


4.3.4 @Async注解指定注册监听异步

默认情况下, 事件发布->触发监听->监听代码执行->返回主线程 是同步的。 我们可以通过在 启动类开启异步支持注解@EnableAsync,然后再监听上加上 @Async 即可开启异步,变为 事件触发->触发监听->(监听代码异步执行) ->主线程代码执行

示例代码如下

package com.executor.executor;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync  //开启支持异步
public class ExecutorApplication {

    public static void main(String[] args) {
        SpringApplication.run(ExecutorApplication.class, args);
    }

}

PublisherDemo3 发布事件

@Override
public void run(ApplicationArguments args) throws Exception {
    long start =System.currentTimeMillis();
    //这里发布的事件  并统计主线程的执行时间
    applicationContext.publishEvent(new ApplicationEventDemo2(this));
    long end =System.currentTimeMillis();
    log.info("程序执行时间  ->{}" , end -start);

    
}

ApplicationListenerDemo3 自定义监听

@Async //开启异步
@EventListener
public void listener002(ApplicationEventDemo2 applicationEventDemo2) throws InterruptedException {
    log.info("listener002 监控到事件EventDemo2: {}",applicationEventDemo2);
    Thread.sleep(2000); //休眠两秒,看看主线程是否受到印象
}

运行结果

在这里插入图片描述

可以看到因为开启了异步,所以主线程并未受到影响。


总结:

本文主要介绍了事件驱动模型,以及基于事件驱动模式的设计模式:观察者模式和发布订阅模式,并且也讲解了,两种模式的区别,给出了对应的示例代码。在观察者模式和发布订阅模式的基础之上,我们又重点介绍了在java-web,spring中的事件监听的使用,希望这种层层递进的方式,能帮大家梳理清楚一个大致的知识脉络。

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值