SpringBootの非同期イベント発行に必要な手順

SpringFrameworkには、Spring管理下にあるインスタンスの実行を「イベント」として登録し、イベント発行で実行を制御できる仕組みが用意されています。使うのも単純ですし、SpringBootでは非同期処理にもできます。今回はSpringBootで非同期イベントを使った非同期処理の実現例を記載します。

# 動作環境

* Java 11
* SpringBoot 2.3.1

# イベント駆動処理に必要なこと

以下3つの作成が必須です

* 処理実行に必要なパラメータ類を渡すEventクラス
* イベントを起動させるPublisherクラス
* イベントを検知して処理を開始するListenerクラス

# 実装例

今回作成するイベント：呼び出し元のクラスから文字列を受け取り、ログに出力する

イベントから受け渡す文字列を格納するクラスは以下を用います。

```java:SampleMessage.java
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;

@AllArgsConstructor @Getter @ToString
public class SampleMessage {
	private String message;

	public static SampleMessage of(String message) {
		return new SampleMessage(message);
	}
}
```

## Event

org.springframework.context.ApplicationEvent を継承します。

```java:PrimaryEvent.java
@Getter
public class PrimaryEvent extends ApplicationEvent {

	private final SampleMessage sampleMessage;

	public PrimaryEvent(Object source,  SampleMessage sampleMessage) {
		super(source);
		this.sampleMessage = sampleMessage;
	}

}

```

コンストラクタの第1引数：Object sourceは必須実装です。

## Publisher

Springが管理している org.springframework.context.ApplicationEventPublisher インスタンスを使ったJavaクラスを作ります。そのため `@Component`や`@Service`など付与してSpring管理に置いたクラスにする[^1]と簡単です。

```java:PrimaryEventPublisher.java
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;

@Component
@AllArgsConstructor
public class PrimaryEventPublisher {

	private final ApplicationEventPublisher applicationEventPublisher;

	public void ignite(String message) {
		SampleMessage sampleMessage = SampleMessage.of(message);
		
		// イベント作成する
		PrimaryEvent event = new PrimaryEvent(this, sampleMessage);

		// イベント発行！
		applicationEventPublisher.publishEvent(event);
	}
}
```

## Listener

Spring管理下[^2]に置き、org.springframework.context.ApplicationListenerインタフェースを実装します。
一番簡単なのは`@Component`を付与します。

```java
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

import lombok.extern.log4j.Log4j2;

@Component
public class PrimaryEventListener implements ApplicationListener<PrimaryEvent>{

	@Override
	public void onApplicationEvent(PrimaryEvent event) {
		SampleMessage sampleMessage = event.getSampleMessage();
		
		// メッセージを受けた後の後続処理↓......
	}

}

```

以上でイベント実行の準備が完了です。

## SpringMVCのControllerからの実行例

これはControllerでリクエストを受けて、イベントを実行してから、画面を表示する例です。

```java:DisplayController.java
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import lombok.AllArgsConstructor;

@RequestMapping("/display")
@Controller
@AllArgsConstructor
public class DisplayController {

	private final PrimaryEventPublisher publisher;

	@GetMapping
	public ModelAndView display(ModelAndView mnv) {

		publisher.ignite("メッセージを送信します");

		mnv.setViewName("display");
		return mnv;
	}
}
```

これだけですとSpringMVCを使った実装と何ら変わらず、イベントが実行されて終了した後にControllerは画面を表示します。

# 非同期処理にする

SpringBootで非同期処理を使うには、`@EnableAsync`をSpringBootの起動クラスに付与した後、非同期処理にしたいクラスに`@Async`を付与します。

```java:SpringEventSampleApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class SpringEventSampleApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringEventSampleApplication.class, args);
	}

}

```

例えば、非同期で動作させたいのはController内にある以下のigniteメソッドにします。

```java:DisplayController.java
public class DisplayController {

	private final PrimaryEventPublisher publisher;

	@GetMapping
	public ModelAndView display(ModelAndView mnv) {
		// 画面表示とは別に処理をさせたい
		publisher.ignite("メッセージを送信します");

		mnv.setViewName("display");
		return mnv;
	}
}
```

PrimaryEventPublisherを非同期に動かしたいので、`@Async`を付けるだけです。

```java:PrimaryEventPublisher.java
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import lombok.AllArgsConstructor;

@Component
@AllArgsConstructor
@Async
public class PrimaryEventPublisher {

	private final ApplicationEventPublisher applicationEventPublisher;

	public void ignite(String message) {
		SampleMessage sampleMessage = SampleMessage.of(message);

		PrimaryEvent event = new PrimaryEvent(this, sampleMessage);
		applicationEventPublisher.publishEvent(event);
	}
}
```
非常に簡単ですね d(・ω・
実行できる処理には`@Repository`を使ってデータソースに出力も可能ですから、何らかの処理終了通知を社内チャットツールやメールに送信、なんてのにも使えます。

[^1]: 実際にはSpringのApplicationContextから取得できますので、`@Component`などのSpring管理下に置くためのアノテーション付与は必須ではありません。「Spring管理下＝ApplicationContextに登録しているクラス」です。なお、SpringBootではSpringのアノテーションである`@Controller`、`@Service`、`@Repository`、`@Component`が付与されたクラスの存在を検出する ComponentScan の機能が有効なパッケージ内にあることが前提です。

[^2]: Spring管理下にある ApplicationEventPublisher から実行されるため。ApplicationContextに登録しておかなければ動作しません（空振りします）