設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用

本文主要內(nèi)容:

  • 介紹觀察者模式
  • 微信公眾號(hào)的發(fā)布/訂閱示例
  • 觀察者模式總結(jié)
  • 分析觀察者模式的典型應(yīng)用
    • JDK 提供的觀察者接口中的觀察者模式
    • Guava EventBus 中的觀察者模式
    • JDK 委托事件模型DEM中的觀察者模式
    • Spring ApplicationContext 事件機(jī)制中的觀察者模式

觀察者模式

觀察者模式是設(shè)計(jì)模式中的 "超級(jí)模式",其應(yīng)用隨處可見,我們以微信公眾號(hào)為例。

微信公眾號(hào)有服務(wù)號(hào)诀蓉、訂閱號(hào)和企業(yè)號(hào)之分柜蜈。以我的公眾號(hào)為例藕甩,我的公眾號(hào)類型是訂閱號(hào)辰狡,名稱是 "小旋鋒",專注于大數(shù)據(jù)含鳞,Java后端類技術(shù)分享。目前主要是分享學(xué)習(xí)筆記為主芹务,盡量做到 "原創(chuàng)"蝉绷、"高質(zhì)量"、"成體系"枣抱。每當(dāng)我發(fā)布一篇博文推送熔吗,訂閱的用戶都能夠在我發(fā)布推送之后及時(shí)接收到推送,即可方便地在手機(jī)端進(jìn)行閱讀佳晶。

微信公眾號(hào).發(fā)布/訂閱

觀察者模式(Observer Pattern):定義對(duì)象之間的一種一對(duì)多依賴關(guān)系桅狠,使得每當(dāng)一個(gè)對(duì)象狀態(tài)發(fā)生改變時(shí),其相關(guān)依賴對(duì)象皆得到通知并被自動(dòng)更新。觀察者模式是一種對(duì)象行為型模式垂攘。

觀察者模式的別名包括發(fā)布-訂閱(Publish/Subscribe)模式维雇、模型-視圖(Model/View)模式、源-監(jiān)聽器(Source/Listener)模式或從屬者(Dependents)模式晒他。

觀察者模式包含觀察目標(biāo)和觀察者兩類對(duì)象吱型,一個(gè)目標(biāo)可以有任意數(shù)目的與之相依賴的觀察者,一旦觀察目標(biāo)的狀態(tài)發(fā)生改變陨仅,所有的觀察者都將得到通知津滞。

角色

Subject(目標(biāo)):目標(biāo)又稱為主題,它是指被觀察的對(duì)象灼伤。在目標(biāo)中定義了一個(gè)觀察者集合触徐,一個(gè)觀察目標(biāo)可以接受任意數(shù)量的觀察者來(lái)觀察,它提供一系列方法來(lái)增加和刪除觀察者對(duì)象狐赡,同時(shí)它定義了通知方法notify()撞鹉。目標(biāo)類可以是接口,也可以是抽象類或具體類颖侄。

ConcreteSubject(具體目標(biāo)):具體目標(biāo)是目標(biāo)類的子類鸟雏,通常它包含有經(jīng)常發(fā)生改變的數(shù)據(jù),當(dāng)它的狀態(tài)發(fā)生改變時(shí)览祖,向它的各個(gè)觀察者發(fā)出通知孝鹊;同時(shí)它還實(shí)現(xiàn)了在目標(biāo)類中定義的抽象業(yè)務(wù)邏輯方法(如果有的話)。如果無(wú)須擴(kuò)展目標(biāo)類展蒂,則具體目標(biāo)類可以省略又活。

Observer(觀察者):觀察者將對(duì)觀察目標(biāo)的改變做出反應(yīng),觀察者一般定義為接口锰悼,該接口聲明了更新數(shù)據(jù)的方法update()柳骄,因此又稱為抽象觀察者。

ConcreteObserver(具體觀察者):在具體觀察者中維護(hù)一個(gè)指向具體目標(biāo)對(duì)象的引用松捉,它存儲(chǔ)具體觀察者的有關(guān)狀態(tài)夹界,這些狀態(tài)需要和具體目標(biāo)的狀態(tài)保持一致;它實(shí)現(xiàn)了在抽象觀察者Observer中定義的update()方法隘世。通常在實(shí)現(xiàn)時(shí)可柿,可以調(diào)用具體目標(biāo)類的attach()方法將自己添加到目標(biāo)類的集合中或通過(guò)detach()方法將自己從目標(biāo)類的集合中刪除。

示例

首先需要一個(gè)訂閱者接口(觀察者)丙者,該接口有一個(gè) receive 方法复斥,用于接收公眾號(hào)推送通知

public interface Subscriber {
    int receive(String publisher, String articleName);
}

然后是一個(gè)微信客戶端(具體觀察者),實(shí)現(xiàn)了 receive 方法

public class WeChatClient implements Subscriber {
    private String username;

    public WeChatClient(String username) {
        this.username = username;
    }

    @Override
    public int receive(String publisher, String articleName) {
        // 接收到推送時(shí)的操作
        System.out.println(String.format("用戶<%s> 接收到 <%s>微信公眾號(hào) 的推送械媒,文章標(biāo)題為 <%s>", username, publisher, articleName));
        return 0;
    }
}

發(fā)布者類(目標(biāo)目锭,被觀察對(duì)象)评汰,該類維護(hù)了一個(gè)訂閱者列表,實(shí)現(xiàn)了訂閱痢虹、取消訂閱被去、通知所有訂閱者等功能

public class Publisher {
    private List<Subscriber> subscribers;
    private boolean pubStatus = false;

    public Publisher() {
        subscribers = new ArrayList<Subscriber>();
    }

    protected void subscribe(Subscriber subscriber) {
        this.subscribers.add(subscriber);
    }

    protected void unsubscribe(Subscriber subscriber) {
        if (this.subscribers.contains(subscriber)) {
            this.subscribers.remove(subscriber);
        }
    }

    protected void notifySubscribers(String publisher, String articleName) {
        if (this.pubStatus == false) {
            return;
        }
        for (Subscriber subscriber : this.subscribers) {
            subscriber.receive(publisher, articleName);
        }
        this.clearPubStatus();
    }

    protected void setPubStatus() {
        this.pubStatus = true;
    }

    protected void clearPubStatus() {
        this.pubStatus = false;
    }
}

微信公眾號(hào)類(具體目標(biāo)),該類提供了 publishArticles 方法奖唯,用于發(fā)布推送惨缆,當(dāng)文章發(fā)布完畢時(shí)調(diào)用父類的通知所有訂閱者方法

public class WeChatAccounts extends Publisher {
    private String name;

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

    public void publishArticles(String articleName, String content) {
        System.out.println(String.format("\n<%s>微信公眾號(hào) 發(fā)布了一篇推送,文章名稱為 <%s>丰捷,內(nèi)容為 <%s> ", this.name, articleName, content));
        setPubStatus();
        notifySubscribers(this.name, articleName);
    }
}

測(cè)試

public class Test {
    public static void main(String[] args) {
        WeChatAccounts accounts = new WeChatAccounts("小旋鋒");

        WeChatClient user1 = new WeChatClient("張三");
        WeChatClient user2 = new WeChatClient("李四");
        WeChatClient user3 = new WeChatClient("王五");

        accounts.subscribe(user1);
        accounts.subscribe(user2);
        accounts.subscribe(user3);

        accounts.publishArticles("設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用", "觀察者模式的內(nèi)容...");

        accounts.unsubscribe(user1);
        accounts.publishArticles("設(shè)計(jì)模式 | 單例模式及典型應(yīng)用", "單例模式的內(nèi)容....");
    }
}

結(jié)果如下坯墨,符合預(yù)期,當(dāng)公眾號(hào)發(fā)布一篇推送時(shí)病往,訂閱該公眾號(hào)的用戶可及時(shí)接收到推送的通知

<小旋鋒>微信公眾號(hào) 發(fā)布了一篇推送捣染,文章名稱為 <設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用>,內(nèi)容為 <觀察者模式的內(nèi)容...> 
用戶<張三> 接收到 <小旋鋒>微信公眾號(hào) 的推送停巷,文章標(biāo)題為 <設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用>
用戶<李四> 接收到 <小旋鋒>微信公眾號(hào) 的推送耍攘,文章標(biāo)題為 <設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用>
用戶<王五> 接收到 <小旋鋒>微信公眾號(hào) 的推送,文章標(biāo)題為 <設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用>

<小旋鋒>微信公眾號(hào) 發(fā)布了一篇推送叠穆,文章名稱為 <設(shè)計(jì)模式 | 單例模式及典型應(yīng)用>少漆,內(nèi)容為 <單例模式的內(nèi)容....> 
用戶<李四> 接收到 <小旋鋒>微信公眾號(hào) 的推送,文章標(biāo)題為 <設(shè)計(jì)模式 | 單例模式及典型應(yīng)用>
用戶<王五> 接收到 <小旋鋒>微信公眾號(hào) 的推送硼被,文章標(biāo)題為 <設(shè)計(jì)模式 | 單例模式及典型應(yīng)用>

可畫出類圖如下

示例.觀察者模式類圖

借此機(jī)會(huì)做個(gè)小推廣,歡迎大家關(guān)注我的微信公眾號(hào)哦 ^_^

關(guān)注【小旋鋒】微信公眾號(hào)

觀察者模式總結(jié)

觀察者模式的主要優(yōu)點(diǎn)如下:

  • 觀察者模式可以實(shí)現(xiàn)表示層和數(shù)據(jù)邏輯層的分離渗磅,定義了穩(wěn)定的消息更新傳遞機(jī)制嚷硫,并抽象了更新接口,使得可以有各種各樣不同的表示層充當(dāng)具體觀察者角色始鱼。

  • 觀察者模式在觀察目標(biāo)和觀察者之間建立一個(gè)抽象的耦合仔掸。觀察目標(biāo)只需要維持一個(gè)抽象觀察者的集合,無(wú)須了解其具體觀察者医清。由于觀察目標(biāo)和觀察者沒(méi)有緊密地耦合在一起起暮,因此它們可以屬于不同的抽象化層次。

  • 觀察者模式支持廣播通信会烙,觀察目標(biāo)會(huì)向所有已注冊(cè)的觀察者對(duì)象發(fā)送通知负懦,簡(jiǎn)化了一對(duì)多系統(tǒng)設(shè)計(jì)的難度。

  • 觀察者模式滿足 "開閉原則" 的要求柏腻,增加新的具體觀察者無(wú)須修改原有系統(tǒng)代碼纸厉,在具體觀察者與觀察目標(biāo)之間不存在關(guān)聯(lián)關(guān)系的情況下,增加新的觀察目標(biāo)也很方便五嫂。

觀察者模式的主要缺點(diǎn)如下:

  • 如果一個(gè)觀察目標(biāo)對(duì)象有很多直接和間接觀察者颗品,將所有的觀察者都通知到會(huì)花費(fèi)很多時(shí)間肯尺。

  • 如果在觀察者和觀察目標(biāo)之間存在循環(huán)依賴,觀察目標(biāo)會(huì)觸發(fā)它們之間進(jìn)行循環(huán)調(diào)用躯枢,可能導(dǎo)致系統(tǒng)崩潰则吟。

  • 觀察者模式?jīng)]有相應(yīng)的機(jī)制讓觀察者知道所觀察的目標(biāo)對(duì)象是怎么發(fā)生變化的,而僅僅只是知道觀察目標(biāo)發(fā)生了變化锄蹂。

適用場(chǎng)景

  • 一個(gè)抽象模型有兩個(gè)方面氓仲,其中一個(gè)方面依賴于另一個(gè)方面,將這兩個(gè)方面封裝在獨(dú)立的對(duì)象中使它們可以各自獨(dú)立地改變和復(fù)用败匹。

  • 一個(gè)對(duì)象的改變將導(dǎo)致一個(gè)或多個(gè)其他對(duì)象也發(fā)生改變寨昙,而并不知道具體有多少對(duì)象將發(fā)生改變,也不知道這些對(duì)象是誰(shuí)掀亩。

  • 需要在系統(tǒng)中創(chuàng)建一個(gè)觸發(fā)鏈舔哪,A對(duì)象的行為將影響B(tài)對(duì)象,B對(duì)象的行為將影響C對(duì)象……槽棍,可以使用觀察者模式創(chuàng)建一種鏈?zhǔn)接|發(fā)機(jī)制捉蚤。

觀察者模式的典型應(yīng)用

JDK 提供的觀察者接口

觀察者模式在Java語(yǔ)言中的地位非常重要。在JDK的 java.util 包中炼七,提供了 Observable 類以及 Observer 接口缆巧,它們構(gòu)成了JDK對(duì)觀察者模式的支持。

其中的 Observer 接口為觀察者豌拙,只有一個(gè) update 方法陕悬,當(dāng)觀察目標(biāo)發(fā)生變化時(shí)被調(diào)用,其代碼如下:

public interface Observer {
    void update(Observable o, Object arg);
}

Observable 類則為目標(biāo)類按傅,相比我們的示例中的 Publisher 類多了并發(fā)和NPE方面的考慮

public class Observable {
    private boolean changed = false;
    private Vector<Observer> obs = new Vector();

    public Observable() {
    }
    // 用于注冊(cè)新的觀察者對(duì)象到向量中
    public synchronized void addObserver(Observer var1) {
        if (var1 == null) {
            throw new NullPointerException();
        } else {
            if (!this.obs.contains(var1)) {
                this.obs.addElement(var1);
            }

        }
    }
    // 用于刪除向量中的某一個(gè)觀察者對(duì)象
    public synchronized void deleteObserver(Observer var1) {
        this.obs.removeElement(var1);
    }

    public void notifyObservers() {
        this.notifyObservers((Object)null);
    }
    // 通知方法捉超,用于在方法內(nèi)部循環(huán)調(diào)用向量中每一個(gè)觀察者的update()方法
    public void notifyObservers(Object var1) {
        Object[] var2;
        synchronized(this) {
            if (!this.changed) {
                return;
            }

            var2 = this.obs.toArray();
            this.clearChanged();
        }

        for(int var3 = var2.length - 1; var3 >= 0; --var3) {
            ((Observer)var2[var3]).update(this, var1);
        }

    }
    // 用于清空向量,即刪除向量中所有觀察者對(duì)象
    public synchronized void deleteObservers() {
        this.obs.removeAllElements();
    }
    // 該方法被調(diào)用后會(huì)設(shè)置一個(gè)boolean類型的內(nèi)部標(biāo)記變量changed的值為true唯绍,表示觀察目標(biāo)對(duì)象的狀態(tài)發(fā)生了變化
    protected synchronized void setChanged() {
        this.changed = true;
    }
    // 用于將changed變量的值設(shè)為false拼岳,表示對(duì)象狀態(tài)不再發(fā)生改變或者已經(jīng)通知了所有的觀察者對(duì)象,調(diào)用了它們的update()方法
    protected synchronized void clearChanged() {
        this.changed = false;
    }
    // 返回對(duì)象狀態(tài)是否改變
    public synchronized boolean hasChanged() {
        return this.changed;
    }
    // 返回向量中觀察者的數(shù)量
    public synchronized int countObservers() {
        return this.obs.size();
    }
}

我們可以使用 Observable 類以及 Observer 接口來(lái)重新實(shí)現(xiàn)微信公眾號(hào)示例况芒。

增加一個(gè)通知類 WechatNotice惜纸,用于推送通知的傳遞

@Data
@AllArgsConstructor
public class WechatNotice {
    private String publisher;
    private String articleName;
}

然后改寫 WeChatClientWeChatAccounts,分別實(shí)現(xiàn)JDK的 Observer 接口和繼承 Observable

public class WeChatClient implements Observer {
    private String username;

    public WeChatClient(String username) {
        this.username = username;
    }

    @Override
    public void update(Observable o, Object arg) {
        //WeChatAccounts weChatAccounts = (WeChatAccounts) o;
        WechatNotice notice = (WechatNotice) arg;
        System.out.println(String.format("用戶<%s> 接收到 <%s>微信公眾號(hào) 的推送绝骚,文章標(biāo)題為 <%s>", username, notice.getPublisher(), notice.getArticleName()));
    }
}

public class WeChatAccounts extends Observable {
    private String name;

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

    public void publishArticles(String articleName, String content) {
        System.out.println(String.format("\n<%s>微信公眾號(hào) 發(fā)布了一篇推送耐版,文章名稱為 <%s>,內(nèi)容為 <%s> ", this.name, articleName, content));
        setChanged();
        notifyObservers(new WechatNotice(this.name, articleName));
    }
}

測(cè)試皮壁,與示例中的測(cè)試代碼的區(qū)別在于調(diào)用的方法不同

public class Test {
    public static void main(String[] args) {
        WeChatAccounts accounts = new WeChatAccounts("小旋鋒");

        WeChatClient user1 = new WeChatClient("張三");
        WeChatClient user2 = new WeChatClient("李四");
        WeChatClient user3 = new WeChatClient("王五");

        accounts.addObserver(user1);
        accounts.addObserver(user2);
        accounts.addObserver(user3);

        accounts.publishArticles("設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用", "觀察者模式的內(nèi)容...");

        accounts.deleteObserver(user1);
        accounts.publishArticles("設(shè)計(jì)模式 | 單例模式及典型應(yīng)用", "單例模式的內(nèi)容....");
    }
}

測(cè)試結(jié)果如下椭更,可以發(fā)現(xiàn)結(jié)果如示例一致

<小旋鋒>微信公眾號(hào) 發(fā)布了一篇推送,文章名稱為 <設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用>蛾魄,內(nèi)容為 <觀察者模式的內(nèi)容...> 
用戶<王五> 接收到 <小旋鋒>微信公眾號(hào) 的推送虑瀑,文章標(biāo)題為 <設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用>
用戶<李四> 接收到 <小旋鋒>微信公眾號(hào) 的推送湿滓,文章標(biāo)題為 <設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用>
用戶<張三> 接收到 <小旋鋒>微信公眾號(hào) 的推送,文章標(biāo)題為 <設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用>

<小旋鋒>微信公眾號(hào) 發(fā)布了一篇推送舌狗,文章名稱為 <設(shè)計(jì)模式 | 單例模式及典型應(yīng)用>叽奥,內(nèi)容為 <單例模式的內(nèi)容....> 
用戶<王五> 接收到 <小旋鋒>微信公眾號(hào) 的推送,文章標(biāo)題為 <設(shè)計(jì)模式 | 單例模式及典型應(yīng)用>
用戶<李四> 接收到 <小旋鋒>微信公眾號(hào) 的推送痛侍,文章標(biāo)題為 <設(shè)計(jì)模式 | 單例模式及典型應(yīng)用>

Guava EventBus 中的觀察者模式

Guava 中的 EventBus 封裝了友好的 "生產(chǎn)/消費(fèi)模型"朝氓,通過(guò)非常簡(jiǎn)單的方式,實(shí)現(xiàn)了觀察者模式中的監(jiān)聽注冊(cè)主届,事件分發(fā)赵哲。

使用了 Guava EventBus 之后,如果需要訂閱消息君丁,不需要實(shí)現(xiàn)任何接口枫夺,只需在監(jiān)聽方法上加上 @Subscribe 注解即可,EventBus 提供了 registerunregister 方法用于注冊(cè)與取消注冊(cè)事件绘闷,當(dāng) EventBus 調(diào)用 post 方法時(shí)將把事件分發(fā)給注冊(cè)的對(duì)象

使用 Guava 重新實(shí)現(xiàn)示例

@Data
@AllArgsConstructor
public class WechatNotice {
    private String publisher;
    private String articleName;
}

public class WeChatClient  {
    private String username;
    
    public WeChatClient(String username) {
        this.username = username;
    }

    @Subscribe
    public void listen(WechatNotice notice) {
        System.out.println(String.format("用戶<%s> 接收到 <%s>微信公眾號(hào) 的推送橡庞,文章標(biāo)題為 <%s>", username, notice.getPublisher(), notice.getArticleName()));
    }
}

public class WeChatAccounts {
    private String name;
    private EventBus eventBus;

    public WeChatAccounts(String name) {
        this.name = name;
        this.eventBus = new EventBus();
    }

    public void publishArticles(String articleName, String content) {
        System.out.println(String.format("\n<%s>微信公眾號(hào) 發(fā)布了一篇推送,文章名稱為 <%s>印蔗,內(nèi)容為 <%s> ", this.name, articleName, content));
        this.eventBus.post(new WechatNotice(this.name, articleName));
    }

    public void register(WeChatClient weChatClient) {
        this.eventBus.register(weChatClient);
    }

    public void unregister(WeChatClient weChatClient) {
        this.eventBus.unregister(weChatClient);
    }
}

測(cè)試

public class Test {
    public static void main(String[] args) {
        WeChatAccounts accounts = new WeChatAccounts("小旋鋒");

        WeChatClient user1 = new WeChatClient("張三");
        WeChatClient user2 = new WeChatClient("李四");
        WeChatClient user3 = new WeChatClient("王五");

        accounts.register(user1);
        accounts.register(user2);
        accounts.register(user3);

        accounts.publishArticles("設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用", "觀察者模式的內(nèi)容...");

        accounts.unregister(user1);
        accounts.publishArticles("設(shè)計(jì)模式 | 單例模式及典型應(yīng)用", "單例模式的內(nèi)容....");
    }
}

不出意料扒最,輸出的內(nèi)容與上面兩個(gè)示例一樣

<小旋鋒>微信公眾號(hào) 發(fā)布了一篇推送,文章名稱為 <設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用>华嘹,內(nèi)容為 <觀察者模式的內(nèi)容...> 
用戶<張三> 接收到 <小旋鋒>微信公眾號(hào) 的推送吧趣,文章標(biāo)題為 <設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用>
用戶<李四> 接收到 <小旋鋒>微信公眾號(hào) 的推送,文章標(biāo)題為 <設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用>
用戶<王五> 接收到 <小旋鋒>微信公眾號(hào) 的推送耙厚,文章標(biāo)題為 <設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用>

<小旋鋒>微信公眾號(hào) 發(fā)布了一篇推送再菊,文章名稱為 <設(shè)計(jì)模式 | 單例模式及典型應(yīng)用>,內(nèi)容為 <單例模式的內(nèi)容....> 
用戶<李四> 接收到 <小旋鋒>微信公眾號(hào) 的推送颜曾,文章標(biāo)題為 <設(shè)計(jì)模式 | 單例模式及典型應(yīng)用>
用戶<王五> 接收到 <小旋鋒>微信公眾號(hào) 的推送,文章標(biāo)題為 <設(shè)計(jì)模式 | 單例模式及典型應(yīng)用>

Guava EventBus 的更多用法可自行查看相關(guān)文檔
Guava EventBus 源碼分析可看這篇 http://t.cn/EZzC35B

JDK 委托事件模型DEM中的觀察者模式

首先來(lái)敲一個(gè)AWT按鈕監(jiān)聽事件的Demo

import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;


public class MouseEvents {
    private Frame frame;
    private Button button;

    MouseEvents() {
        frame = new Frame("點(diǎn)擊按鈕觸發(fā)點(diǎn)擊事件秉剑,控制臺(tái)將打印日志");
        frame.setBounds(300, 200, 600, 300);
        frame.setLayout(new FlowLayout());

        button = new Button("this is a button");
        button.setFont(new Font("Default", 0, 30));
        frame.add(button);

        dealwithEvent();

        frame.setVisible(true);
    }

    //事件監(jiān)聽器以及處理事件
    private void dealwithEvent() {
        // 監(jiān)聽窗體關(guān)閉事件
        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });

        button.addActionListener(new ActionListener() {
            private int eventCount = 1;

            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println(String.format("動(dòng)作事件發(fā)生 %d 次", eventCount++));
            }
        });
    }

    public static void main(String[] args) {
        new MouseEvents();
    }
}

運(yùn)行 main 方法桌面將彈出下面的面板和按鈕

按鈕監(jiān)聽鼠標(biāo)事件

按鈕的 addActionListener 添加指定的動(dòng)作偵聽器泛豪,以接收發(fā)自此按鈕的動(dòng)作事件,當(dāng)用戶在按鈕上按下或釋放鼠標(biāo)時(shí)侦鹏,JVM將產(chǎn)生一個(gè)相應(yīng)的 ActionEvent 類型的事件對(duì)象诡曙,并在觸發(fā)事件時(shí)將調(diào)用按鈕的 fireXXX() 方法(繼承自 Component),在該方法內(nèi)部略水,將調(diào)用注冊(cè)到按鈕中的 ActionListener 對(duì)象的 actionPerformed() 方法(也就是我們實(shí)現(xiàn)的匿名事件處理類)价卤,實(shí)現(xiàn)對(duì)事件的處理

動(dòng)作事件發(fā)生 1 次
動(dòng)作事件發(fā)生 2 次
動(dòng)作事件發(fā)生 3 次
動(dòng)作事件發(fā)生 4 次

Spring ApplicationContext 事件機(jī)制中的觀察者模式

spring的事件機(jī)制是從java的事件機(jī)制拓展而來(lái),ApplicationContext 中事件處理是由 ApplicationEvent 類和 ApplicationListener 接口來(lái)提供的渊涝。如果一個(gè)Bean實(shí)現(xiàn)了 ApplicationListener 接口慎璧,并且已經(jīng)發(fā)布到容器中去床嫌,每次 ApplicationContext 發(fā)布一個(gè) ApplicationEvent 事件,這個(gè)Bean就會(huì)接到通知

  • ApplicationContext:事件源胸私,其中的 publishEvent()方法用于觸發(fā)容器事件
  • ApplicationEvent:事件本身厌处,自定義事件需要繼承該類,可以用來(lái)傳遞數(shù)據(jù)
  • ApplicationListener:事件監(jiān)聽器接口岁疼,事件的業(yè)務(wù)邏輯封裝在監(jiān)聽器里面

使用 spring 事件機(jī)制重新實(shí)現(xiàn)示例

@Data
public class WechatNotice extends ApplicationEvent {
    private String publisher;
    private String articleName;

    public WechatNotice(Object source, String publisher, String articleName) {
        super(source);
        this.publisher = publisher;
        this.articleName = articleName;
    }
}

public class WeChatClient implements ApplicationListener {
    private String username;

    public WeChatClient(String username) {
        this.username = username;
    }

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof WechatNotice) {
            WechatNotice notice = (WechatNotice) event;
            System.out.println(String.format("用戶<%s> 接收到 <%s>微信公眾號(hào) 的推送阔涉,文章標(biāo)題為 <%s>", username, notice.getPublisher(), notice.getArticleName()));
        }
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

public class WeChatAccounts implements ApplicationContextAware {
    private ApplicationContext ctx;
    private String name;

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

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.ctx = applicationContext;
    }

    public void publishArticles(String articleName, String content) {
        System.out.println(String.format("\n<%s>微信公眾號(hào) 發(fā)布了一篇推送,文章名稱為 <%s>捷绒,內(nèi)容為 <%s> ", this.name, articleName, content));
        ctx.publishEvent(new WechatNotice(this.name, this.name, articleName));
    }
}

在 resources 目錄下創(chuàng)建 spring.xml 文件瑰排,填入下面的內(nèi)容

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="WeChatAccounts" class="com.observer.sprintevent.WeChatAccounts" scope="prototype">
        <constructor-arg name="name" value=""></constructor-arg>
    </bean>
    <bean id="WeChatClient1" class="com.observer.sprintevent.WeChatClient">
        <constructor-arg name="username" value="張三"></constructor-arg>
    </bean>
    <bean id="WeChatClient2" class="com.observer.sprintevent.WeChatClient">
        <constructor-arg name="username" value="李四"></constructor-arg>
    </bean>
    <bean id="WeChatClient3" class="com.observer.sprintevent.WeChatClient">
        <constructor-arg name="username" value="王五"></constructor-arg>
    </bean>
</beans>

測(cè)試

public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");

        WeChatAccounts accounts = (WeChatAccounts) context.getBean("WeChatAccounts");
        accounts.setName("小旋鋒");
        accounts.setApplicationContext(context);

        accounts.publishArticles("設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用", "觀察者模式的內(nèi)容...");
    }
}

輸出如下

<小旋鋒>微信公眾號(hào) 發(fā)布了一篇推送,文章名稱為 <設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用>暖侨,內(nèi)容為 <觀察者模式的內(nèi)容...> 
用戶<張三> 接收到 <小旋鋒>微信公眾號(hào) 的推送椭住,文章標(biāo)題為 <設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用>
用戶<李四> 接收到 <小旋鋒>微信公眾號(hào) 的推送,文章標(biāo)題為 <設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用>
用戶<王五> 接收到 <小旋鋒>微信公眾號(hào) 的推送它碎,文章標(biāo)題為 <設(shè)計(jì)模式 | 觀察者模式及典型應(yīng)用>

在此示例中 ApplicationContext 對(duì)象的實(shí)際類型為 ClassPathXmlApplicationContext函荣,其中的與 publishEvent 方法相關(guān)的主要代碼如下:

private ApplicationEventMulticaster applicationEventMulticaster;

public void publishEvent(ApplicationEvent event) {
    this.getApplicationEventMulticaster().multicastEvent(event);
    if (this.parent != null) {
        this.parent.publishEvent(event);
    }
}

ApplicationEventMulticaster getApplicationEventMulticaster() throws IllegalStateException {
    return this.applicationEventMulticaster;
}

protected void initApplicationEventMulticaster() {
        ConfigurableListableBeanFactory beanFactory = this.getBeanFactory();
        if (beanFactory.containsLocalBean("applicationEventMulticaster")) {
            this.applicationEventMulticaster = (ApplicationEventMulticaster)beanFactory.getBean("applicationEventMulticaster", ApplicationEventMulticaster.class);
        } else {
            this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
            beanFactory.registerSingleton("applicationEventMulticaster", this.applicationEventMulticaster);
        }

    }

其中的 SimpleApplicationEventMulticaster 如下,multicastEvent 方法主要是通過(guò)遍歷 ApplicationListener(注冊(cè)由 AbstractApplicationEventMulticaster 實(shí)現(xiàn))扳肛,使用線程池框架 Executor 來(lái)并發(fā)執(zhí)行 ApplicationListeneronApplicationEvent 方法傻挂,與示例本質(zhì)上是一致的

public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {
    private Executor taskExecutor;

    public void multicastEvent(final ApplicationEvent event) {
        Iterator var2 = this.getApplicationListeners(event).iterator();

        while(var2.hasNext()) {
            final ApplicationListener listener = (ApplicationListener)var2.next();
            Executor executor = this.getTaskExecutor();
            if (executor != null) {
                executor.execute(new Runnable() {
                    public void run() {
                        listener.onApplicationEvent(event);
                    }
                });
            } else {
                listener.onApplicationEvent(event);
            }
        }

    }
}

參考:
劉偉:設(shè)計(jì)模式Java版
慕課網(wǎng)java設(shè)計(jì)模式精講 Debug 方式+內(nèi)存分析
用guava實(shí)現(xiàn)簡(jiǎn)單的事件驅(qū)動(dòng)
springboot 事件機(jī)制

后記

歡迎評(píng)論、轉(zhuǎn)發(fā)挖息、分享金拒,您的支持是我最大的動(dòng)力

更多內(nèi)容可訪問(wèn)我的個(gè)人博客:http://laijianfeng.org

關(guān)注【小旋鋒】微信公眾號(hào),及時(shí)接收博文推送

關(guān)注_小旋鋒_微信公眾號(hào)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末套腹,一起剝皮案震驚了整個(gè)濱河市绪抛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌电禀,老刑警劉巖幢码,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異尖飞,居然都是意外死亡症副,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門政基,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)贞铣,“玉大人,你說(shuō)我怎么就攤上這事沮明≡樱” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵荐健,是天一觀的道長(zhǎng)酱畅。 經(jīng)常有香客問(wèn)我琳袄,道長(zhǎng),這世上最難降的妖魔是什么圣贸? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任挚歧,我火速辦了婚禮,結(jié)果婚禮上吁峻,老公的妹妹穿的比我還像新娘滑负。我一直安慰自己,他們只是感情好用含,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布矮慕。 她就那樣靜靜地躺著,像睡著了一般啄骇。 火紅的嫁衣襯著肌膚如雪痴鳄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天缸夹,我揣著相機(jī)與錄音痪寻,去河邊找鬼。 笑死虽惭,一個(gè)胖子當(dāng)著我的面吹牛橡类,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播芽唇,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼顾画,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了匆笤?” 一聲冷哼從身側(cè)響起研侣,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎炮捧,沒(méi)想到半個(gè)月后庶诡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡咆课,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年灌砖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片傀蚌。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蘸吓,靈堂內(nèi)的尸體忽然破棺而出善炫,到底是詐尸還是另有隱情,我是刑警寧澤库继,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布箩艺,位于F島的核電站窜醉,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏艺谆。R本人自食惡果不足惜榨惰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望静汤。 院中可真熱鬧琅催,春花似錦、人聲如沸虫给。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)抹估。三九已至缠黍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間药蜻,已是汗流浹背瓷式。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留语泽,地道東北人贸典。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像湿弦,于是被迫代替她去往敵國(guó)和親瓤漏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容