While working on the AOT smoke test configuration-properties I encountered a case where binding a nested class fails in native image. This works in JVM mode.

See the failing build here: https://ci.spring.io/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-1.0.x/jobs/configuration-properties

2022-07-13T15:35:51.578+02:00  WARN 44862 --- [           main] o.s.c.support.GenericApplicationContext  : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'CLR': Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'app-com.example.configprops.AppProperties': Could not bind properties to 'AppProperties' : prefix=app, ignoreInvalidFields=false, ignoreUnknownFields=true
2022-07-13T15:35:51.579+02:00 DEBUG 44862 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : Application failed to start due to an exception

org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'app.nested-list' to java.util.List<com.example.configprops.AppProperties$Nested>
        at org.springframework.boot.context.properties.bind.Binder.handleBindError(Binder.java:387) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:347) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$4(Binder.java:472) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:95) ~[na:na]
        at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:83) ~[na:na]
        at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:59) ~[na:na]
        at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$5(Binder.java:476) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:590) ~[na:na]
        at org.springframework.boot.context.properties.bind.Binder$Context.withDataObject(Binder.java:576) ~[na:na]
        at org.springframework.boot.context.properties.bind.Binder.bindDataObject(Binder.java:474) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:414) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:343) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:332) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:262) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:249) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.ConfigurationPropertiesBinder.bind(ConfigurationPropertiesBinder.java:95) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.bind(ConfigurationPropertiesBindingPostProcessor.java:89) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization(ConfigurationPropertiesBindingPostProcessor.java:78) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:425) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1745) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:604) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:526) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1374) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1294) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.aot.AutowiredInstantiationArgumentsResolver.resolveArgument(AutowiredInstantiationArgumentsResolver.java:302) ~[na:na]
        at org.springframework.beans.factory.aot.AutowiredInstantiationArgumentsResolver.resolveArguments(AutowiredInstantiationArgumentsResolver.java:232) ~[na:na]
        at org.springframework.beans.factory.aot.AutowiredInstantiationArgumentsResolver.resolve(AutowiredInstantiationArgumentsResolver.java:154) ~[na:na]
        at com.example.configprops.CLR__BeanDefinitions.getCLRInstance(CLR__BeanDefinitions.java:30) ~[na:na]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainInstanceFromSupplier(AbstractAutowireCapableBeanFactory.java:1224) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1209) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1156) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:566) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:526) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:930) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:926) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:592) ~[configuration-properties:6.0.0-SNAPSHOT]
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:729) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:428) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:310) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1301) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1290) ~[configuration-properties:3.0.0-SNAPSHOT]
        at com.example.configprops.ConfigPropsApplication.main(ConfigPropsApplication.java:10) ~[configuration-properties:na]
Caused by: org.springframework.boot.context.properties.bind.UnboundConfigurationPropertiesException: The elements [app.nested-list[0].a-int,app.nested-list[1].a-int] were left unbound.
        at org.springframework.boot.context.properties.bind.IndexedElementsBinder.assertNoUnboundChildren(IndexedElementsBinder.java:136) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindIndexed(IndexedElementsBinder.java:113) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindIndexed(IndexedElementsBinder.java:86) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindIndexed(IndexedElementsBinder.java:70) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.bind.CollectionBinder.bindAggregate(CollectionBinder.java:49) ~[na:na]
        at org.springframework.boot.context.properties.bind.AggregateBinder.bind(AggregateBinder.java:56) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.bind.Binder.lambda$bindAggregate$3(Binder.java:438) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:590) ~[na:na]
        at org.springframework.boot.context.properties.bind.Binder.bindAggregate(Binder.java:438) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:399) ~[configuration-properties:3.0.0-SNAPSHOT]
        at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:343) ~[configuration-properties:3.0.0-SNAPSHOT]
        ... 49 common frames omitted

2022-07-13T15:35:51.579+02:00 ERROR 44862 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target [Bindable@270c5b7b type = java.util.List<com.example.configprops.AppProperties$Nested>, value = 'provided', annotations = array<Annotation>[[empty]]] failed:

    Property: app.nested-list[0].a-int
    Value: 1
    Origin: class path resource [application.yaml] - 6:14
    Reason: The elements [app.nested-list[0].a-int,app.nested-list[1].a-int] were left unbound.
    Property: app.nested-list[1].a-int
    Value: 2
    Origin: class path resource [application.yaml] - 7:14
    Reason: The elements [app.nested-list[0].a-int,app.nested-list[1].a-int] were left unbound.

Action:

Update your application's configuration

Comment From: mhalbritter

It seems that there are hints missing for the com.example.configprops.AppProperties.Nested class.

Comment From: wilkinsona

This feels like something we overlooked in #30916. As it has already shipped, I think we should handle this as a bug as it's something that we expected to work now but does not.

Comment From: OlgaMaciaszek

Possibly related issue:

For the following configuration:

spring:  
  cloud:
    discovery:
      client:
        simple:
          instances:
            proxy:
              - uri: http://localhost:9083/
            fraud-verifier:
              - uri: http://localhost:9981/
            user-service:
              - uri: http://localhost:9082/

that should be binded to this bean when I package with AOT and run via java -jar -Dspring.aot.enabled=true , the app works fine. However, when I package it as a native image and then run it, I'm getting

org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'spring.cloud.discovery.client.simple.instances.proxy' to java.util.List<org.springframework.cloud.client.DefaultServiceInstance>

Description:

Binding to target [Bindable@36feed5f type = java.util.List<org.springframework.cloud.client.DefaultServiceInstance>, value = 'none', annotations = array<Annotation>[[empty]]] failed:

    Property: spring.cloud.discovery.client.simple.instances.proxy[0].uri
    Value: "http://localhost:9083/"
    Origin: class path resource [application.yml] - 10:22
    Reason: The elements [spring.cloud.discovery.client.simple.instances.proxy[0].uri] were left unbound.

Comment From: mhalbritter

It's important that the getters and setters are public, otherwise the Java bean introspection won't find them.

Comment From: mhalbritter

After a discussion with the team, i decided to revert my commit and reopen the issue.

@OlgaMaciaszek: You can generate hints by annotating the field which points to a non-inner class which should be bound with @NestedConfigurationProperty. That's consistent with the spring-boot-configuration-processor and as a bonus you even get auto-generated documentation. See this documentation for details.

The current algorithm works as follows:

  • if the nested configuration is an inner class, hints are generated
  • if the field pointing to the nested configuration is annotated with @NestedConfiguratinProperty, hints are generated
  • if the nested configuration is used in a Collection or Map or as an array, hints are generated.
  • the setters/getters must be public, otherwise the nested configuration classes aren't discovered

We're coming up in the future with a better way to handle that, but we have to do some design work first.

Comment From: mhalbritter

Nonetheless I think there is a bug in our implementation:

Map<String, List<SomeType>> getMap();

won't lead to the class SomeType to be registered in the reflection hints. The annotation processor would pick it up.

Comment From: mhalbritter

We now generate hints correctly for nested generics.

Comment From: mhalbritter

I've summarized the problems and possible ways forward in this document.

Comment From: philwebb

We're going to keep things as they are and expect users to add @NestedConfigurationProperties to classes if they're working on native applications. We're not going to break existing JVM users.

Comment From: mhalbritter

Interesting. I just tested it again, and some change we did made the @NestedConfigurationProperties annotation on fields obsolete.

This class binds without problems in a native image:

@ConfigurationProperties(prefix = "my.properties")
class MyProperties {

    private String name;

    private final Nested nested = new Nested();

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Nested getNested() {
        return nested;
    }

    public static class Nested {

        private int number;

        public int getNumber() {
            return number;
        }

        public void setNumber(int number) {
            this.number = number;
        }
    }

}

The getters/setters still have to be public, though.