here's the spring initializr configuration i used

here's some kotlin code

package com.example.nativekotlin

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.boot.runApplication
import org.springframework.context.ApplicationListener
import org.springframework.context.annotation.Bean
import org.springframework.context.support.beans
import org.springframework.data.annotation.Id
import org.springframework.data.repository.CrudRepository
import org.springframework.stereotype.Component

@SpringBootApplication
class NativeKotlinApplication  {

    /*@Bean
    fun myListener(cr: CustomerRepository) = MyListener(cr)*/

}


fun main(args: Array<String>) {
    runApplication<NativeKotlinApplication>(*args)  {

        this.addInitializers(beans {
            bean {
                MyListener(ref())
            }
        })

    }
}

class MyListener(val repo: CustomerRepository) : ApplicationListener<ApplicationReadyEvent> {

    override fun onApplicationEvent(event: ApplicationReadyEvent) {
        listOf("James", "Josh")
            .map { Customer(null, it) }
            .map { repo.save(it) }
            .forEach { println(it) }
    }
}


interface CustomerRepository : CrudRepository<Customer, Int>

data class Customer(@Id val id: Int?, val name: String) 

I compile it using mvn -Pnative -DskipTests clean package to get a GraalVM binary. if I leave the code as-is, on the JRE, I see James and Josh. If I run it as a native image on GraalVM, I see James, Josh, James, Josh. (the same thing, twice). My listener is being called twice, for some reason.

Odder still, if I use @Bean to register the MyListener or if I use @Component, then in those cases I only see Josh, and James once. Not twice.

Comment From: snicoll

Thanks for the report, as far as I can see this has nothing to do with kotlin or the functional config. When AOT processes the context, it honors the initializers that you've defined. This defines a bean in the bean factory so it's processed. When the AOT-optimized context runs, it uses the generated context that registers a bean, and then call your initializer again. Can you please confirm that my analysis is correct (you should see some generated code for MyListener).

Comment From: sdeleuze

I had a look and I think your analysis is correct @snicoll. There is some code generated (MyListener__BeanDefinitions) and with a quick test with that updates and the messages are only printed one time each:

fun main(args: Array<String>) {
    runApplication<NativeKotlinApplication>(*args)  {

        this.addInitializers(beans {
            if (!AotDetector.useGeneratedArtifacts()) {
                bean {
                    MyListener(ref())
                }
            }
        })
    }
}

Comment From: snicoll

Thanks for testing @sdeleuze. We've been discussing this one as part of https://github.com/spring-projects/spring-boot/issues/32262 and we believe that this should work out-of-the-box.

Our thinking is that adding the AotDetector check around the childeren in this method might do the trick + a test in a smoke test that uses the Kotlin DSL (if that doesn't exist).

WDYT?

Comment From: sdeleuze

Yeah it was not a Gradle project (so painful to test by modifying Spring Framework snapshots) so I went the easy way to do a basic test.

I am ok with your proposal, but the fix will be Kotlin DSL specific and people using directly the Java functional API to do the same thing will continue to see the double registration if they don't do the AotDetector check manually. I don't see a better option so I guess we have to live with that but just asking for confirmation.

Comment From: snicoll

I don't see a better option so I guess we have to live with that but just asking for confirmation.

Yes. Our plan is to gather more feedback and then make an API change if necessary with more feedback. This one looks like it can be handled internally so we should do that regardless.