SIer だけど技術やりたいブログ

Spring Framework で同一アプリ内でのイベントを扱う(ApplicationEvent、EventPublisher)

Java Spring SpringBoot Servlet

Spring Frameworkには、イベントを扱うための機能がある。
イベントの登場人物は以下。

名前役割実装方法
Publisherイベントを発行するApplicationEventPublisher
Listenerイベントを受け取る@EventListener、ApplicationListener

なぜイベントを使うのか

イベントを利用すると、コンポーネント間を疎結合に実装することができる。
あるイベントが起きたときに、そのイベントきっかけで実施しないといけない処理が多くなるようなときに使うと、そのイベントに対してどのような処理をするべきかという責務が特定のServiceクラスにふくれあがることなく、それぞれのListenerにもっていけるので見通しがよくなる。(と思う)
また、ライブラリでイベントを発行しておけば、ライブラリ本体に手を入れなくても特定タイミングで処理をはさみこめるようになる。(Springコンテナの初期化が終わったらxxxの処理をしたい、とか)

デフォルトで提供されているイベント

Spring Frameworkにはデフォルトでいくつかのイベントが定義されているので、アプリケーションコンテキストのCRUDに関する任意のタイミングで、アプリケーション実装者が処理をはさみこめるようになっている。

イベント名イベントが発生するタイミング
ContextRefreshedEventConfigurableApplicationContext のrefresh()
ContextStartedEventConfigurableApplicationContextのstart()
ContextStoppedEventConfigurableApplicationContextのstop()
ContextClosedEventConfigurableApplicationContextのclose()
RequestHandledEventリクエスト処理が終わったとき(WEB限定)

Listenerの実装方法

Listenerの実装方法は2通りある。

  • ApplicationListenerを実装する方法(~spring4.1)
  • @EventListenerアノテーションを使う方法(spring4.2~)

ApplicationListenerを実装する方法(~spring4.1)

ApplicationListener<T>のTにハンドリングしたいイベントを指定する。

@Slf4j
@Component
public class BeforeSpring42Listener implements ApplicationListener<ContextClosedEvent> {
  @Override
  public void onApplicationEvent(ContextClosedEvent event) {
    log.info("bood bye");
  }
}

@Eventlistenerアノテーションを使う方法(spring4.2~)

メソッド引数にハンドリングしたいイベントを指定し、@EventListenerを付与する。

@Component
@Slf4j
public class AfterSpring42listener {
  @EventListener
  public void processContextClosedEvent(ContextClosedEvent event) {
    log.info("good bye");
  }
}

カスタムイベントの実装方法

上記以外に、任意のイベントを作成することもできる。

サンプルアプリの題材説明

カスタムイベントのサンプルを書いた。
題材はスロットマシーン。

通知するApplicationEventをカスタムイベントにする。

Publisher -> (ApplicationEvent) -> Listener
クラス役割
SlotMachine3回スロットを回すクラス。スロットを回すときにSlotStartEventを発行する。
Rotationスロット1回を表現するクラス
SlotStartEventスロットが始まったことを表現するイベント
BeforeSpring42ListenerApplicationListnerで実装したリスナ
AfterSpring42listener@EventListenerで実装したリスナ
FeverEvent大当たりが発生したことを表現するイベント

kimullaa/event-examplegithub.com

ApplicationEvent

ApplicationEventを拡張して任意のイベントを作成する。

public class SlotStartEvent extends ApplicationEvent {
  private final Rotation rotation;

  public SlotStartEvent(Object source, Rotation rotation) {
    super(source);
    this.rotation = rotation;
  }

  public Rotation getRotation() {
    return rotation;
  }
}

Publisher

ApplicationEventPublisher を使ってイベントを発行する。

@Component
@Data
@Slf4j
public class SlotMachine {
  private final ApplicationEventPublisher publisher;
  public void execute() {
    LongStream.rangeClosed(1, 3).forEach(i -> {
      log.info(">>>-------------------------");
      this.publisher.publishEvent(new SlotStartEvent(this, new Rotation(i)));
      log.info("<<<-------------------------");
    });
  }
}

Listener

ApplicationEventを受け取って処理する。

@Component
@Slf4j
public class BeforeSpring42Listener implements ApplicationListener<SlotStartEvent> {
  @Override
  public void onApplicationEvent(SlotStartEvent event) {
    log.info("before ver4.2 listen : " + event.getRotation());
  }
}
@Component
@Slf4j
public class AfterSpring42listener {
  @EventListener
  public void processExecuteStartEvent(SlotStartEvent event) {
    log.info("after ver4.2 listen : " + event.getRotation());
  }

実行結果

2016-09-23 11:50:52.290  INFO 5136 --- [           main] com.example.SlotMachine                  : >>>-------------------------
2016-09-23 11:50:52.310  INFO 5136 --- [           main] com.example.AfterSpring42listener        : after ver4.2 listen : Rotation(id=1)
2016-09-23 11:50:52.310  INFO 5136 --- [           main] com.example.BeforeSpring42Listener       : before ver4.2 listen : Rotation(id=1)
2016-09-23 11:50:52.310  INFO 5136 --- [           main] com.example.SlotMachine                  : <<<-------------------------
2016-09-23 11:50:52.310  INFO 5136 --- [           main] com.example.SlotMachine                  : >>>-------------------------
2016-09-23 11:50:52.311  INFO 5136 --- [           main] com.example.AfterSpring42listener        : after ver4.2 listen : Rotation(id=2)
2016-09-23 11:50:52.311  INFO 5136 --- [           main] com.example.BeforeSpring42Listener       : before ver4.2 listen : Rotation(id=2)
2016-09-23 11:50:52.311  INFO 5136 --- [           main] com.example.SlotMachine                  : <<<-------------------------
2016-09-23 11:50:52.311  INFO 5136 --- [           main] com.example.SlotMachine                  : >>>-------------------------
2016-09-23 11:50:52.311  INFO 5136 --- [           main] com.example.AfterSpring42listener        : after ver4.2 listen : Rotation(id=3)
2016-09-23 11:50:52.311  INFO 5136 --- [           main] com.example.BeforeSpring42Listener       : before ver4.2 listen : Rotation(id=3)
2016-09-23 11:50:52.311  INFO 5136 --- [           main] com.example.SlotMachine                  : <<<-------------------------

注意点

デフォルトの動作だと、イベントを発行したスレッドと同一のスレッドでListenerが実行される。(実行結果のログをみても、すべてmainスレッドで処理されているとわかる)
同期的に処理していればスレッドに紐づいたコンテキスト(トランザクションコンテキストやセキュリティコンテキスト)を取得できるため、ある時には有効だが、非同期に実行したい場合もある。

非同期にListenerを実行する

まず、@EnableAsyncで非同期を有効にしたうえでTaskExecutorを用意する。

@SpringBootApplication
@EnableAsync
public class EventExampleApplication {
  public static void main(String[] args) {
    SpringApplication.run(EventExampleApplication.class, args);
  }

  @Autowired
  private SlotMachine machine;

  @Bean
  public CommandLineRunner execute() {
    return args -> {
      machine.execute();
    };
  }

  @Bean
  public TaskExecutor getTaskExecutor(){
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(5);
    executor.setMaxPoolSize(10);
    executor.setQueueCapacity(100);
    return executor;
  }
}

@Eventlistenerを付与したメソッドに@Asyncを付与する。
ver4.2以前の場合は、SimpleApplicationEventMulticasterにThreadPoolTaskExecutorを指定してBean定義すればいけそうだけど、めんどくさいから試してない。

@Component
@Slf4j
public class AfterSpring42listener {
  @EventListener
  @Async
  public void processExecuteStartEvent(SlotStartEvent event) {
    log.info("after ver4.2 listen : " + event.getRotation());
  }

実行結果

2016-09-23 12:10:03.511  INFO 3556 --- [tTaskExecutor-1] com.example.AfterSpring42listener        : after ver4.2 listen : Rotation(id=1)
2016-09-23 12:10:03.521  INFO 3556 --- [tTaskExecutor-4] com.example.AfterSpring42listener        : after ver4.2 listen : Rotation(id=3)
2016-09-23 12:10:03.521  INFO 3556 --- [tTaskExecutor-3] com.example.AfterSpring42listener        : after ver4.2 listen : Rotation(id=2)

@EventListenerでSpEL式を使う

Spring Framework4.3からは、@EventListenerにSpEL式が記述できるようになった。
また、メソッドの戻り値にApplicationEventを取ると、イベントを発行することができる。(ver4.2~)

  @EventListener(condition = "#event.rotation.id == 2")
  public FeverEvent conditionOn2(SlotStartEvent event) {
    log.info("fever event start");
    return new FeverEvent(event, event.getRotation());
  }  

まとめ

同一ApplicationContext上でのイベントを気軽に作れる。

参考