Java用独自イベントシステムEventBusの紹介

2019年4月2日

※弊社製オープンソースソフトはオープンソースソフトウェアとしてまとめているので参照されたい。

※EventBusについては/tag/eventBusに投稿一覧がある。

ここでは独自に作成したイベントシステムEventBusを紹介する。GuavaやGWTにもEventBusというシステムがあるが、それとは全く別物であるので注意されたい。しかし、それらよりもはるかに使い勝手は良いと自負しているので、是非使っていただきたい。

githubリポジトリはhttps://gitlab.com/ysugimura/eventBusにある。
Mavenリポジトリはhttps://ysugimura.github.io/mavenになる。

Java歴代のイベントシステム

ここでは、簡単にJava歴代のイベントシステムの主なものをふりかえってみる。

Swing

Swing以前のおそらくAWTも同様だったのかもしれないが、使ったことがない。Swingでは、例えば、ボタンが押されたら何かする場合は以下を書く。

    JButton b = new JButton();
    b.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        System.out.println("clicked"); 
      }      
    });

これは本当に面倒で仕方なかった、リスナーの登録も面倒なら、新たなイベントを登録するのも非常に面倒。以下を記述しなくてはいけない。

  • リスナー登録のメソッド
  • リスナーインターフェース
  • アクションオブジェクト(イベントオブジェクト)

もちろん後の二つは使いますことはできるのだが。

SWT

SWT(Eclipse)のイベントシステムはSwingとほぼ同じで、言及するに値しない。

GWT

GWT(Google Web Toolkit)のEventBusは、上に比較するとはるかに洗練されたものであった。例えば以下のように記述する。

EventBus bus = new SimpleEventBus();

AdminAccountModelEvent.Handler handler = ....
bus.addHandler(AdminAccountModelEvent.TYPE, handler);

AdminAccountModelEvent event = new AdminAccountModelEvent(...);
eventBus.fireEvent(event);

AdminAccountModelEventというイベントクラスとハンドラクラスを定義する。後はそのイベントクラスを指定してハンドラを登録する。そして、イベントオブジェクトを作ってイベントを発行する。ただ、このイベントクラスが少々面倒で、基本的には以下のようになる。

public class AdminAccountModelEvent extends GwtEvent<AdminAccountModelEvent.Handler> {

  public final static Type<Handler>TYPE = new Type<Handler>();

  public final AdminLoginInfo loginInfo;

  AdminAccountModelEvent(AdminLoginInfo info) {
    loginInfo = info;
  }

  @Override
  public Type<Handler> getAssociatedType() {
    return TYPE;
  }

  @Override
  protected void dispatch(Handler handler) {
    handler.onChanged(this);
  }

  public interface Handler extends EventHandler {
    public void onChanged(AdminAccountModelEvent e);
  }

  public interface HasHandlers extends com.google.gwt.event.shared.HasHandlers {
    public HandlerRegistration addChangedHandler(Handler handler);
  }
}

もう少し簡単にならないものか?

GuavaのEventBus

GoogleによるGuavaというライブラリの中に、やはりEventBusという名前の付くものがある。これは非常に簡単に扱える。が、正直申し上げて、これは信じられない位ひどい設計だ

public class Guava {

  public static void main(String[]args) {
    EventBus bus = new EventBus("bus");
    bus.register(new SampleListener());
    bus.post(new Event());
  }

  static class Event {    
  }

  public static class SampleListener {
    @Subscribe
    public void receive(Event event) {
      System.out.println("hello " + event);
    }
  }
}

この設計のひどさと言ったら言語道断ものだと思うのだが、どう思われるか?私は絶対にこれを使うことは無いと思う。

  • @Subscribeを書き忘れると何も言わずに何もしない。
  • @Subscribeが必須なので、Java8のラムダ式やメソッド参照が使えない。registerの引数がObject型になってる。
  • イベントの型が違っていたら何も言わずに何もしない。

こんなものを疑問も無く使っている者がいることを私は信じられないのだが。。。

古いイベントシステム全般について

もちろん、以上のイベントシステムはJava8登場以前に設計されたものであるし、Java8がどのような仕様となるのか不明であったから、スムーズな以降も考慮されていなかっただろう。

独自のイベントシステムEventBusの説明

今回作成した独自イベントシステムEventBusだが、当然のことながらJava8以降を前提としている。コードを変更すればそれ以前でも使用できると思われるが、あえてJava8以降のjava.util.function.Consumerを全面的に使用している。

基本的な使い方

使い方は極めて簡単である。

EventBus bus = new EventBus();

bus.listen(Event.class, e-> { System.out.println("hello " + e); });

bus.dispatchEvent(new Event());

これだけだ。Eventクラスは何でも良い。任意の型が可能である。

public class Event {
}

dispatchEventについては説明不要と思われる。問題はlistenの方だが、以下のような定義にしてある。

  public <T> Unlistener<T> listen(Class<T> eventType, Consumer<T>listener) {
    ....
  }

つまり、指定されたイベント型のConsumerのみをリスナーとして受け入れる。このため、eventTypeで指定したクラスを引数とするConsumer型のラムダ式あるいはメソッド参照を記述することができる。

Unlistenerによる登録解除

返り値のUnlistenerはリスナー登録を解除するためのものである。GUI等の構築では、ほぼ確実にリスナー登録を解除する必要は生じないのだが、他の場面では解除したい場合もある。そのような場合には、Unlistenerを保存しておき、後からそれを呼び出せばよい。

EventBus bus = new EventBus();

Unlistener<Event>unlistener = bus.listen(Event.class, e-> { System.out.println("hello " + e); });

bus.dispatchEvent(new Event());

unlistener.unlisten();

さらに、Unlistenerがnullだった場合のときのため、nullの場合は何もしないstaticメソッドも用意してある。例えば以下のように使用する。

private Unlistener unlistener;

void register() {
  unlistener = bus.listen(Event.class, e-> {});
}

void unregister() {
  Unlistener.execute(unlistener);
}

弱参照によるリスナー

リスナーを弱参照で保持するメソッドも用意してある。ただし、この場合には保持する通知先を指定しなければならない。リスナーだけを弱参照しても所望の結果にはならないからだ。これは以下のメソッドになる。

  public <T> Unlistener<T> listenWeak(Class<T> eventType, Object object, Consumer<T> listener) {
    ....
  }

これは以下を参考にさせていただいた。この場でお礼申し上げる。

おまけ:CallerStackの組み込み

イベントはアプリ内のあちこちで発火される場合がある。デバッグ用としてアプリ内のどの部分から発火されたものかを調べるために、CallerStackを組み込んである。これはJavaスタックトレースから呼び出し元を取得するにて説明している。