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
CollectionorMapor 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.