Spring Frameworkには、イベントを扱うための機能がある。
イベントの登場人物は以下。
名前 | 役割 | 実装方法 |
Publisher | イベントを発行する | ApplicationEventPublisher |
Listener | イベントを受け取る | @EventListener、ApplicationListener |
なぜイベントを使うのか
イベントを利用すると、コンポーネント間を疎結合に実装することができる。
あるイベントが起きたときに、そのイベントきっかけで実施しないといけない処理が多くなるようなときに使うと、そのイベントに対してどのような処理をするべきかという責務が特定のServiceクラスにふくれあがることなく、それぞれのListenerにもっていけるので見通しがよくなる。(と思う)
また、ライブラリでイベントを発行しておけば、ライブラリ本体に手を入れなくても特定タイミングで処理をはさみこめるようになる。(Springコンテナの初期化が終わったらxxxの処理をしたい、とか)
デフォルトで提供されているイベント
Spring Frameworkにはデフォルトでいくつかのイベントが定義されているので、アプリケーションコンテキストのCRUDに関する任意のタイミングで、アプリケーション実装者が処理をはさみこめるようになっている。
イベント名 | イベントが発生するタイミング | |
ContextRefreshedEvent | ConfigurableApplicationContext のrefresh() | |
ContextStartedEvent | ConfigurableApplicationContextのstart() | |
ContextStoppedEvent | ConfigurableApplicationContextのstop() | |
ContextClosedEvent | ConfigurableApplicationContextの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
クラス | 役割 |
SlotMachine | 3回スロットを回すクラス。スロットを回すときにSlotStartEventを発行する。 |
Rotation | スロット1回を表現するクラス |
SlotStartEvent | スロットが始まったことを表現するイベント |
BeforeSpring42Listener | ApplicationListnerで実装したリスナ |
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上でのイベントを気軽に作れる。