Spring BootでSSE


Spring BootでSSE(text/event-stream)やりてえ〜。やりてえよね。

ということでおあつらえ向きなSseEmitterがある。

簡単な使い方

1秒ごとに現在日時を返すやつ。非同期実行自体は https://qiita.com/takkkun/items/1364fdaac8ae2b9fdc01 を参照。

@Service
class SseService {

  @Async
  fun emitDateTime(emitter: SseEmitter) {
    var finished = false
    while (!finished) {
      TimeUnit.SECONDS.sleep(1)

      try {
        sse.send(LocalDateTime.now().toString())
      }
      catch (e: IOException) {
        finished = true
      }
    }
  }
}

@Controller 
class SseController(private val service: SseService) {

  @RequestMapping(/* ... */)
  fun sse(): ResponseBodyEmitter {
    val emitter = SseEmitter()
    service.emitDateTime(emitter)
    return emitter
  }
}

これで以下のようなレスポンスが返ってくる。

$ curl -i 'http://localhost:8080/sse'
HTTP/1.1 200
Content-Type: text/event-stream;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 20 Jan 2020 07:01:31 GMT

data:2020-01-20T16:01:32.155751

data:2020-01-20T16:01:33.164949

data:2020-01-20T16:01:34.168721

data:2020-01-20T16:01:35.174836

data:2020-01-20T16:01:36.179743

...

タイムアウト

デフォルトのタイムアウトはSpring Bootが定めている。具体的にはspring.mvc.async.request-timeoutというプロパティで。単位はミリ秒。

もしくはSseEmitterのコンストラクタでも指定できる。この場合、コンストラクタに与えた値が優先される。

無期限にしたい場合は0を指定。

実際にタイムアウトが起きた場合は、SseEmitter#onTimeoutで指定したコールバックが呼ばれ、AsyncRequestTimeoutExceptionが投げられる。

クライアントから切断

クライアントから切断された瞬間にそれを検出する術はない、と思う。

ただ、クライアントから切断されている状態でSseEmitter#sendを呼び出すと、IOException(実際はそのサブクラスのClientAbortExceptionなはず)が投げられるので、それで検出することは可能。

最終的にSseEmitter#onErrorで指定したコールバックが呼ばれるので、それでも良いかな?

ちなみに@Asyncの中でハンドルされていない例外が発生すると、非同期処理実行の仕組みの方でログを記録するので、ちゃんとハンドルしてあげた方がベターだと思う(JavaだとIOExceptionがチェック例外なので強制されると思うけど)。

サーバーから完了

SseEmitter#completeを呼ぶとレスポンスが完了する。

クライアントから切断することがほとんどだとは思うけど、用途によってはあり得るので一応。