Reputation: 3651
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:
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?
Upvotes: 3
Views: 3622
Reputation: 12043
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
Upvotes: 3
Reputation: 24565
Not a direct answer to your question, but I'd simply use Spring AOP in that case instead of implementing BeanPostProcessor. To do so, you can define the following annotation and and a corresponding aspect, e.g.:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
annotation class OfflineProcessing(val allowed: Boolean = true)
@Aspect
@Component
class OfflineProcessingAspect {
@Pointcut("@annotation(<path.to.annotation.package>.OfflineProcessing)")
fun offlineProcessingPointcut(offlineProcessing: OfflineProcessing?) {
}
@Around("offlineProcessingPointcut(offlineProcessing)")
@Throws(Throwable::class)
fun around(pjp: ProceedingJoinPoint, offlineProcessing: OfflineProcessing): Object {
if (offlineProcessing.allowed()) {
// your pre-processing logic here
val result = pjp.proceed()
// post-processing logic here
return result
}
// do the same for non-allowed offline-processing ...
}
}
Lastly, add the following dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Upvotes: 2