Search code examples
springspring-bootkotlinannotationsspring-annotations

How to process custom annotation in Spring Boot with Kotlin?


CONTEXT: I would like to create a custom annotation in Spring Boot and add extra logic for processing. I give an example with rather simplistic annotation but I want to have several of such annotations with more fine-grained control.

There are several approaches to this issue:

  • Create Filter
  • Create Interceptor
  • Create annotation with custom processing

I have to move with the latest one as two above don't work with my use-case.

ISSUE:

I have a custom annotation in Kotlin and I want it to be registered and be checked in the runtime.

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class OfflineProcessing(val allow: Bool)

REST controller is below:

@RestController
class ApiController {

    @GetMapping("/api/version")
    @OfflineProcessing(true)
    fun apiVersion(): String {
        return "0.1.0"
    }
}

The idea is to have annotation per method and make conditional logic based on OflineProcessing allowed or not.

I have tried creating elementary PostBeanProcessor:

@Component
class OfflineProcessingAnnotationProcessor @Autowired constructor(
        val configurableBeanFactory: ConfigurableListableBeanFactory
) : BeanPostProcessor {

    @Throws(BeansException::class)
    override fun postProcessBeforeInitialization(bean: Any, beanName: String): Any? {
        println("Before processor. Bean name: $beanName, Bean: $bean. Bean factory: $configurableBeanFactory.")
        return super.postProcessBeforeInitialization(bean, beanName)
    }

    @Throws(BeansException::class)
    override fun postProcessAfterInitialization(bean: Any, beanName: String): Any? {
        println("After processor. Bean name: $beanName, Bean: $bean. Bean factory: $configurableBeanFactory.")
        return super.postProcessAfterInitialization(bean, beanName)
    }

}

Apparently, annotation doesn't get logged among other annotations in BeanPostProcessor and I confused how to access it, so far I didn't find any other good examples without BeanPostProcessor.

Dependencies:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
</dependency>

Is there anything I do wrong? Or do I trying to use wrong method for the task?


Solution

  • This is a general question and not specific to Kotlin.

    I think the misconception in your attempt to solve this is the fact that you are relaying on BeanPostProcessors. The bean is created in an early stage and it’s probably a singleton so it will not be called when you execute a rest request. This means that you will need to check for a bean that has your annotation and then somehow create a proxy bean over them with your logic embedded in that proxy.

    This is very similar to what AOP does and then @eol’s approach is match easter.

    I would like to suggest using an interceptor but not a bean creation interceptor.

    My answer was inspired by Spring Boot Adding Http Request Interceptors

    Define the annotation

    @Retention(AnnotationRetention.RUNTIME)
    @Target(AnnotationTarget.FUNCTION)
    annotation class OfflineProcessing(val allow: Boolean)
    

    Define the interceptor

    @Component
    class CustomRestAnnotationInterceptor:HandlerInterceptor {
        private val logger: Logger = LoggerFactory.getLogger(this.javaClass)
    
        override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
            if (handler is HandlerMethod) {
                var isOffline: OfflineProcessing? = handler.getMethodAnnotation(OfflineProcessing::class.java)
                if (null == isOffline) {
                    isOffline = handler.method.declaringClass
                        .getAnnotation(OfflineProcessing::class.java)
                }
                if (null != isOffline) {
                    logger.info("method called has OfflineProcessing annotation with allow=${isOffline.allow}" )
                }
            }
            return true
        }
    }
    

    Add the interceptor to the path

    @Configuration
    class WebConfig : WebMvcConfigurer {
        @Autowired
        lateinit var customRestAnnotationInterceptor: CustomRestAnnotationInterceptor
    
        override fun addInterceptors(registry: InterceptorRegistry) {
            // Custom interceptor, add intercept path and exclude intercept path
            registry.addInterceptor(customRestAnnotationInterceptor).addPathPatterns("/**")
        }
    }
    

    Use the annotation on your controller

    the log will display

    2022-04-14 08:48:58.785  INFO 32595 --- [nio-8080-exec-1] .h.s.k.q.CustomRestAnnotationInterceptor : method called has OfflineProcessing annotation with allow=true