java和Spring中觀察者模式的應(yīng)用詳解
一、觀察者模式基本概況
1.概念
觀察者模式(Observer Design Pattern)也被稱為發(fā)布訂閱模式(Publish-Subcribe Design Pattern)。定義如下
Define a one-to-many dependency between objects so that when one object changes state,all its dependents are notified and update automatically。
觀察者模式定義了一種一對多的依賴關(guān)系,讓多個觀察者對象同時監(jiān)聽某一個主題對象。這個主題對象在狀態(tài)變化時,會通知所有觀察者對象,使它們能夠自動更新自己。
2.作用
參考設(shè)計模式之美的一段總結(jié)
回到本質(zhì),設(shè)計模式要干的事情就是解耦。創(chuàng)建型模式是將創(chuàng)建對象和使用對象解耦,結(jié)構(gòu)型模式是將不同功能代碼解耦,行為型模式是將不同的行為代碼解耦,具體到觀察者模式,是將觀察者和被觀察者代碼解耦。
3.實現(xiàn)方式
觀察者模式在不同的場景有不同的實現(xiàn)方式,自然也有不同的名字。如Publisher-Subscriber、Producer-Consumer、Dispatcher-Listener,等等。
有哪些不同實現(xiàn)方式呢?同步阻塞方式,異步非阻塞方式;進(jìn)程內(nèi)實現(xiàn)方式,跨進(jìn)程實現(xiàn)方式。
同步阻塞方式是經(jīng)典的實現(xiàn)方式,是說在主題通知觀察者和觀察者執(zhí)行自己的邏輯是由同一個線程完成的,比如下面要說的java中Observer接口和Observable類,后有UML圖
public void notifyObservers(Object arg) {
for (int i = arrLocal.length-1; i>=0; i--){
//使用了for循環(huán)同步通知觀察者
((Observer)arrLocal[i]).update(this, arg);
}
}
異步非阻塞方式是在通知觀察者和觀察者執(zhí)行自己邏輯不是同一個線程,如下代碼
public void notifyObservers(Object arg) {
for (int i = arrLocal.length-1; i>=0; i--){
//使用了開啟新線程方式異步通知觀察者
new Thread(()->{
((Observer)arrLocal[i]).update(this, arg);
}).start();
}
}
上面都是在同一個進(jìn)程中,還有跨進(jìn)程的實現(xiàn)方式就是常用的MQ,消息隊列
二、java實現(xiàn)兩種觀察者模式
1.Observer接口和Observable類
下面我們使用JDK提供的接口和類實現(xiàn)項目中使用觀察者模式

先說下Observer接口和Observable類
Observable類,被觀察者。有三個組成部分,用Vector管理注冊的觀察者(Observer),并提供了增刪、統(tǒng)計方法。一旦狀態(tài)(changed)改變就通知(notifyObservers)觀察者。notifyObservers方法會調(diào)用觀察者(Observer)的update方法。
Observer接口就是一個觀察者的標(biāo)準(zhǔn),實現(xiàn)該接口并重寫update方法就可以實現(xiàn)一個觀察者。
具體的源碼大家可以自己看下,比較簡單,在java.util包下。
實現(xiàn)一個需求
當(dāng)自定義被觀察者data值=1時通知各個觀察者執(zhí)行update方法,應(yīng)該如何做?
UML圖已經(jīng)畫出來了,剩下就是寫代碼了。
代碼如下
//自定義被觀察者
public class MyObservable extends Observable {
private int data;
public void setData(int data) {
this.data = data;
}
public void notifyObserver(){
//當(dāng)data等于1時通知觀察者
if (data==1){
this.setChanged();
super.notifyObservers();
}
}
}
//自定義觀察者
public class MyObserver implements Observer {
@Override
public void update(Observable o, Object arg) {
System.out.println("自定義觀察者執(zhí)行了.....");
}
}
//測試類
public class PubSubTest {
public static void main(String[] args) {
MyObservable observable = new MyObservable();
observable.addObserver(new MyObserver());
observable.setData(1);
observable.notifyObserver();
}
}
2.EventObject和EventListener
JDK提供了EventObject類和EventListener接口定義了實現(xiàn)觀察者模式的第二個標(biāo)準(zhǔn),只是定義了標(biāo)準(zhǔn)沒有提供默認(rèn)實現(xiàn),但是其他框架如Spring實現(xiàn)該方式,這個下面再說。
先看EventObject類
該類實現(xiàn)的功能就是定義一個事件,其中有一個最重要的屬性source,叫事件源。含義是哪個類發(fā)出的事件。自定義事件時需要繼承該類
public class EventObject implements java.io.Serializable {
protected transient Object source;
public EventObject(Object source) {
if (source == null)
throw new IllegalArgumentException("null source");
this.source = source;
}
public Object getSource() {
return source;
}
}
EventListener接口是一個空接口,自定義監(jiān)聽器需要繼承該接口。
注意此時事件就是一個被觀察者,監(jiān)視器就是觀察者。
三、Spring事件監(jiān)聽實戰(zhàn)及原理
1.Spring如何使用EventObject和EventListener實現(xiàn)觀察者?
在Spring中為自定義事件和自定義監(jiān)聽者,分別提供一個類和一個接口。
ApplicationEvent類繼承了EventObject,用于在Spring環(huán)境下自定義事件
public abstract class ApplicationEvent extends EventObject {
/**
* Create a new {@code ApplicationEvent}.
* @param source the object on which the event initially occurred or with
* which the event is associated (never {@code null})
*/
public ApplicationEvent(Object source) {
super(source);
this.timestamp = System.currentTimeMillis();
}
}
ApplicationListener接口繼承JDK的EventListener,用于在Spring環(huán)境下自定義監(jiān)聽者
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
/**
* Handle an application event.
* @param event the event to respond to
*/
void onApplicationEvent(E event);
}
onApplicationEvent方法就是根據(jù)傳過來的事件參數(shù),監(jiān)聽是否是自己感興趣的事件,然后執(zhí)行方法體。
介紹完類和接口剩下的就是如何在Spring中如何自定義事件、如何發(fā)布事件、如何使用自定義監(jiān)聽器監(jiān)聽事件,下面舉個例子
2.先實戰(zhàn)—要先會用
實現(xiàn)一個需求:當(dāng)調(diào)用一個類的方法完成時,該類發(fā)布事件,事件監(jiān)聽器監(jiān)聽該類的事件并執(zhí)行的自己的方法邏輯
假設(shè)這個類是Request、發(fā)布的事件是ReuqestEvent、事件監(jiān)聽者是ReuqestListener。當(dāng)調(diào)用Request的doRequest方法時,發(fā)布事件。模擬的代碼如下
public class SpringEventTest {
public static void main(String[] args) {
ApplicationContext context=new AnnotationConfigApplicationContext("com.thinkcoder.parttern.behavioral.pubsub.spring");
Request request = (Request) context.getBean("request");
//調(diào)用方法,發(fā)布事件
request.doRequest();
}
}
//定義事件
class RequestEvent extends ApplicationEvent {
public RequestEvent(Request source) {
super(source);
}
}
//發(fā)布事件
@Component
class Request{
@Autowired
private ApplicationContext applicationContext;
public void doRequest(){
System.out.println("調(diào)用Request類的doRequest方法發(fā)送一個請求");
applicationContext.publishEvent(new RequestEvent(this));
}
}
//監(jiān)聽事件
@Component
class RequestListener implements ApplicationListener<RequestEvent> {
@Override
public void onApplicationEvent(RequestEvent event) {
System.out.println("監(jiān)聽到RequestEvent事件,執(zhí)行方法");
}
}
//打印的日志
調(diào)用Request類的doRequest方法發(fā)送一個請求
監(jiān)聽到RequestEvent事件,執(zhí)行方法
上面我們依靠spring實現(xiàn)了事件—監(jiān)聽機制,使用的步驟有如下幾步
- 定義事件:繼承ApplicationEvent類,實現(xiàn)方法傳入事件源。由事件源產(chǎn)生事件
- 發(fā)布事件:使用Spring的IOC容器ApplicationContext的publishEvent方法發(fā)布事件
- 監(jiān)聽事件:實現(xiàn)ApplictionListener接口重寫方法即可實現(xiàn)自定義監(jiān)聽器
3.會原理—搞清楚為什么會這樣
先將上述過程畫成一幅圖,然后展開來解釋各個步驟,相信我你能看懂

通過上面的流程圖,回答下面幾個問題
1.監(jiān)聽器什么時候注冊到IOC容器中?
注冊的開始邏輯是在AbstractApplicationContext類的refresh方法,該方法包含了整個IOC容器初始化所有方法。其中有一個registerListeners()方法就是注冊系統(tǒng)監(jiān)聽者(spring自帶的)和自定義監(jiān)聽器的。
看registerListeners的關(guān)鍵方法體,其中的兩個方法addApplicationListener和addApplicationListenerBean,從方法可以看出是添加監(jiān)聽者。
for (ApplicationListener<?> listener : getApplicationListeners()) {
getApplicationEventMulticaster().addApplicationListener(listener);
}
// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let post-processors apply to them!
String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
for (String listenerBeanName : listenerBeanNames) {
getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
}
那么最后將監(jiān)聽者放到哪里了呢?就是ApplicationEventMulticaster接口的子類

該接口主要兩個職責(zé),維護ApplicationListener相關(guān)類和發(fā)布事件。
實現(xiàn)是在默認(rèn)實現(xiàn)類AbstractApplicationEventMulticaster,最后將Listener放到了內(nèi)部類ListenerRetriever兩個set集合中
private class ListenerRetriever {
public final Set<ApplicationListener<?>> applicationListeners = new LinkedHashSet<>();
public final Set<String> applicationListenerBeans = new LinkedHashSet<>();
ListenerRetriever又被稱為監(jiān)聽器注冊表。
2.Spring如何發(fā)布的事件并通知的監(jiān)聽者?
該問題開始是在ApplicationContext.publishEvent的方法,該方法調(diào)用路線
AbstractApplicationContext.publishEvent→SimpleApplicaitonEventMulticaster.multicastEvent→SimpleApplicaitonEventMulticaster.invokeListener→SimpleApplicaitonEventMulticaster.doInvokeListener→調(diào)用系統(tǒng)及自定義listener的onApplicationEvent方法,這個就是發(fā)布事件并通知的調(diào)用路線。
這個注意的有兩個方法
multicastEvent方法,該方法有兩種方式調(diào)用invokeListener,通過線程池和直接調(diào)用,進(jìn)一步說就是通過異步和同步兩種方式調(diào)用
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
Executor executor = getTaskExecutor();
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
}
最后看doInvokeListener方法
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
try {
//直接調(diào)用了listener接口的onApplicationEvent方法
listener.onApplicationEvent(event);
}
}
四、最后一張圖總結(jié)

上圖包含了在Spring如何自定義事件、監(jiān)聽器以及發(fā)布事件和通知監(jiān)聽者的原理,大家可以自己梳理下。
相關(guān)文章
Java常用HASH算法總結(jié)【經(jīng)典實例】
這篇文章主要介紹了Java常用HASH算法,結(jié)合實例形式總結(jié)分析了Java常用的Hash算法,包括加法hash、旋轉(zhuǎn)hash、FNV算法、RS算法hash、PJW算法、ELF算法、BKDR算法、SDBM算法、DJB算法、DEK算法、AP算法等,需要的朋友可以參考下2017-09-09
Windows下Java調(diào)用可執(zhí)行文件代碼實例
這篇文章主要介紹了Windows下Java調(diào)用可執(zhí)行文件代碼實例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-12-12
教你用springboot連接mysql并實現(xiàn)增刪改查
今天教各位小伙伴用springboot連接mysql并實現(xiàn)增刪改查功能,文中有非常詳細(xì)的步驟及代碼示例,對正在學(xué)習(xí)Java的小伙伴們有非常好的幫助,需要的朋友可以參考下2021-05-05
Spring?Boot?Actuator?漏洞利用小結(jié)
spring對應(yīng)兩個版本,分別是Spring Boot 2.x和Spring Boot 1.x,因此后面漏洞利用的payload也會有所不同,這篇文章主要介紹了Spring?Boot?Actuator?漏洞利用小結(jié),需要的朋友可以參考下2023-11-11

