I included a third party spring boot starter. That starter includes a configuration properties class. This class is a kotlin data class and employs Hibernate validator for some validation logic.

@Validated
@ConfigurationProperties(prefix = "mqtt")
data class MqttProperties(
    @get:NotEmpty
    val host: String = "",
    ...
)

Not quite sure why that is though, but this class gets stubbed on runtime by a Spring cglib proxy. I suspect it's due to the validation logic. When now building a native image, this cglib proxy seems to not get register with native image. The following error is generated when trying to run the native executable:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'de.smartsquare.starter.mqtt.MqttSubscriberCollector': Instantiation of supplied bean failed
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1223) ~[demo:6.1.5]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1161) ~[demo:6.1.5]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562) ~[demo:6.1.5]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[demo:6.1.5]
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326) ~[demo:6.1.5]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[demo:6.1.5]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324) ~[demo:6.1.5]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:205) ~[demo:6.1.5]
    at org.springframework.context.support.PostProcessorRegistrationDelegate.registerBeanPostProcessors(PostProcessorRegistrationDelegate.java:277) ~[na:na]
    at org.springframework.context.support.AbstractApplicationContext.registerBeanPostProcessors(AbstractApplicationContext.java:805) ~[demo:6.1.5]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:608) ~[demo:6.1.5]
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[demo:3.2.4]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[demo:3.2.4]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456) ~[demo:3.2.4]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:334) ~[demo:3.2.4]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1354) ~[demo:3.2.4]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343) ~[demo:3.2.4]
    at com.example.demo.DemoApplicationKt.main(DemoApplication.kt:13) ~[demo:na]
    at java.base@21.0.2/java.lang.invoke.LambdaForm$DMH/sa346b79c.invokeStaticInit(LambdaForm$DMH) ~[na:na]
Caused by: org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class de.smartsquare.starter.mqtt.MqttProperties: Common causes of this problem include using a final class or a non-visible class
    at org.springframework.aop.framework.CglibAopProxy.buildProxy(CglibAopProxy.java:227) ~[demo:6.1.5]
    at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:160) ~[demo:6.1.5]
    at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:110) ~[na:na]
    at org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver.buildLazyResolutionProxy(ContextAnnotationAutowireCandidateResolver.java:136) ~[na:na]
    at org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver.buildLazyResolutionProxy(ContextAnnotationAutowireCandidateResolver.java:84) ~[na:na]
    at org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver.getLazyResolutionProxyIfNecessary(ContextAnnotationAutowireCandidateResolver.java:54) ~[na:na]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1347) ~[demo:6.1.5]
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:904) ~[na:na]
    at org.springframework.beans.factory.support.RegisteredBean.resolveAutowiredArgument(RegisteredBean.java:229) ~[demo:6.1.5]
    at org.springframework.beans.factory.aot.BeanInstanceSupplier.resolveAutowiredArgument(BeanInstanceSupplier.java:341) ~[na:na]
    at org.springframework.beans.factory.aot.BeanInstanceSupplier.resolveArguments(BeanInstanceSupplier.java:264) ~[na:na]
    at org.springframework.beans.factory.aot.BeanInstanceSupplier.get(BeanInstanceSupplier.java:204) ~[na:na]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.obtainInstanceFromSupplier(DefaultListableBeanFactory.java:949) ~[demo:6.1.5]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1217) ~[demo:6.1.5]
    ... 18 common frames omitted
Caused by: org.springframework.cglib.core.CodeGenerationException: java.lang.NoSuchMethodException-->de.smartsquare.starter.mqtt.MqttProperties$$SpringCGLIB$$0.CGLIB$SET_THREAD_CALLBACKS([Lorg.springframework.cglib.proxy.Callback;)
    at org.springframework.cglib.proxy.Enhancer$EnhancerFactoryData.<init>(Enhancer.java:501) ~[na:na]
    at org.springframework.cglib.proxy.Enhancer.wrapCachedClass(Enhancer.java:801) ~[na:na]
    at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.lambda$new$1(AbstractClassGenerator.java:108) ~[na:na]
    at org.springframework.cglib.core.internal.LoadingCache.lambda$createEntry$1(LoadingCache.java:52) ~[na:na]
    at java.base@21.0.2/java.util.concurrent.FutureTask.run(FutureTask.java:317) ~[demo:na]
    at org.springframework.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:57) ~[na:na]
    at org.springframework.cglib.core.internal.LoadingCache.get(LoadingCache.java:34) ~[na:na]
    at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:130) ~[na:na]
    at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:317) ~[demo:6.1.5]
    at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:562) ~[na:na]
    at org.springframework.cglib.proxy.Enhancer.createClass(Enhancer.java:407) ~[na:na]
    at org.springframework.aop.framework.ObjenesisCglibAopProxy.createProxyClassAndInstance(ObjenesisCglibAopProxy.java:62) ~[na:na]
    at org.springframework.aop.framework.CglibAopProxy.buildProxy(CglibAopProxy.java:218) ~[demo:6.1.5]
    ... 31 common frames omitted
Caused by: java.lang.NoSuchMethodException: de.smartsquare.starter.mqtt.MqttProperties$$SpringCGLIB$$0.CGLIB$SET_THREAD_CALLBACKS([Lorg.springframework.cglib.proxy.Callback;)
    at java.base@21.0.2/java.lang.Class.checkMethod(DynamicHub.java:1075) ~[demo:na]
    at java.base@21.0.2/java.lang.Class.getDeclaredMethod(DynamicHub.java:1165) ~[demo:na]
    at org.springframework.cglib.proxy.Enhancer.getCallbacksSetter(Enhancer.java:901) ~[na:na]
    at org.springframework.cglib.proxy.Enhancer$EnhancerFactoryData.<init>(Enhancer.java:490) ~[na:na]
    ... 43 common frames omitted

When duplicating the configuration properties class in the main source set of the application the error does not happen and the native image generated works out of the box without any additional configuration. I also tried adding reflect config via , which did not solve the @RegisterReflectionForBinding(MqttProperties::class) problem though.

When adding the following reflect-config.json the error is gone:

[
  {
    "name": "de.smartsquare.starter.mqtt.MqttProperties$$SpringCGLIB$$0",
    "fields": [
      {
        "name": "CGLIB$CALLBACK_FILTER"
      },
      {
        "name": "CGLIB$FACTORY_DATA"
      }
    ],
    "methods": [
      {
        "name": "CGLIB$SET_THREAD_CALLBACKS",
        "parameterTypes": [
          "org.springframework.cglib.proxy.Callback[]"
        ]
      }
    ]
  }
]

I feel like that I as a developer should not be responsible for generating implementation detail code like reflection hints for cglib reflections. I think the code should work without any modifications out of the box.

I prepared a demo application which show cases the problem when generating it with ./gradlew nativeCompile and then running the native binary.

Is there something I'm missing here? Whats the suggested approach for a library owner which wants to support native-image for a custom spring starter?

Unsure, but maybe related to https://github.com/spring-projects/spring-framework/issues/29873?

Comment From: wilkinsona

Thanks for the report.

There's quite a lot going on in the demo application. There are many classes that I don't think are related to the problem. Kotlin is also involved which makes things quite a bit more complicated, particularly when using GraalVM. Can you please reduce the classes involved to the bare minimum required to reproduce the problem and, unless the problem only occurs with Kotlin, please write those classes in Java.

Comment From: cmdjulian

I minified the example in the repository. Please check again. I now identified the main problem. It is not related to kotlin. The main problem seems to be the @Lazy in the BeanPostProcessor. Without that @Lazy the tests run successfully.

Comment From: cmdjulian

I also tried refactoring my code to use ObjectProvider<MqttProperties> via class MqttSubscriberCollector(private val config: ObjectProvider<MqttProperties>) : BeanPostProcessor but this seems to not work here, as the following error is reported: Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'mqtt-de.smartsquare.starter.mqtt.MqttProperties': Requested bean is currently in creation: Is there an unresolvable circular reference?

Any other way I could ditch the @Lazy as a workaround?

Comment From: wilkinsona

Thanks very much for minimising the sample.

Using ObjectProvider<MqttProperties> seems to work for me (with your Java-based sample) so I'm not sure what's happening for you when you try it using Kotlin.

The use of @Lazy causes MqttProperties to be proxied. Ideally, Spring Framework's AOT support would generate the necessary reflection metadata for this automatically but that isn't happening. I also tried using a @Bean method with a @Lazy parameter:

@Bean
static MqttSubscriberCollector mqttSubscriverCollectior(@Lazy MqttProperties properties) {
    return new MqttSubscriberCollector(properties);
}

My goal was to make the use of @Lazy more obvious such that it would be automatically detected by Framework but it was not successful.

We'll transfer this to the Framework team so that they can investigate.

Comment From: cmdjulian

Yeah I got it working with ObjectProvider meanwhile. Was just a stupid mistake to eagerly call the getObject() method inside the bean post processors constructor. You were right, that seems to workaround this. Thanks for investigating 😃

Comment From: snicoll

Thanks for the report, I believe this is a duplicate of #30985 and we'll validate this sample when we get to it.