SpringBootでEventListenerの実装

エキサイト株式会社エンジニアの佐々木です。2021年Exciteホールディングスアドベントカレンダー17日目を担当させていただきます。

SpringBootでのイベントリスナーの実装についてです。

はじめに

SpringBootでは、アノテーションを用いたイベントリスナーの実装が比較的簡単にできるかと思います。

コード

今回は、イベント、パブリッシャー、リスナーと3つのクラスを書いていきます。

イベント

イベントのオブジェクトのコードは下記になります。

public class DemoEvent extends ApplicationEvent {
    private String message;

    public DemoEvent(Object source, String name) {
        super(source);
        this.message = name;
    }

    public String getMessage(){
        return this.message;
    }

}

ApplicationEventを継承して、あとはそれに必要な要素やメッセージを取得するメソッドをつけてあげれば実装完了です。

パブリッシャー

イベント発火を管理するパブリッシャーは下記のように記載します。

@Component
@AllArgsConstructor
public class DemoPublisher {

    private final ApplicationEventPublisher publisher; // Springで用意されている

    public void publishEvent(String name){
        publisher.publishEvent(new DemoEvent(this, name));
    }
}

Spring側で用意されているApplicationEventPublisherをDIし、そこにイベントのオブジェクトをセットするメソッドを実装しておきます。

リスナー

リスナーになります。パブリッシャーが発火したイベントを受け取るオブジェクトになります。

@Component
@AllArgsConstructor
@Slf4j
public class DemoListener {

    @EventListener // Springで用意されているアノテーションを付与
    public void onEvent(DemoEvent demoEvent){   // 引数にイベントのオブジェクトを設定する
        log.info("class: {} , event: {} , timestamp: {} ",
                new Object(){}.getClass().getEnclosingClass().getName(),
                demoEvent.getMessage(),
                demoEvent.getTimestamp()
        );
    }
}

リスナークラス自体を@ComponentでDIコンテナに登録し、実行したいメソッドに、@EventListnerを付与し、引数にイベントオブジェクトを設定します。

パブリッシャーの呼び出し

パブリッシャーを呼び出す処理を記述します。

@RestController
@RequestMapping
@AllArgsConstructor
public class DemoController {

    private final DemoPublisher publisher;  // パブリッシャーをDIする

    @GetMapping
    public void index(){
        publisher.publishEvent("index");
    }

}

今回は、コントローラにリクエストがあったら、パブリッシャーを呼んでもらう実装にしています。

実行

$ curl http://localhost:8080/

2021-12-17 07:59:16.248  INFO 14746 --- [nio-8080-exec-1] c.e.excite20211212.listner.DemoListener  : class: com.example.excite20211212.listner.DemoListener , event: index , timestamp: 1639695556247 

このようなログが出力されます。ちゃんとDemoListenerからログがでていることがわかります。

リスナーを増やす

アプリケーションが大きくなってきて、リスナーの処理を増やしたいが、別々で処理したいとします。その場合パブリッシャーとリスナーをそれぞれ修正せずとも、リスナーだけ増やせば可能になります。

@Component
@AllArgsConstructor
@Slf4j
public class Demo2Listener {  //クラス名変更

    @EventListener
    public void onEvent2(DemoEvent demoEvent){  // メソッド名も変更(変更しなくてもOK)
        log.info("class: {} , event: {} , timestamp: {} ",
                new Object(){}.getClass().getEnclosingClass().getName(),
                demoEvent.getMessage(),
                demoEvent.getTimestamp());
    }
}

これで、パブリッシャーに気付かれずにリスナーを増やすことが可能になりました。

$ curl http://localhost:8080/

2021-12-17 08:09:34.669  INFO 15034 --- [nio-8080-exec-2] c.e.e.listner.Demo2Listener              : class: com.example.excite20211212.listner.Demo2Listener , event: index , timestamp: 1639696174669 
2021-12-17 08:09:34.669  INFO 15034 --- [nio-8080-exec-2] c.e.excite20211212.listner.DemoListener  : class: com.example.excite20211212.listner.DemoListener , event: index , timestamp: 1639696174669 

まとめ

SpringのDIコンテナのおかげで、比較的簡単にイベント処理を疎結合に実装することが可能になりました。

おまけ

イベントは、ApplicationEventの継承がなくても動作しました。

public class DemoEvent  {
    private String message;

    public DemoEvent(Object source, String name) {
        this.message = name;
    }

    public String getMessage(){
        return this.message;
    }

}

ApplicationEventは、実行時間やシリアライズのインタフェースを実装してくれているので、継承はしていた方がいいと思いますが、DIがよしなに解釈して実行できていました。

最後に

エキサイトホールディングスアドベントカレンダー17日目を担当させていただきました。読んでいただいてありがとうございます。引き続き、アドベントカレンダーをお楽しみいただけると幸いです。

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

カジュアル面談はこちらになります! meety.net

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com