Custom annotation with Spring @Autowire fails due to generated private static $annotations() function


#1

We’re trying to create a custom annotation that builds on Spring @Autowired to provide internationalization support for fields. The code comes from https://moelholm.com/2016/10/15/spring-4-3-custom-annotations/, translated to Kotlin.

tl;dr: Kotlin generates a private static $annotations() method that has our annotation instead of the field we declare it on, and Spring is (naturally) unable to use this method to set the field. How do we get Kotlin to stop creating this static method and/or put the annotation on the field, where (we think) it belongs?

Details with code:

Here’s a sample of intended usage:

class Foo {
    @LocalizedMessage("foo.bar")
    private lateinit var bar: Message

    fun baz(): String {
        return bar.getMessage()
    }
}

Here’s LocalizedMessage:

@Autowired
@Retention(AnnotationRetention.RUNTIME)
annotation class LocalizedMessage(val value: String = "")

The Message class:

class Message(private val messageSource: MessageSource, private val messageKey: String) {
    fun getMessage(vararg args: Any): String {
        val locale = LocaleContextHolder.getLocale()
        return messageSource.getMessage(messageKey, args, "", locale)
    }
}

And finally, we have a @Configuration class with the following function to resolve instances of Message:

@Bean
    @Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    fun message(ip: InjectionPoint): Message {
        val annotation = ip.methodParameter.parameterAnnotations.find { it -> it.annotationClass == LocalizedMessage::class }
        val localizedMessage = AnnotationUtils.getAnnotation(annotation, LocalizedMessage::class.java)
        val resourceBundleKey = localizedMessage.value
        return Message(messageSource(), resourceBundleKey)
    }

The trouble we encounter is that when Spring attempts to autowire the private lateinit var bar: Message we get the following error:

Creating instance of bean 'foo'
2018-01-03 14:31:25 - org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor Autowired annotation is not supported on static methods: private static void foo.Foo.bar$annotations()

Consequently, the bar field is not initialized, and Kotlin (rightly) complains when it is accessed.

When we dug deeper we found that Kotlin is generating a $annotations method on the class and putting the LocalizedMessage annotation on it. So when Spring’s AutowiredAnnotationBeanPostProcessor class iterates over the methods in the class, it finds this method.

Here’s a decompile of the relevant field and method:

 private foo.Message bar;
    descriptor: Lfoo/Message;
    flags: (0x0002) ACC_PRIVATE

  private static void bar$annotations();
    descriptor: ()V
    flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=0, locals=0, args_size=0
         0: return
    Deprecated: true
    RuntimeVisibleAnnotations:
      0: #10(#11=s#12)

Notice the RuntimeVisibleAnnotations on the $annotations() method. It points to the LocalizedMessage annotation in the constants table.

So, our question, as per the tl;dr above, how can we create and use this custom annotation so that it is placed on the field, and not on this Kotlin-generated $annotations method?

Thanks!


#2

The $annotations method is created because the target of the annotation is a property; properties have no natural representation in the Java bytecode, so we create this method to have something to attach the annotations to. To have the annotation applied to the field, add @Target(AnnotationTarget.FIELD) to the declaration of LocalizedMessage.


#3

Thanks @yole. I think what I’m reading is that since there’s no correct/clear answer to the question of which of the generated elements for a property would get annotated by default – the field, the getter, or the setter? – you chose the path of creating a separate, unique element to attach the annotation to. That makes sense to me.