监听器和事件驱动模型

2021/04/06 Java高级知识 共 8609 字,约 25 分钟
闷骚的程序员

1. 监听器

1.1 什么是监听器?

监听器是软件服务设计时的一种机制,服务在这个运行生命周期中,通过自身绑定的监听器对象来实时的监听运行过程当中发生的某些动作。
通俗点说,监听器就是一个实现特定接口的普通java程序,这个程序专门用来监听另外一个java对象的方法调用或者属性改变,当被监听的对象发生上述动作后,监听器某个方法将立即被执行。

1.2 如何实现监听器?

前面从委托机制、分析到方法转发、再从方法转发分析到回调机制、最后从回调机制一步步扩展出更加解耦的钩子模式。
简单点回顾下钩子模式:
Server端通过定义一个钩子接口,约束客户端需要实现的行为;
Client端在访问Server端时需要将钩子接口的实现对象注册到Server端;
Server端在设计之初就已经确定了在哪些运行时机去触发钩子对象的回调逻辑。
通过上面的步骤,就能够很轻易的将Client端的请求通过注册钩子对象的方式,转发到第三方的钩子对象的回调逻辑上去。

那根据监听器的需求,我们要想实现一个Listener,肯定是要监听Server端的各个生命周期阶段,当Server端执行到了某个生命周期阶段时,然后触发监听器的逻辑,从而达到对Server端各个生命周期阶段的监听。
很明显,要实现这种场景,钩子接口是唯一的选择。
因此,后面我们听到监听器,第一反应,这玩意儿就是一个服务方定义的钩子接口。

1.3 监听器案例

1.3.1 定义一个监听器接口(钩子)

CustomListener如下:

package zeh.myjavase.code35pattern23.callback.mode3;

public interface CustomListener {

    // 服务端开始执行阶段
    void start();

    // 服务端正在执行阶段
    void process();

    // 服务端结束执行阶段
    void end();
}

1.3.2 实现监听器接口(一般是客户端负责实现)

MyListener如下:

package zeh.myjavase.code35pattern23.callback.mode3;

public class MyListener implements CustomListener {

    @Override
    public void start() {
        System.out.println("Server 开始执行...");
    }

    @Override
    public void process() {
        System.out.println("Server 正在执行...");
    }

    @Override
    public void end() {
        System.out.println("Server 结束执行...");
    }
}

1.3.3 Server端

Server如下:

package zeh.myjavase.code35pattern23.callback.mode3;

public class Server {

    private MyListener listener;

    // 注册监听器(向服务端注册钩子对象)
    public void addListener(MyListener listener) {
        this.listener = listener;
    }

    // Server核心功能
    public void handler() {
        startServer();
        processServer();
        endServer();
    }

    private void startServer() {
        System.out.println("开始启动 Server");
        // 回调监听器的start逻辑
        this.listener.start();
    }

    private void processServer() {
        System.out.println("正在执行 Server");
        System.out.println("1 + 1 = 2");
        // 回调监听器的process逻辑
        this.listener.process();
    }

    private void endServer() {
        System.out.println("结束执行 Server");
        // 回调监听器的end逻辑
        this.listener.end();
    }
}

1.3.4 控制程序(客户端)

Controller如下:

package zeh.myjavase.code35pattern23.callback.mode3;

public class Controller {

    public static void main(String args[]) {
        Server server = new Server();
        server.addListener(new MyListener());
        server.handler();
    }
}

1.3.5 运行Controller如下:

开始启动 Server
Server 开始执行...
正在执行 Server
1 + 1 = 2
Server 正在执行...
结束执行 Server
Server 结束执行...

1.4 运行监听器

上面的案例是一个很容易理解的监听器钩子,实际上监听器的本质就是负责监听Server端的运行生命周期中,某个阶段是否有特定的事情发生,如果有,则触发对应监听器的回调逻辑,达到一种“监听”的目的。
因此,我们一般在设计监听器时,如果这个监听器负责监听服务端容器的各个生命周期阶段的运行状况,我们一般将这种监听器称作运行监听器
在命名时,一般监听器接口命名为 ”RunListener“。
除此之外,其他的监听器如果不加以特殊说明,都属于事件监听器。运行监听器是一种特殊的事件监听器。

2. 事件驱动模型

2.1 一个最基本的回调架子

一个最基本的回调应该包括:

  1. 调用方A和callback回调方法的定义。
  2. 服务方B(被调用方)和事件、以及对事件监听状态的定义。
  3. 控制程序,负责将A对象注入给B,并触发对B的某个方法的调用(即触发事件)。这一步很关键,因为回调的前提就是你中有我。

2.2 回调模式架子升级

按照回调模式的思路,一个成熟的回调方案应该包含如下三种实现思路:

  1. 回调callback:仅仅使用回调,将事件和监听都包含在B对象里面,不灵活。
  2. 将触发事件Event通过接口抽象出去,这样就不用服务方B负责去实现事件了。
  3. 将对事件的监听Listener通过接口抽象出去,这样就不用服务方B负责去监听事件的状态了(往往是通过另外启动一个线程去监听事件的状态)。
    注意:不管哪种方式实现回调,回调既可用于同步回调,也可用于异步回调;而且回调天然具备监听属性,它往往多使用于异步回调中。

2.3 一句话总结回调

你中有我是回调,调用方式很灵活。

3. 事件驱动模型

上面说的监听器仅仅是钩子模式的一种简单实现,而事件驱动模型是一个完整的设计模型,这里面使用了事件、事件源和监听器。
参与事件驱动模型的监听器核心目的就是用来监听事件,因此也把事件驱动模型中的监听器称为事件监听器。

3.1 什么是事件驱动模型

常说的事件监听通知大多数是通过回调机制实现的,因为回调callback机制天生具有监听功能。

  1. 事件驱动模型,实际上就是我们常说的事件监听机制。
  2. 事件驱动模型中,回调(钩子)模式是核心。

3.2 事件驱动模型三要素

  1. 事件
    将服务方发生的行为或者动作抽象出去,包含一层属性和方法,可以理解描述一个行为的发生。
    比如吃饭、喝水。
    事件会持有事件源,也就是说事件源会随着事件的发生,通过发布事件将事件发布出去,而发布出去的事件对象本身又聚合了事件源,将事件发布给事件监听器。

  2. 事件源
    发生事件的实体,即产生事件的主体,事件发生在谁身上,一般用来充当服务方。
    由事件源感知自身是否发生了某个事件,如果发生了指定的事件,会将该事件发布给事件监听器,从而触发事件监听器的回调逻辑执行。
    事件源会持有事件监听器。
    事件源向外提供的能力是:注册事件监听器、移除事件监听器、触发事件监听器等操作,即外部通过操作事件源对象向事件源对象中注册事件监听器、移除事件监听器、触发事件监听器。
    注意,事件源就是回调中定义的服务端程序,事件监听器就是回调中定义的消费方钩子函数,即消费方定义的回调函数。
    事件源触发事件监听器实际上就是在执行事件监听器中的回调逻辑。

  3. 事件监听器
    一旦事件源感知到自身对应的事件发生(客户端线程触发了该事件)将触发执行的回调对象,充当客户方,只需要实现对应的回调逻辑。
    而服务方对事件监听器定义的钩子接口中,对应的方法会持有一个指定的事件对象。
    事件源感知事件发生后,会将事件对象传递给事件监听器,从而触发事件监听器的回调逻辑执行。
    事件监听器会拿到对应的事件对象,拿到里面的具体事件行为和事件源对象,进一步对根据事件和事件源采取对应的回调策略。

我们来总结一下:
事件用于描述事件源会发生的行为,同时事件会依赖事件源;
事件源会依赖事件监听器,当事件源感知到自身的事件发生后,会触发事件监听器的回调逻辑执行;
事件源会接收到一个事件对象,拿到事件对象和事件对象依赖的事件源,完成对应的逻辑处理。

3.3 一句话总结事件驱动模型

事件驱动模型:当事件源感知到自身发生某个事件后,将传入事件对象给事件监听器,触发事件监听器的回调逻辑执行。

4. 回调模式的应用场景

  1. 实现监听通知机制:
    所谓事件监听通知机制,就是“当….的时候,就….”。
    Listener,Filter,Aop,Interceptor,Zookeeper,redis,activeMQ,observer观察者模式等都或多或少的采用回调机制实现事件监听。
    注意:同步回调和异步回调就是用来实现同步事件监听通知或者异步事件监听通知的方案。
  2. 实现方法统一转发
    回调机制能够很方便地将消费方调用的B的某些方法最终统一都转发到B调用A的回调方法上。
    通过接口设计A回调逻辑的标准,将非常容易实现一个方法转发的统一方案。
    详细实现原理请参考动态代理,回调机制在动态代理中就是将所有对代理类的方法访问最终都统一转发到InvocationHandler接口的invoke()方法上,其实invoke()方法就是一个callback方法。

5. 回调模式案例

5.1 mode1

1. 控制程序:

package zeh.test.demo.com.call.back.mode1;
// 最简单的回调模式 充当Client端和控制程序
// 控制程序通过服务端对象进行事件的触发,触发事件后回调客户端提供的回调方法。由此可见,事件是发生在服务方对象上的,回调逻辑是由客户端定义的。
public class Controller {
    public static void main(String args[]) {
        Server server = new Server();
        //调用触发方法触发事件
        server.handler();
    }
    public void callback() {
        System.out.println("客户端提供的回调方法");
    }
}

2. 服务端程序:

package zeh.test.demo.com.call.back.mode1;

// 服务端程序
public class Server {
    private Controller controller = new Controller();
    public void handler() {
        System.out.println("服务端程序暴露的接口,提供触发能力");
        //当服务端业务逻辑执行完毕,即执行客户端提供的回调逻辑
        controller.callback();
    }
}

5.2 mode2

1. 控制程序

package zeh.test.demo.com.call.back.mode2;

// 外部控制程序
public class Controller {
    public static void main(String args[]) {
        Server server = new Server();
        server.addClient(new Client());
        //触发
        server.handler();
    }
}

2. 客户端程序

package zeh.test.demo.com.call.back.mode2;

// 客户端 提供回调逻辑
// 将客户端和控制程序分离
public class Client {
    public void callback() {
        System.out.println("客户端提供的回调方法");
    }
}

3. 服务端程序

package zeh.test.demo.com.call.back.mode2;

// 服务端程序 暴露触发程序和回调注册接口
public class Server {
    private Client client;
    //服务端暴露注册客户端回调逻辑的接口
    public void addClient(Client client) {
        this.client = client;
    }
    public void handler() {
        System.out.println("服务端暴露的触发接口");
        //服务端业务逻辑执行完毕则执行客户端提供的回调逻辑
        client.callback();
    }
}

5.3 mode3

1. 控制程序

package zeh.test.demo.com.call.back.mode3;

// 控制程序
// mode3中,将客户端抽象为了监听器,因为客户端实际上就是提供回调方法的一方,而事件监听器本身的作用就是提供回调逻辑。
// 服务方提供的能力比较多:提供触发方法(监听事件是否发生)、提供回调注册方法等。
public class Controller {
    public static void main(String args[]) {
        Server server = new Server();
        server.addListener(new Listener());
        server.handler();
    }
}

2. 监听器

package zeh.test.demo.com.call.back.mode3;

// 监听器
// 就是回调处理器,即原来定义的客户端;将客户端抽象出来,作为一个监听器,用来监听当事件发生后该采用的回调逻辑;
// 2020-06-28
// zWX5331241
public class Listener {
    //事件监听器永远比较简单,只需要定义当事件被触发后要执行的回调逻辑即可
    public void callback() {
        System.out.println("事件监听器提供的回调逻辑,即回调执行程序");
    }
}

3. 服务端程序

package zeh.test.demo.com.call.back.mode3;

// 服务端提供触发方法、提供回调注册方法。
// 即:事件是发生在服务端对象上的,由服务方监听事件的发生然后调用客户端对象的回调逻辑。
// 习惯上,服务端是用来监听事件的(因为事件永远是发生在服务端对象上的),而事件监听器并不是用来监听事件的,而是用来处理事件的,即当事件发生后应该执行的回调逻辑。
// 客户端==事件监听器
public class Server {
    private Listener listener;
    //服务端暴露注册客户端回调逻辑的接口
    public void addListener(Listener listener) {
        this.listener = listener;
    }
    public void handler() {
        System.out.println("服务端暴露的触发接口");
        //服务端业务逻辑执行完毕则执行客户端提供的回调逻辑
        listener.callback();
    }
}

6. 事件驱动模型实现事件通知机制

6.1 一个简单的事件驱动模型程序

监听器
监听器定义为接口,监听的方法需要事件对象传递进来,从而在监听器上通过事件对象获取得到事件源,对事件源进行修改!

 // 事件监听器,监听Person事件源的eat和sleep方法
    interface PersonListener{

        void doEat(Event event);
        void doSleep(Event event);
    }

事件源
事件源是一个Person类,它有eat和sleep()方法。
事件源需要注册监听器(即在事件源上关联监听器对象)。
如果触发了eat或sleep()方法的时候,会调用监听器的方法,并将事件对象传递进去。

    // 事件源Person,事件源要提供方法注册监听器(即在事件源上关联监听器对象)
    class Person {

        //在成员变量定义一个监听器对象
        private PersonListener personListener ;

        //在事件源中定义两个方法
        public void Eat() {

            //当事件源调用了Eat方法时,应该触发监听器的方法,调用监听器的方法并把事件对象传递进去
            personListener.doEat(new Event(this));
        }

        public void sleep() {

            //当事件源调用了Eat方法时,应该触发监听器的方法,调用监听器的方法并把事件对象传递进去
            personListener.doSleep(new Event(this));
        }

        //注册监听器,该类没有监听器对象啊,那么就传递进来吧。
        public void registerLister(PersonListener personListener) {
            this.personListener = personListener;
        }
    }

事件对象
事件对象封装了事件源。
监听器可以从事件对象上获取得到事件源的对象(信息)。

    // 事件对象Even,事件对象封装了事件源,在监听器上能够通过事件对象获取得到事件源
    class Event{
        private Person person;

        public Event() {
        }

        public Event(Person person) {
            this.person = person;
        }

        public Person getResource() {
            return person;
        }

    }

测试

public static void main(String[] args) {

        Person person = new Person();

        //注册监听器()
        person.registerLister(new PersonListener() {
            @Override
            public void doEat(Event event) {
                Person person1 = event.getResource();
                System.out.println(person1 + "正在吃饭呢!");
            }

            @Override
            public void doSleep(Event event) {
                Person person1 = event.getResource();
                System.out.println(person1 + "正在睡觉呢!");
            }
        });

        //当调用eat方法时,触发事件,将事件对象传递给监听器,最后监听器获得事件源,对事件源进行操作
        person.Eat();
    }

分析上面程序:
事件源:拥有事件
监听器:监听事件源所拥有的事件(带事件对象参数的)
事件对象:事件对象封装了事件源对象
事件源要与监听器有关系,就得注册监听器【提供方法得到监听器对象】
触发事件源的事件,实际会提交给监听器对象处理,并且把事件对象传递过去给监听器。

6.2 稍微复杂点的事件驱动模型程序

7. 通过方法间传递参数也可以实现回调,你见过吗?

上面的案例都是,A或者其他控制程序调用B,B里面委托了A,当执行了B的指定方法后,线程又回头来执行A的某个逻辑。
上面的A对象是注入到B中的,即A此时是作为B的一个成员变量存在的。当然,这也符合我们常规的注入、委托、依赖等定义。因为大多数的委托机制,被委托的对象都是作为成员存在的。
那你有没有想过,被委托的对象直接作为方法形参去接收呢?
请看下面的案例,也是回调的一种:
客户端程序

package zeh.test.demo.com.call.back.method;

// 客户端 提供回调逻辑
// 将客户端和控制程序分离
public class Client {
    public void callback() {
        System.out.println("客户端提供的回调方法");
    }
}

服务端程序

package zeh.test.demo.com.call.back.method;

// 服务端程序 暴露触发程序和回调注册接口
// 2020-06-28
// zWX5331241
public class Server {
    public void handler(Client client) {
        System.out.println("服务端暴露的触发接口");
        //服务端业务逻辑执行完毕则执行客户端提供的回调逻辑
        client.callback();
    }
}

控制程序

package zeh.test.demo.com.call.back.method;

public class Controller {
    public static void main(String[] args) {
        // 回调对象实际上直接作为方法参数传递进去,而不是作为一个成员注入进去
        new Server().handler(new Client());
    }
}

执行结果

服务端暴露的触发接口
客户端提供的回调方法
Process finished with exit code 0

感想
是不是大吃一惊?
这种也叫做回调!
所以,平时我们创建一个目标对象,执行目标对象的某个方法,如果在这个方法里面传入了另外一个对象,则这个对象很可能就是一个回调对象。
因为你传递进去实际上就是为了使用这个对象的某个功能,除非你传递进去并不进行任何调用。

文档信息

Search

    Table of Contents