Spring5.0 + KotlinではDoma、Request Interceptorあたりはどうなっているのか調べてみた

Spring5.0のリリースが迫るなか、プロジェクトへ導入に向けて色々と調べている。インタセプターなどのSpring Frameworkにおける作法はどうなっているか、便利に使えていたライブラリとの相性はどうなのか、などをアウトプットしていく。

次の2つのはてな?を調べてみた。

  • DBアクセスにはDomaを使いたい。Spring Transactionalの@Transactionalも合わせて使える?
  • HandlerInterceptorAdapter@ControllerAdviceなどAOPがRouterFunctionで使いたいけど、どうすれば?

それでは1つずつ整理していく。

Domaとの相性

Spring Boot 1.5系とDomaの相性は良くSpring Transactionalの@Transactionalも問題なく使えている。
@Transactionalの使い勝手は良くアノテーションを付けたメソッド無いでRuntimeExceptionが発生すればロールバックしてくれる。

@Transactional
override fun invoke(command: CreateTaskCommand): Task {
    return taskRepository.create(command.title).fold({
        task -> task
    }, {
        error -> throw handle(error)
    })
}

com.mysql.jdbc.ReplicationDriverのドライバを使い @Transactional(readOnly = true)をつければスレーブに対してDBアクセスが行われる。

DomaのKotlinサポートの対応方法をベースにSpringBoot 2.0で試してみたところ、問題なく使えた!

Domaとの相性は問題ないと結果が得られたのでDBアクセスには引き続きDomaを使っていく。

動いているコードはgithubにあるので合わせて参照ください。

RouterFunctionでリクエストやExceptionを良しなに処理したい

注意!FilterFunctionを用いたエラーハンドリングは正しくありません。Exceptionをエラーハンドリングしたい場合は WebExceptionHandlerを使います。 詳細は次のエントリにまとまっています。

blog.soushi.me


HTTPリクエストをハンドリングするアプリケーションの関心事として次のようなものがある。

  • HTTPリクエストをインターセプトして共通の処理を入れたい
  • エラーレスポンスの扱いを共通化したい

これらの関心事をSpring5.0から導入されるRouterFunctionでは、どのように解決すれば良いか。
Spring4系であればHTTPリクエストのインターセプトにはHandlerInterceptorAdapterを実装したクラスを用意すれば良かった。サービス層で発生したエクセプションに応じてエラーレスポンスを定義するクラスを@ControllerAdviceのアノテーションをつけたクラスを用意すれば良かった。

RouterFunctionでは、FilterFunctionをつかって2つの関心事を解決できる。
コードとしては次のようになる。

@Configuration
class TaskRoutes(private val taskHandler: TaskHandler, private val exceptionFilter: ExceptionFilter) {

    @Bean
    fun taskRouter() = router {
        (accept(APPLICATION_JSON) and "/api").nest {
            "/task".nest {
                POST("/", taskHandler::create)
                GET("/{id}", taskHandler::fetchByTaskId)
                PUT("/{id}", taskHandler::updateByTaskId)
                DELETE("/{id}", taskHandler::deleteByTaskId)
                PUT("/{id}/finish", taskHandler::finishByTaskId)
            }
            "/tasks".nest {
                GET("/", taskHandler::fetchAll)
            }
        }
    }.filter(exceptionFilter())
}

@Component
class ExceptionFilter {

    private val logger = KotlinLogging.logger {}

    operator fun invoke(): (request: ServerRequest, next: HandlerFunction<ServerResponse>) -> Mono<ServerResponse> = { request, next ->
        try {
            next.handle(request)
        } catch (e: Exception) {
            when (e) {
                is SystemException -> status(e.status).json().body(Mono.just(ErrorItem(e.message ?: "web application error", e.status.value().toString(), null)))
                else -> {
                    logger.error(e) { "unknown exception: %s".format(e.message ?: "unknown error") }
                    status(HttpStatus.INTERNAL_SERVER_ERROR).json().body(Mono.just(ErrorItem(e.message ?: "internal server error", null, null)))
                }
            }
        }
    }
}

.filter(exceptionFilter())の箇所でFilter Functionを定義している。
コードの例ではExceptionFilterクラスでは各APIで発生したExceptionをフィルタリングしている。Exceptionが発生した場合はエラーレスポンスを返すようにしている。これで例外処理をハンドリングできるインタセプターをRouterFunctionに追加できる。

Spring4系で HandlerInterceptorAdapter@ControllerAdviceで分かれていたところがFilterFunctionに集約された格好である。

コード

コードを書きながら調べたのでgithubと合わせて確認いただけます。

github.com

まとめ

  • Domaとの相性が問題ないことやHTTPハンドリングの関心事もRouterFunctionでも解決できることが分かった。
  • 並行してSpringFoxとの相性も調べている。APIドキュメントの快適さもSpring4系を使う恩恵があったのでSpring5.0からも問題なく使えることを期待している。