Describe the bug

Spring Boot 3 Native - "Could not bind properties" error when properties class located in another module uses @ConstructorBinding in multi-module gradle project

To Reproduce Environment - Spring Boot: 3.0.0-RC2 - Native Buildtools: 0.9.17 - GraalVM version : graalvm-ce-java17-22.3.0 - JDK version: openjdk 17.0.5 - Architecture: AMD64

Sample project https://github.com/michalkrajcovic/spring-native-multi-modules-bind-properties

Compile

./gradlew nativeCompile

Run

./greetings-app/build/native/nativeCompile/greetings-app

Fails with

o.s.c.support.GenericApplicationContext  : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'application': Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'greetings-com.example.greetings.configuration.GreetingsProperties': Could not bind properties to 'GreetingsProperties' : prefix=greetings, ignoreInvalidFields=false, ignoreUnknownFields=true
Description:
Failed to bind properties under 'greetings' to com.example.greetings.configuration.GreetingsProperties:
    Reason: java.lang.IllegalStateException: Failed to extract parameter names for public com.example.greetings.configuration.GreetingsProperties(java.lang.String)

Runs without issues on JVM

./gradlew bootRun

Expected behavior Application starts without issues using JVM and native image.

Observation Does not fail when properties class uses setters see brach setter. Also ConstructorBinding doesn't fail in single module project see branch single-module

Originally reported for Spring Boot 2.7.5 in spring-native

Comment From: snicoll

@michalkrajcovic thanks for the report. GreetingsProperties must be annotated with @NestedConfigurationProperties to indicate to the inference algorithm that it isn't a scalar value but a "sub-namespace".

See #31708.

Comment From: michalkrajcovic

@snicoll actually, this is not nested property class, as described in the docs here. Please see the properties class here it has only one field, type java.lang.String.

Comment From: snicoll

Apologizes for this, I must have misread something but I can't tell what now.

Comment From: snicoll

The error is

Caused by: java.lang.IllegalStateException: Failed to extract parameter names for public com.example.greetings.configuration.GreetingsProperties(java.lang.String)
    at org.springframework.util.Assert.state(Assert.java:97) ~[na:na]
    at org.springframework.boot.context.properties.bind.ValueObjectBinder$DefaultValueObject.parseConstructorParameters(ValueObjectBinder.java:270) ~[na:na]
    at org.springframework.boot.context.properties.bind.ValueObjectBinder$DefaultValueObject.<init>(ValueObjectBinder.java:264) ~[na:na]
    at org.springframework.boot.context.properties.bind.ValueObjectBinder$DefaultValueObject.get(ValueObjectBinder.java:291) ~[na:na]
    at org.springframework.boot.context.properties.bind.ValueObjectBinder$ValueObject.get(ValueObjectBinder.java:198) ~[greetings-app:3.0.0-RC2]
    at org.springframework.boot.context.properties.bind.ValueObjectBinder.bind(ValueObjectBinder.java:67) ~[greetings-app:3.0.0-RC2]
    at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$5(Binder.java:476) ~[greetings-app:3.0.0-RC2]
    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) ~[greetings-app:3.0.0-RC2]
    at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:414) ~[greetings-app:3.0.0-RC2]
    at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:343) ~[greetings-app:3.0.0-RC2]
    ... 39 common frames omitted

There is a reflection hint for GreetingProperties:

 {
    "name": "com.example.greetings.configuration.GreetingsProperties",
    "methods": [
      {
        "name": "<init>",
        "parameterTypes": [
          "java.lang.String"
        ]
      }
    ]
  }

It feels like --parameter wasn't set or something.

Comment From: wilkinsona

It feels like --parameter wasn't set or something.

I think that's exactly it. Boot's Gradle plugin sets -parameters but it hasn't been applied to the greetings-configuration project. I suspect that it works on the JVM as we're able to fall back to the LocalVariableTableParameterNameDiscoverer. That fallback would only work in a native image if the class files were made available as resources.

Comment From: wilkinsona

The sample works with this addition to the greetings-configuration project:

tasks.named('compileJava').configure {
    options.compilerArgs = ['-parameters']
}

There's no way for us to do this automatically as Boot's plugin hasn't been applied (and nor should it have been in this case). I wonder if there's something else that we could do to make this work automatically. If including the class files as resources works, perhaps we could generate hints that do that if we detect that the standard reflection parameter information is missing?

Comment From: wilkinsona

If including the class files as resources works, perhaps we could generate hints that do that if we detect that the standard reflection parameter information is missing?

I've prototyped this and it works. I'd like to discuss this with team to see if it's something that we want to do or if we'd prefer to document the need to compile with -parameters when targeting a native image.

Comment From: wilkinsona

We discussed this today and concluded that relying on .class resources and ASM-based processing isn't the right choice for a native image. We're going to document the need for -parameters and also fail fast at build time when we detect that it hasn't been used.