Spring事件發佈與監聽

最近算是把spring整體的又過了一遍,發現很多東西雖然用的多,但是有些思想理解的不夠透徹,在此記錄下,

順便感嘆下,spring源碼看了大部分,這纔沒過多久又忘了 TnT 。

一、事件監聽相關概念介紹 

1、流程分析

事件:做了什麼事。例如,我在寫博客,寫博客就是一個事件。

監聽器:監聽發生事件的組件。例如,我們日常生活中的火災報警器,監聽有沒有發生火災事件。

在一個完整的事件體系中,除了事件監聽器以外,還應該有3個概念;

1. 事件源:事件的產生者,任何一個event都必須有一個事件源;

2. 事件廣播器:它是事件和事件監聽器之間的橋樑,負責把事件通知給事件監聽器;

3. 事件監聽器註冊表:就是spring框架爲所有的監聽器提供了一個存放的地方;

通過流程圖,可以看出它們是如何各司其職的,如下:

其實通過流程圖,我們很容易發現事件體系就是觀察者模式的具體實現,它並沒有任何的神祕之處。

2、結構分析

結構分析:

1.  事件類(ApplicaitonEvent):目前spring框架本身僅僅提供了幾個事件,很多的事件都是需要自定義的。

 ApplicationEvent唯一的構造函數是ApplicaitonEvent(Object source),通過source指定事件源。  它有兩個子類;

(1)ApplicationContextEvent:容器事件,也就是說事件源是ApplicationContext,框架提供了四個子類,分別代表容器啓動,刷新,停止和關閉事件。

(2)RequestHandleEvent:這是一個與Web應用相關的事件,當一個請求被處理後,纔會產生該事件。

一般來說,我們都是擴展ApplicationEvent來自定義事件。下面會有栗子。

 

2. 事件監聽器接口(ApplicationListener)

所有的監聽器都需要實現該接口,該接口只定義了一個方法:onApplicaitonEvent (E event),該方法接收事件對象,在該方法中編寫事件的響應處理邏輯。

 

二、手寫模擬事件發佈與監聽

注:想直接瞭解Spring事件監聽與發佈的,可以跳過這節,但是我建議你還是看一下。

需求:

假設現在公司讓你開發一個文件操作幫助類 ,

定義一個文件讀寫方法 讀寫某個文件 寫到某個類裏面去 //但是 有時候可能會需要記錄文件讀取進度條的需求

有時候需要進度條 如何實現?

答案:我們可以採用事件發佈與監聽。

事件:文件上傳

事件源:事件在哪裏發佈的,比如說我們在A類中,發佈了事件。那麼A類的對象就是事件源。

監聽器:我們編寫的FileUploadListener對這個事件進行了監聽。並在監聽到了當前事件之後,發佈事件。

代碼編寫:

/**
 * @ClassName ApplicationEvent
 * @Description
 * @Author EvanWang
 * @Version 1.0.0
 * @Date 2019/12/9 20:29
 */
public class ApplicationEvent {
}
/**
 * @ClassName ApplicationListener
 * @Description
 * @Author EvanWang
 * @Version 1.0.0
 * @Date 2019/12/9 20:29
 */
public interface ApplicationListener<E extends ApplicationEvent> {
    void onEvent(E e);
}
/**
 * @ClassName ListenerManage
 * @Description
 * @Author EvanWang
 * @Version 1.0.0
 * @Date 2019/12/9 20:44
 */
//事件管理器
public class ListenerManage {

    //保存所有的監聽器
    static List<ApplicationListener<ApplicationEvent>> list = new ArrayList<>();

    //添加監聽器 注:如果要做的更加優雅,應該做成掃描全局,通過掃描將所有的監聽器放入管理器的容器列表,這裏爲了方便演示就不做複雜了。
    //springboot是從spring的BeanFactory中獲取listener
    public static void addListener(ApplicationListener listener) {
        list.add(listener);
    }

    //判斷一下 有哪些監聽器 監聽了這個事件
    public static void publishEvent(ApplicationEvent event) {
        for (ApplicationListener<ApplicationEvent> applicationListener : list) {
            //獲取ApplicationListener的泛型
            Class typeParameter = (Class) ((ParameterizedType) applicationListener.getClass().getGenericInterfaces()[0]).getActualTypeArguments()[0];
            if (typeParameter.equals(event.getClass())) {
                applicationListener.onEvent(event);
            }
        }
    }
}
/**
 * @ClassName FileUploadEvent
 * @Description
 * @Author EvanWang
 * @Version 1.0.0
 * @Date 2019/12/9 21:37
 */
public class FileUploadEvent extends ApplicationEvent {

    private int fileSize;

    private int readSize;

    public FileUploadEvent(int fileSize, int readSize) {
        this.fileSize = fileSize;
        this.readSize = readSize;
    }

    public int getFileSize() {
        return fileSize;
    }

    public void setFileSize(int fileSize) {
        this.fileSize = fileSize;
    }

    public int getReadSize() {
        return readSize;
    }

    public void setReadSize(int readSize) {
        this.readSize = readSize;
    }
}
/**
 * @ClassName FileUploadListener
 * @Description
 * @Author EvanWang
 * @Version 1.0.0
 * @Date 2019/12/9 21:38
 */
public class FileUploadListener implements ApplicationListener<FileUploadEvent> {
    @Override
    public void onEvent(FileUploadEvent fileUploadEvent) {
        double molecule = fileUploadEvent.getFileSize();
        double denominator = fileUploadEvent.getReadSize();
        System.out.println("當前文件上傳進度百分比:" + (denominator / molecule * 100 + "%"));
    }
}
/**
 * @ClassName FileUtil
 * @Description
 * @Author EvanWang
 * @Version 1.0.0
 * @Date 2019/12/9 17:06
 */
public class FileUtil {

    public static int READ_SIZE = 100;

    public static void fileWrite(InputStream is, OutputStream os) throws Exception {
        fileWrite(is, os, null);
    }

    public static void fileWrite(InputStream is, OutputStream os, FileListener fileListener) throws Exception {
        BufferedInputStream bis = new BufferedInputStream(is);
        BufferedOutputStream bos = new BufferedOutputStream(os);

        /**
         * 如果是網絡請求最好不要用這個方法拿fileSize,因爲這個方法會產生阻塞。最好傳一個File對象進來。
         * 這裏作爲演示,就不去處理細節了。
         */

        //文件總大小
        int fileSize = is.available();
        //一共讀取了多少
        int readSize = 0;
        byte[] readedBytes = new byte[READ_SIZE];

        //控制是否退出
        boolean exit = true;

        while (exit) {
            //文件小於第一次讀的大小的時候
            if (fileSize < READ_SIZE) {
                byte[] fileBytes = new byte[fileSize];
                //將緩衝區中的數據寫入到字節數組fileBytes中
                bis.read(fileBytes);
                //向文件寫入fileBytes數組的內容
                bos.write(fileBytes);
                readSize = fileSize;
                exit = false;
                //當你是最後一次讀的時候
            } else if (fileSize < readSize + READ_SIZE) {
                byte[] bytes = new byte[fileSize - readSize];
                readSize = fileSize;
                bis.read(bytes);
                bos.write(bytes);
                exit = false;
            } else {
                bis.read(readedBytes);
                readSize += READ_SIZE;
                bos.write(readedBytes);
            }
            //發佈事件
            ListenerManage.publishEvent(new FileUploadEvent(fileSize, readSize));
            if (fileListener != null) {
                fileListener.updateLoad(fileSize, readSize);
            }
        }
        bis.close();
        bos.close();
    }
}
/**
 * @ClassName FileReadTest
 * @Description
 * @Author EvanWang
 * @Version 1.0.0
 * @Date 2019/12/9 18:26
 */
public class FileReadTest {
    public static void main(String[] args) throws Exception {
        ListenerManage.addListener(new FileUploadListener());
        //這裏根據實際情況去設置讀寫的文件
        File file = new File("F:\\測試寫出.txt");
        if (!file.exists()) {
            file.createNewFile();
        }
        //如果需要做進度條功能,再添加一個fileListener參數
        fileWrite(new FileInputStream(new File("F:\\明天要做的事.txt")), new FileOutputStream(file));
    }
}

運行結果:

當前文件上傳進度百分比:14.245014245014245%
當前文件上傳進度百分比:28.49002849002849%
當前文件上傳進度百分比:42.73504273504273%
當前文件上傳進度百分比:56.98005698005698%
當前文件上傳進度百分比:71.22507122507122%
當前文件上傳進度百分比:85.47008547008546%
當前文件上傳進度百分比:99.71509971509973%
當前文件上傳進度百分比:100.0%

三、Spring的事件發佈與監聽

我們在上面手動模擬了Spring的事件發佈與監聽,如果看懂了上面的例子,我們再使用Spring寫一個事件發佈與監聽的例子。

package com.evan.spring.config;

import org.springframework.context.annotation.ComponentScan;

/**
 * @ClassName Appconfig
 * @Description
 * @Author EvanWang
 * @Version 1.0.0
 * @Date 2019/12/10 16:04
 */
@ComponentScan("com")
public class AppConfig {
}
package com.evan.spring.event;

import org.springframework.context.ApplicationContext;
import org.springframework.context.event.ApplicationContextEvent;
import org.springframework.context.event.ContextStartedEvent;

/**
 * @ClassName MyEvent
 * @Description
 * @Author EvanWang
 * @Version 1.0.0
 * @Date 2019/12/10 15:39
 */
public class WriteBlogEvent extends ApplicationContextEvent {
    String name;
    String address;

    public WriteBlogEvent(ApplicationContext source, String name, String address) {
        super(source);
        this.name = name;
        this.address = address;
    }

    public String getName() {
        return name;
    }

    public String getAddress() {
        return address;
    }

}

 Spring的事件監聽可以基於註解或實現接口。對於同一個事件,如果兩個都存在,相當於多個監聽器監聽一個事件。

兩個監聽器內的方法都會執行。

package com.evan.spring.listener;

import com.evan.spring.event.WriteBlogEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
 * @ClassName WriteBlogListener
 * @Description
 * @Author EvanWang
 * @Version 1.0.0
 * @Date 2019/12/10 15:47
 */
@Component
public class WriteBlogListener implements ApplicationListener<WriteBlogEvent> {
    @Override
    public void onApplicationEvent(WriteBlogEvent writeBlogEvent) {
        String name = writeBlogEvent.getName();
        String address = writeBlogEvent.getAddress();
        System.out.println("基於實現接口:" + name + "在" + address + "寫了一篇博客");
    }
}
package com.evan.spring.listener;

import com.evan.spring.event.WriteBlogEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

/**
 * @ClassName WriteBlogListenerAnnotation
 * @Description
 * @Author EvanWang
 * @Version 1.0.0
 * @Date 2019/12/10 16:30
 */
@Component
public class WriteBlogListenerAnnotation {
    @EventListener
    public void annotationListen(WriteBlogEvent writeBlogEvent) {
        String name = writeBlogEvent.getName();
        String address = writeBlogEvent.getAddress();
        System.out.println("基於註解:" + name + "在" + address + "寫了一篇博客");
    }
}
package com.evan.spring.test;

import com.evan.spring.config.AppConfig;
import com.evan.spring.event.WriteBlogEvent;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

/**
 * @ClassName EventTest
 * @Description
 * @Author EvanWang
 * @Version 1.0.0
 * @Date 2019/12/10 15:56
 */
public class EventTest {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        WriteBlogEvent writeBlogEvent = new WriteBlogEvent(ac, "Evan", "家裏");
        ac.publishEvent(writeBlogEvent);
    }
}

運行結果:

基於註解:Evan在家裏寫了一篇博客
基於實現接口:Evan在家裏寫了一篇博客

四、總結

1、spring 如何得知有哪些監聽器?
通過2個步驟:1.從Bean工廠拿到所有ApplicationListener類型的Bean.
            2.掃描所有帶@EventListener

2、spring如何發佈事件?
大邏輯上通過2個步驟:  1.判斷是否有監聽器對該事件感興趣
                      2.調用監聽器方法

 文章還有很多不足,歡迎抱着友善交流的態度的朋友提出建議,如果這篇文章幫到了您,麻煩點個贊 : )

如果有什麼問題不明白,可以直接留言,或者加入技術交流羣,我都會及時回覆。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章