Google guava原始碼之EventBus

Java極客發表於2020-03-22

Google guava原始碼之EventBus

Java極客  |  作者  /  鏗然一葉
這是Java極客的第 49 篇原創文章

1、學習開原始碼的收穫

學習開原始碼可以有以下收穫:

1.學些其架構設計思想,看看怎麼實現一個高可用,可擴充套件的架構。

2.學習一些好的java語法,畢竟你在實際程式碼過程中不會使用到所有的java語法,而在看原始碼的過程中,就有可能發現你未曾使用過,但比較巧妙的用法。

3.學習設計模式,開原始碼常常會使用到一些設計模式,可以加深你對設計模式的理解。

4.學習一些基本設計原則,比如通過有界佇列避免記憶體溢位,通過非同步執行緒提高效能,通過依賴注入提高擴充套件性,可測性等等。

2、EventBus是什麼

EventBus 是Google的一個開源庫,它利用釋出/訂閱者者模式來對專案進行解耦。它可以利用很少的程式碼,來實現多元件間通訊。

3、EventBus程式碼結構

EventBus程式碼結構如下:

Google guava原始碼之EventBus

類說明:

3.1、EventBus

EventBus是核心入口類,如果全都採用預設實現,只需要實現Listener,並通過呼叫EventBus類的方法則可完成所有功能。

3.2、SubscriberExceptionHandler

SubscriberExceptionHandler是異常處理介面,可替換自己的實現。

3.3、Executor

Executor用於非同步執行Listener的監聽方法,可替換自己的實現。

3.4、Dispatcher

Dispatcher是event派發介面,可替換自己的實現,預設提供了3個實現類。

3.5、SubscriberRegistry

SubscriberRegistry是事件註冊類,也用於獲取訂閱者。

3.6、Subscriber

Subscriber是訂閱者,對Listener做了封裝,遮蔽了複雜的呼叫邏輯,使得使用者不必關心這些複雜邏輯,只要提供Listener的具體實現則可。

3.7、SynchronizedSubscriber

SynchronizedSubscriber是支援併發呼叫的訂閱者,可以通過在Listener的事件監聽方法上新增AllowConcurrentEvents註解來達到使用SynchronizedSubscriber的目的。

3.8、Subscribe

Subscribe是一個註解類,可以在任何類的方法上新增該註解來表達該方法是一個事件監聽方法。

3.9、DeadEvent

DeadEvent用於記錄那些已經沒有訂閱者的事件。

3.10、SubscriberExceptionContext

SubscriberExceptionContext是異常上下文,用於當訂閱者處理異常時記錄相關的上下文資訊,方便異常處理實現類獲得這些資訊來處理異常。

4、EventBus的優秀之處

1.面向介面程式設計:Executor,Dispatcher,SubscriberExceptionHandler都是介面,使用者可以替換具體的實現類。

2.使用依賴注入,使得可測性增強。從圖中可以看到EventBus持有Executor,Dispatcher,SubscriberExceptionHandler三個物件,它們可通過EventBus的構造器注入,這樣使用者就有機會方便的替換具體的實現,或者mock一個物件來用於測試。如果它們不是可注入的,而是直接在某個方法內呼叫,就失去了替換的機會。當然,介面注入的方式還有很多,例如通過set方法,通過反射動態生成等等,但構造器注入是最簡單,最省心的方法。

3.非同步處理,提高效能。Subscriber最終處理event是通過Executor以非同步執行緒方式執行,不會因為同步而阻塞,大大提高了效能。

4.利用Subscribe註解+反射,使得任何類的任何方法都可以成為事件監聽類,而不需要實現特定的監聽者介面。

5.考慮了異常處理機制,一方面提供了SubscriberExceptionHandler介面讓使用者來實現具體的處理邏輯,另外一方面提供了DeadEvent類,將那些失去訂閱者的事件統一歸類到DeadEvent,由使用者自行實現一個Listener去處理它們。

6.通過模板方法固化了整個呼叫流程,使用者只需要按照要求簡單實現則可使用。

5、EventBus具體使用過程

5.1、實現一個Listener

5.1.1、使用說明

Listener是一個事件監聽類,一個Listener類可以通過不同的方法同時監聽多個事件,任何類都可以作為Listener,但需要遵循以下要求:

1.必須在監聽方法上新增Subscribe註解,用以表達該方法是一個監聽方法

2.此監聽方法只能有一個引數,這個引數就是要監聽的事件,引數類的Class可以理解為就是要監聽的EventType

5.1.2、相關程式碼

1.Listener例子

public class MyListener {

  // 新增Subscribe註解則表示要監聽某個事件
  @Subscribe
  public void onEvent(MyEvent1 event1) {
    // do something
  }
  
  // 一個Listener可以監聽多個事件
  @Subscribe
  public void onEvent(MyEvent2 event2) {
    // do something
  }
}
複製程式碼

2.SubscriberRegistry.java:

  // 傳入的clazz類就是Listener的Class
  private static ImmutableList<Method> getAnnotatedMethodsNotCached(Class<?> clazz) {
    Set<? extends Class<?>> supertypes = TypeToken.of(clazz).getTypes().rawTypes();
    Map<MethodIdentifier, Method> identifiers = Maps.newHashMap();
    for (Class<?> supertype : supertypes) {
      for (Method method : supertype.getDeclaredMethods()) {
        // 這裡查詢方法上是否有Subscribe註解
        if (method.isAnnotationPresent(Subscribe.class) && !method.isSynthetic()) {
          // TODO(cgdecker): Should check for a generic parameter type and error out
          Class<?>[] parameterTypes = method.getParameterTypes();
          // 這裡檢查方法的引數只有1個
          checkArgument(
              parameterTypes.length == 1,
              "Method %s has @Subscribe annotation but has %s parameters."
                  + "Subscriber methods must have exactly 1 parameter.",
              method,
              parameterTypes.length);

          MethodIdentifier ident = new MethodIdentifier(method);
          if (!identifiers.containsKey(ident)) {
            identifiers.put(ident, method);
          }
        }
      }
    }
    return ImmutableList.copyOf(identifiers.values());
  }
複製程式碼

5.2、構造EventBus

5.2.1、使用說明

1.在一個系統中,根據用途不同,可以同時存在多個EventBus,不同的EventBus通過identifier來識別。

2.為方便使用,EventBus提供了多個構造器,使用者可以根據需要注入不同的實現類,最簡單的構造器是一個無參構造器,全部使用預設實現。

3.在實際使用過程中,可以使用一個單例類來持有EventBus例項,如有需要,可以持有不同的EventBus例項用於不同的用途。

5.2.2、相關程式碼

1.EventBus.java

  //無參構造器
  public EventBus() {
    this("default");
  }
  
  //指定識別符號構造器
  public EventBus(String identifier) {
    this(
        identifier,
        MoreExecutors.directExecutor(),
        Dispatcher.perThreadDispatchQueue(),
        LoggingHandler.INSTANCE);
  }
  
  //注入自定義異常類構造器
  public EventBus(SubscriberExceptionHandler exceptionHandler) {
    this(
        "default",
        MoreExecutors.directExecutor(),
        Dispatcher.perThreadDispatchQueue(),
        exceptionHandler);
  }
  
  //注入所有引數構造器,需注意的是,此方法不是public的,只能在包內訪問。
  EventBus(
      String identifier,
      Executor executor,
      Dispatcher dispatcher,
      SubscriberExceptionHandler exceptionHandler) {
    this.identifier = checkNotNull(identifier);
    this.executor = checkNotNull(executor);
    this.dispatcher = checkNotNull(dispatcher);
    this.exceptionHandler = checkNotNull(exceptionHandler);
  }
複製程式碼

5.3、註冊Listener

5.3.1、使用說明

以上兩步完成後,即可通過EventBus來註冊Listener。

5.3.2、相關程式碼

1.EventBus.java

  private final SubscriberRegistry subscribers = new SubscriberRegistry(this);
  
  public void register(Object object) {
    subscribers.register(object);
  }
複製程式碼

2.SubscriberRegistry.java

  void register(Object listener) {
    // 查詢有Subscribe註解的方法,並封裝為Subscriber,Multimap的key記錄的Class就是要監聽的物件的Class
    Multimap<Class<?>, Subscriber> listenerMethods = findAllSubscribers(listener);

    for (Entry<Class<?>, Collection<Subscriber>> entry : listenerMethods.asMap().entrySet()) {
      Class<?> eventType = entry.getKey();
      Collection<Subscriber> eventMethodsInListener = entry.getValue();

      CopyOnWriteArraySet<Subscriber> eventSubscribers = subscribers.get(eventType);

      if (eventSubscribers == null) {
        CopyOnWriteArraySet<Subscriber> newSet = new CopyOnWriteArraySet<>();
        eventSubscribers =
            MoreObjects.firstNonNull(subscribers.putIfAbsent(eventType, newSet), newSet);
      }

      eventSubscribers.addAll(eventMethodsInListener);
    }
  }
複製程式碼

5.4、釋出Event

5.4.1、使用說明

釋出事件比較簡單,只要在需要釋出事件的地方得到EventBus例項,然後呼叫post方法則可。

5.4.2、相關程式碼

1.EventBus.java

  public void post(Object event) {
    // 根據event找到訂閱者,這裡實際是根據event.Class來查詢,也即和Listener的監聽方法的引數的Class一致。
    Iterator<Subscriber> eventSubscribers = subscribers.getSubscribers(event);
    if (eventSubscribers.hasNext()) {
      //通過dispatcher來派發事件,最終呼叫的是Subscriber的dispatchEvent方法
      dispatcher.dispatch(event, eventSubscribers);
    } else if (!(event instanceof DeadEvent)) {
      // the event had no subscribers and was not itself a DeadEvent
      post(new DeadEvent(this, event));
    }
  }
複製程式碼

2.Subscriber.java

  final void dispatchEvent(final Object event) {
    // 通過executor來實現非同步呼叫,這個executor在EventBus是可注入的,可以注入修改後的實現類
    executor.execute(
        new Runnable() {
          @Override
          public void run() {
            try {
              invokeSubscriberMethod(event);
            } catch (InvocationTargetException e) {
              // 這裡最終呼叫的是在EventBus中注入的SubscriberExceptionHandler,可以注入修改後的實現類
              bus.handleSubscriberException(e.getCause(), context(event));
            }
          }
        });
  }
  
  void invokeSubscriberMethod(Object event) throws InvocationTargetException {
    try {
      //這裡的method就是Listener的監聽方法,target就是Listener物件,event就是這個監聽方法的入參
      method.invoke(target, checkNotNull(event));
    } catch (IllegalArgumentException e) {
      throw new Error("Method rejected target/argument: " + event, e);
    } catch (IllegalAccessException e) {
      throw new Error("Method became inaccessible: " + event, e);
    } catch (InvocationTargetException e) {
      if (e.getCause() instanceof Error) {
        throw (Error) e.getCause();
      }
      throw e;
    }
  }
複製程式碼

6、Google guava原始碼地址

github.com/google/guav…

end.


相關閱讀:
HikariPool原始碼(一)初識
HikariPool原始碼(二)設計思想借鑑


Java極客站點: javageektour.com/

相關文章