Intercepter

この記事は

Intercepterを使用して、コントローラーの処理の前後に任意の処理を実行する方法を書く。
なお、REST APIを想定して書いている。

GETメソッドの作成

以下のような単純なエンドポイントを用意する。

import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class HelloController {

    @GetMapping("hello")
    fun hello(): ResponseEntity<HelloMessage> {
        return ResponseEntity.ok(HelloMessage("hello!"))
    }

    data class HelloMessage(val message: String)
}

/helloに対してリクエストを実行すると、{"message": "hello!"}が返ってくるだけの単純な処理である。 この処理の前後にログを出力するようなインターセプターを実装する。

インターセプターの実装

インターセプターの実装例は以下のようなものである。

import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.Logger
import org.springframework.stereotype.Component
import org.springframework.web.servlet.HandlerInterceptor
import org.springframework.web.servlet.ModelAndView
import java.lang.Exception

@Component
class LoggingInterceptor(
    private val logger: Logger
) : HandlerInterceptor {

    // コントローラーの実行前の処理を定義する
    override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
        logger.info("----------------preHandle")
        return super.preHandle(request, response, handler)
    }

    // コントローラーの実行後の処理を定義する。より正確には、レスポンス送信前の処理
    override fun postHandle(
        request: HttpServletRequest,
        response: HttpServletResponse,
        handler: Any,
        modelAndView: ModelAndView?
    ) {
        logger.info("----------------postHandle")
        super.postHandle(request, response, handler, modelAndView)
    }

    // レスポンス送信後の処理
    override fun afterCompletion(
        request: HttpServletRequest,
        response: HttpServletResponse,
        handler: Any,
        ex: Exception?
    ) {
        logger.info("----------------afterHandle")
        super.afterCompletion(request, response, handler, ex)
    }
}

ロギングコンポーネント

上述したLoggingInterceptorにインジェクションするLoggerBeanを用意する。以下のコンフィグクラスを作成することで、Loggerを利用しやすくなる。詳細についてはこちらを参照。

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.InjectionPoint
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Scope

@Configuration
class LoggerConfig {

    @Bean
    @Scope("prototype")
    fun logger(injectionPoint: InjectionPoint): Logger {
        val targetClass = injectionPoint.targetClass
            ?: throw IllegalArgumentException("couldn't obtain a target class for logger from $injectionPoint")
        return LoggerFactory.getLogger(targetClass)
    }

    private val InjectionPoint.targetClass: Class<*>?
        get() = methodParameter?.containingClass ?: field?.declaringClass
}

また、インターセプターを有効化するためにWebMvcConfigurerを継承したクラスに作成したインターセプターを登録するようなコードも必要となる。

import com.example.demo.controller.LoggingInterceptor
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

@Configuration
class WebMvcConfig(
    private val interceptor: LoggingInterceptor
) : WebMvcConfigurer {
    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(interceptor)
        // 他に登録したいインターセプターがある場合は同様に書いていく
        // registry.addInterceptor(interceptor2)
        // registry.addInterceptor(interceptor3)
    }
}

ここまでできれば先ほどと同様に/helloに対してリクエストを投げてみると、以下のようなログが出力されるようになるはず。

2023-02-07T12:05:28.568+09:00  INFO 258 --- [nio-8080-exec-1] c.e.demo.controller.LoggingInterceptor   : ----------------preHandle
2023-02-07T12:05:28.610+09:00  INFO 258 --- [nio-8080-exec-1] c.e.demo.controller.LoggingInterceptor   : ----------------postHandle
2023-02-07T12:05:28.610+09:00  INFO 258 --- [nio-8080-exec-1] c.e.demo.controller.LoggingInterceptor   : ----------------afterHandle

特定のアノテーションが付与されているメソッドのみに対する処理

続いて、特定のアノテーションが付与されているメソッドのみ実行する処理を書いてみる。 今回は@Helloアノテーションを作成し、このアノテーションが付与されているメソッドが実行された場合にログにhello! ${EXECUTED_METHOD}を出力してみようと思う。
まずはアノテーションから

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Hello {
}

続いて、インターセプター

import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.Logger
import org.springframework.core.annotation.AnnotationUtils
import org.springframework.stereotype.Component
import org.springframework.web.method.HandlerMethod
import org.springframework.web.servlet.HandlerInterceptor

@Component
class HelloInterceptor(
    private val logger: Logger
) : HandlerInterceptor {

    override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
        // HandlerMethodを取得し、実行したメソッドの名前・付与されているアノテーション情報を取得する
        val handlerMethod = handler as HandlerMethod
        val method = handlerMethod.method
        val methodName = method.name
        val helloAnnotation = AnnotationUtils.findAnnotation(method, Hello::class.java)
        helloAnnotation?.let {
            logger.info("hello!, $methodName")
        }
        return super.preHandle(request, response, handler)
    }
}

インターセプターの登録も忘れずに

@Configuration
class WebMvcConfig(
    private val interceptor: LoggingInterceptor,
    private val helloInterceptor: HelloInterceptor
) : WebMvcConfigurer {
    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(interceptor)
        // 追加
        registry.addInterceptor(helloInterceptor)
    }
}

上記までで準備が済んでいるので、/helloを実行してみると・・・

2023-02-07T12:44:32.165+09:00  INFO 2556 --- [nio-8080-exec-5] c.e.demo.controller.LoggingInterceptor   : ----------------preHandle
2023-02-07T12:44:32.166+09:00  INFO 2556 --- [nio-8080-exec-5] c.e.demo.controller.HelloInterceptor     : hello!, hello
2023-02-07T12:44:32.170+09:00  INFO 2556 --- [nio-8080-exec-5] c.e.demo.controller.LoggingInterceptor   : ----------------postHandle
2023-02-07T12:44:32.170+09:00  INFO 2556 --- [nio-8080-exec-5] c.e.demo.controller.LoggingInterceptor   : ----------------afterHandle

上記のようなログが出力される。それぞれ、LoggingInterceptorHelloInterceptorで定義したログが出力されていることがわかる。

余談:preHandleでfalseを返すとどうなるか

ちょっと気になったので試してみる。 インターセプターでoverrideしたpreHandleメソッドについて、常にfalseを返却するように修正する。

    override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
        logger.info("----------------preHandle")
        // 常にfalseを返すとどうなる???
        return false
    }

↓↓↓↓実行結果のログ

2023-02-07T14:15:14.397+09:00  INFO 6835 --- [nio-8080-exec-2] c.e.demo.controller.LoggingInterceptor   : ----------------preHandle

上記の通り、preHandleの処理で実行された.logger.infoのみが実行され、postHandleおよびafterCompletionが実行されていない(ログが出力されていない)ことがわかる。

終わりに

今回はいずれも単純な例だったが、インターセプターを使用することで各コントローラーに対して共通した処理を実装することができる。そのため、認証やロギングなどに用いられることが多い(と思う)。

参考

SpringBootの特定のAnnotationが付与されたControllerのメソッドに対して事前処理を行う - Qiita

【Spring Boot】Interceptorによる共通処理