痛點:手動實現監聽器的繁瑣儀式
在 Java 中實現觀察者模式(Event/Listener)時,開發者往往需要重複編寫:
維護 List 集合(線程安全考量)
實現 addListener / removeListener 方法
遍歷列表調用回調,並處理單個監聽器異常不影響整體
針對不同事件類型定義專用接口(ActionListener, ChangeListener...)
這不僅增加代碼體積,更分散核心業務邏輯。有沒有一個通用組件,能像發送廣播一樣簡單觸發事件?
方案:基於反射的通用 Notifier
核心思想:約定大於配置 。監聽器只需實現特定名稱的方法(如 actionPerformed(Event)),Notifier 通過反射動態查找並調用,無需實現特定接口。
核心實現(增強版)
package com.generationjava.lang;
import java.util.*;
import java.lang.reflect.*;
import java.util.concurrent.*;
public class Notifier {
// 使用 CopyOnWriteArrayList 保證線程安全,遍歷無需加鎖
private final List listeners = new CopyOnWriteArrayList<>();
private final String listenerMethod;
private final Class>[] eventParamTypes;
/**
* @param methodName 監聽器方法名,如 "actionPerformed"
* @param eventType 事件參數類型,如 ActionEvent.class
*/
public Notifier(String methodName, Class> eventType) {
this.listenerMethod = methodName;
this.eventParamTypes = new Class[]{eventType};
}
public void addListener(Object listener) {
if (listener == null) throw new NullPointerException("Listener cannot be null");
listeners.add(listener);
}
public void removeListener(Object listener) {
listeners.remove(listener);
}
public void notify(EventObject event) {
if (event == null) throw new NullPointerException("Event cannot be null");
// 快速失效:事件類型不匹配直接拋異常,而非靜默忽略
if (!eventParamTypes[0].isInstance(event)) {
throw new IllegalArgumentException("Event type mismatch: expected " +
eventParamTypes[0].getName() + ", got " + event.getClass().getName());
}
for (Object listener : listeners) {
try {
Method method = findListenerMethod(listener.getClass());
method.invoke(listener, event);
} catch (InvocationTargetException e) {
// 解包真實業務異常,避免反射包裹層干擾排查
Throwable cause = e.getCause();
System.err.println("Listener " + listener + " failed: " + cause);
cause.printStackTrace();
} catch (Exception e) {
System.err.println("Reflection error for listener " + listener + ": " + e);
}
}
}
// 緩存 Method 查找結果,提升性能
private final ConcurrentMap, Method> methodCache = new ConcurrentHashMap<>();
private Method findListenerMethod(Class> listenerClass) {
return methodCache.computeIfAbsent(listenerClass, cls -> {
try {
// 支持繼承鏈查找
Method method = cls.getMethod(listenerMethod, eventParamTypes);
// 允許非 public 方法(需設置 accessible)
if (!method.isAccessible()) method.setAccessible(true);
return method;
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException(
"Listener class " + cls.getName() +
" must have method " + listenerMethod + "(" + eventParamTypes[0].getSimpleName() + ")"
);
}
});
}
}
使用示例:極簡調用
// 1. 定義事件源持有 Notifier
public class Button {
private final Notifier notifier = new Notifier("actionPerformed", ActionEvent.class);
public void addActionListener(Object listener) { notifier.addListener(listener); }
public void removeActionListener(Object listener) { notifier.removeListener(listener); }
public void click() {
notifier.notify(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "click"));
}
}
// 2. 監聽器無需實現任何接口,只需約定方法名
class MyHandler {
public void actionPerformed(ActionEvent e) {
System.out.println("Button clicked: " + e.getSource());
}
}
// 3. 組裝
Button btn = new Button();
btn.addActionListener(new MyHandler());
btn.click(); // 輸出: Button clicked: Button@...
進階優化與避坑指南
維度 原版缺陷 增強策略
線程安全
ArrayList 非線程安全,遍歷中增刪拋 ConcurrentModificationException
採用 CopyOnWriteArrayList,讀多寫少場景無鎖遍歷
反射性能
每次通知反射查找 Method,開銷大
ConcurrentHashMap 緩存 Method 對象,首次查找後 O(1) 獲取
異常處理
printStackTrace() 吞沒異常,難以排查
解包 InvocationTargetException,記錄真實業務異常堆棧;單個監聽器失敗不影響其他
類型安全
運行時才發現方法不存在/參數不匹配
構造器檢查事件類型;findListenerMethod 啟動時驗證監聽器合規性
內存洩漏
監聽器未移除導致無法 GC
建議搭配 WeakReference 實現 WeakNotifier 變體,或規範生命周期管理
適用邊界與替代方案
適用: 輕量級 UI 組件、領域事件發布、插件擴展點、測試雙模擬事件總線。
不適用: 高吞吐核心鏈路(反射開銷)、強類型契約要求場景、需異步/持久化事件總線場景。
現代替代:
Guava EventBus(註解驅動、異步支持、死事件檢測)
Spring ApplicationEventPublisher(容器集成、事務綁定)
RxJava Subject/Processor(響應式流、背壓控制)
結語
Notifier 類以約 50 行代碼,通過反射機制消除了 Event/Listener 模式 80% 的樣板代碼。它展示了「約定優於配置」在 Java 元編程中的實踐價值。雖然生產級系統建議採用成熟事件總線框架,但理解並能手寫一個線程安全、高性能的 Notifier,是掌握 Java 反射與組件設計的極佳練習。下次當你第 N 次寫 for (Listener l : listeners) l.onEvent(e) 時,不妨引入這個小工具,讓代碼回歸業務本質。