Skip to content

Consistent configuration property binding for nested classes on JVM and native-image #31708

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
mhalbritter opened this issue Jul 13, 2022 · 10 comments
Assignees
Labels
theme: aot An issue related to Ahead-of-time processing type: bug A general bug

Comments

@mhalbritter
Copy link
Contributor

mhalbritter commented Jul 13, 2022

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
@mhalbritter mhalbritter added type: enhancement A general enhancement theme: aot An issue related to Ahead-of-time processing labels Jul 13, 2022
@mhalbritter mhalbritter added this to the 3.0.x milestone Jul 13, 2022
@mhalbritter
Copy link
Contributor Author

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

@wilkinsona
Copy link
Member

wilkinsona commented Jul 13, 2022

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.

@mhalbritter mhalbritter added type: bug A general bug and removed type: enhancement A general enhancement labels Jul 13, 2022
@mhalbritter mhalbritter changed the title Add support for configuration property binding on native-image Configuration property binding doesn't work with nested classes on native-image Jul 13, 2022
@mhalbritter mhalbritter self-assigned this Jul 19, 2022
@OlgaMaciaszek
Copy link
Contributor

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.

@mhalbritter mhalbritter modified the milestones: 3.0.x, 3.0.0-M4 Jul 20, 2022
@mhalbritter
Copy link
Contributor Author

mhalbritter commented Jul 20, 2022

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

@mhalbritter mhalbritter reopened this Jul 20, 2022
@mhalbritter
Copy link
Contributor Author

mhalbritter commented Jul 20, 2022

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.

@mhalbritter mhalbritter modified the milestones: 3.0.0-M4, 3.0.x Jul 20, 2022
@mhalbritter mhalbritter added the status: pending-design-work Needs design work before any code can be developed label Jul 20, 2022
@snicoll snicoll changed the title Configuration property binding doesn't work with nested classes on native-image Consistent configuration property binding for nested classes on JVM and native-image Jul 20, 2022
@mhalbritter
Copy link
Contributor Author

mhalbritter commented Jul 21, 2022

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.

@mhalbritter
Copy link
Contributor Author

We now generate hints correctly for nested generics.

@mhalbritter
Copy link
Contributor Author

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

@wilkinsona wilkinsona added the for: team-meeting An issue we'd like to discuss as a team to make progress label Oct 5, 2022
@philwebb
Copy link
Member

philwebb commented Oct 5, 2022

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.

@philwebb philwebb closed this as not planned Won't fix, can't repro, duplicate, stale Oct 5, 2022
@philwebb philwebb removed status: pending-design-work Needs design work before any code can be developed for: team-meeting An issue we'd like to discuss as a team to make progress labels Oct 5, 2022
@philwebb philwebb removed this from the 3.0.x milestone Oct 5, 2022
@mhalbritter
Copy link
Contributor Author

mhalbritter commented Oct 19, 2022

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
theme: aot An issue related to Ahead-of-time processing type: bug A general bug
Projects
None yet
Development

No branches or pull requests

4 participants