Affects: 6.0.12

I am trying to update from Spring boot 2.7.13 to 3.1.4. We are using this custom annotation to document configuration properties.

@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
annotation class DocProperty(
  val name: String = "",
  val displayName: String = "",
  val description: String = "",
  val defaultValue: String = "",
  val defaultExplanation: String = "",
  val children: Array<DocProperty> = [],
  val prefix: String = "",
  val removedIn: String = "",
  val removalReason: String = "",
  val hidden: Boolean = false
)

When I run the app, it fails:

2023-10-10T17:17:04.743+02:00 ERROR 22027 --- [  restartedMain] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.BeanDefinitionStoreException: Failed to read candidate component class: URL [jar:file:/Users/jenik/IdeaProjects/tolgee-server/public/backend/data/build/libs/data-local-plain.jar!/io/tolgee/configuration/tolgee/S3Settings.class]
    at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.scanCandidateComponents(ClassPathScanningCandidateComponentProvider.java:463) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.findCandidateComponents(ClassPathScanningCandidateComponentProvider.java:317) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.doScan(ClassPathBeanDefinitionScanner.java:276) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.annotation.ComponentScanAnnotationParser.parse(ComponentScanAnnotationParser.java:128) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClassParser.java:289) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:243) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:196) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:164) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:415) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:287) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:344) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:115) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:771) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:589) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.1.4.jar:3.1.4]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:737) ~[spring-boot-3.1.4.jar:3.1.4]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) ~[spring-boot-3.1.4.jar:3.1.4]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:315) ~[spring-boot-3.1.4.jar:3.1.4]
    at io.tolgee.Application$Companion.main(Application.kt:26) ~[main/:na]
    at io.tolgee.Application.main(Application.kt) ~[main/:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
    at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:50) ~[spring-boot-devtools-3.1.4.jar:3.1.4]
Caused by: java.lang.StackOverflowError: null
    at java.base/java.lang.ref.ReferenceQueue.poll(ReferenceQueue.java:117) ~[na:na]
Caused by: java.lang.StackOverflowError: null

    at org.springframework.util.ConcurrentReferenceHashMap$ReferenceManager.pollForPurge(ConcurrentReferenceHashMap.java:1006) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.util.ConcurrentReferenceHashMap$Segment.restructureIfNecessary(ConcurrentReferenceHashMap.java:574) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.util.ConcurrentReferenceHashMap$Segment.getReference(ConcurrentReferenceHashMap.java:495) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.util.ConcurrentReferenceHashMap.getReference(ConcurrentReferenceHashMap.java:265) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.util.ConcurrentReferenceHashMap.get(ConcurrentReferenceHashMap.java:235) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationsScanner.getDeclaredAnnotations(AnnotationsScanner.java:446) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationsScanner.getDeclaredAnnotation(AnnotationsScanner.java:435) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMapping.resolveAliasedForTargets(AnnotationTypeMapping.java:144) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMapping.<init>(AnnotationTypeMapping.java:122) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings.addIfPossible(AnnotationTypeMappings.java:112) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings.addAllMappings(AnnotationTypeMappings.java:75) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings.<init>(AnnotationTypeMappings.java:68) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings$Cache.createMappings(AnnotationTypeMappings.java:245) ~[spring-core-6.0.12.jar:6.0.12]
    at java.base/java.util.concurrent.ConcurrentMap.computeIfAbsent(ConcurrentMap.java:330) ~[na:na]
    at org.springframework.core.annotation.AnnotationTypeMappings$Cache.get(AnnotationTypeMappings.java:241) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings.forAnnotationType(AnnotationTypeMappings.java:199) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings.forAnnotationType(AnnotationTypeMappings.java:182) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings.forAnnotationType(AnnotationTypeMappings.java:169) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMapping.computeSynthesizableFlag(AnnotationTypeMapping.java:405) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMapping.<init>(AnnotationTypeMapping.java:126) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings.addIfPossible(AnnotationTypeMappings.java:112) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings.addAllMappings(AnnotationTypeMappings.java:75) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings.<init>(AnnotationTypeMappings.java:68) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings$Cache.createMappings(AnnotationTypeMappings.java:245) ~[spring-core-6.0.12.jar:6.0.12]
    at java.base/java.util.concurrent.ConcurrentMap.computeIfAbsent(ConcurrentMap.java:330) ~[na:na]
    at org.springframework.core.annotation.AnnotationTypeMappings$Cache.get(AnnotationTypeMappings.java:241) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings.forAnnotationType(AnnotationTypeMappings.java:199) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings.forAnnotationType(AnnotationTypeMappings.java:182) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings.forAnnotationType(AnnotationTypeMappings.java:169) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMapping.computeSynthesizableFlag(AnnotationTypeMapping.java:405) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMapping.<init>(AnnotationTypeMapping.java:126) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings.addIfPossible(AnnotationTypeMappings.java:112) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings.addAllMappings(AnnotationTypeMappings.java:75) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings.<init>(AnnotationTypeMappings.java:68) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings$Cache.createMappings(AnnotationTypeMappings.java:245) ~[spring-core-6.0.12.jar:6.0.12]
    at java.base/java.util.concurrent.ConcurrentMap.computeIfAbsent(ConcurrentMap.java:330) ~[na:na]
    at org.springframework.core.annotation.AnnotationTypeMappings$Cache.get(AnnotationTypeMappings.java:241) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings.forAnnotationType(AnnotationTypeMappings.java:199) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings.forAnnotationType(AnnotationTypeMappings.java:182) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.core.annotation.AnnotationTypeMappings.forAnnotationType(AnnotationTypeMappings.java:169) ~[spring-core-6.0.12.jar:6.0.12]
    at 
...

I've epxlored what happens, and the issue is that method org.springframework.core.annotation.AnnotationTypeMapping#computeSynthesizableFlag finds the children property of our DocProperty class annotation and calls the forAnnotationType, with the children's type which is DocProperty again. And that's infinite loop.

It's reproducible here: https://github.com/tolgee/tolgee-platform/pull/1938/commits/259cd313e625b9e87af26e4f9c0e7a4b96583ada (the specific commit)

Comment From: jhoeller

It looks like #28618 removed the support for cyclic annotation definitions introduced in #28012 but unfortunately also removed the ability to handle nested references to the same annotation type.

Comment From: JanCizmar

Is there any workaround like ignoring the annotation?

Comment From: jhoeller

I'm afraid there isn't an obvious workaround. We'll have to address this in our AnnotationTypeMapping implementation.

Note that Java annotations are not allowed to refer to themselves in their attributes, so this remains Kotlin specific. Kotlin prohibits deep cycles (X -> Y -> X) in annotation declarations as of Kotlin 1.9 (aligned with Java) but still allows self references (in contrast to Java)... unfortunately we missed the latter part there.

Comment From: lorenzsimon

@jhoeller The fix does not work for the following legal Kotlin code:

@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
annotation class DocProperty(
    val name: String = "",
    val displayName: String = "",
    val description: String = "",
    val defaultValue: String = "",
    val defaultExplanation: String = "",
    val children: Array<DocProperty> = [],
    val prefix: String = "",
    val removedIn: String = "",
    val removalReason: String = "",
    val hidden: Boolean = false,
    val externalMap: Array<DocPropertyMapEntry> = []
)

annotation class DocPropertyMapEntry(
    val docProperty: DocProperty = DocProperty()
)

see the externalMap property. Can Spring add support for this valid use-case please 🙏

This is my use-case: https://github.com/OpenFolder/kotlin-asyncapi/blob/master/kotlin-asyncapi-annotation/src/main/kotlin/org/openfolder/kotlinasyncapi/annotation/Schema.kt