There are cases that configuration property binding does not work when integrating 3rd party configuration class and it deploy to the application server as war file(= When JndiPropertySource is enabled).

Versions

  • 2.2.0.RELEASE(2.2.0.M5+) ※2.2.0.M4 work fine

Details

For example, following properties class does not work. (As actually, MyBatis's configuration class matches this pattern...)

package com.example;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;

@ConfigurationProperties(prefix = "my")
public class MyProperties {

  private String name;

  @NestedConfigurationProperty
  private ThirdPartyConfiguration configuration; // ### Third party class

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

  public String getName() {
    return name;
  }

  public void setConfiguration(ThirdPartyConfiguration configuration) {
    this.configuration = configuration;
  }

  public ThirdPartyConfiguration getConfiguration() {
    return configuration;
  }

}
package com.example;

public class ThirdPartyConfiguration {

  private String encoding;

  private boolean enabled;

  private final List<ResultMap> resultMaps = new ArrayList<>(); // ### Collection

  public void setEncoding(String encoding) {
    this.encoding = encoding;
  }

  public String getEncoding() {
    return encoding;
  }

  public void setEnabled(boolean enabled) {
    this.enabled = enabled;
  }

  public boolean isEnabled() {
    return enabled;
  }

  public Collection<ResultMap> getResultMaps() {
    return resultMaps;
  }

  public void addResultMap(ResultMap rm) {
    resultMaps.add(rm);
  }

}
package com.example;

public class ResultMap {
  private String name;
  private String option;

  private ResultMap() { // ### Define private (If define public, this issue does not occurred)
  }

  public String getName() {
    return name;
  }

  public String getOption() {
    return option;
  }

  public static class Builder { // ### Builder class
    private final ResultMap resultMap;

    public Builder(String name) {
      resultMap = new ResultMap();
      resultMap.name = name;
    }

    public Builder option(String option) {
      resultMap.option = option;
      return this;
    }

    public ResultMap build() {
      return resultMap;
    }
  }
}

StackTrace

...
[INFO] [talledLocalContainer] Caused by: org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'my.configuration.result-maps[0]' to com.example.ResultMap
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.handleBindError(Binder.java:337)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:297)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.lambda$null$1(Binder.java:385)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder$Context.withSource(Binder.java:519)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder$Context.access$1000(Binder.java:486)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.lambda$bindAggregate$2(Binder.java:386)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindIndexed(IndexedElementsBinder.java:106)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindIndexed(IndexedElementsBinder.java:86)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindIndexed(IndexedElementsBinder.java:71)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.CollectionBinder.bindAggregate(CollectionBinder.java:49)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.AggregateBinder.bind(AggregateBinder.java:56)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.lambda$bindAggregate$3(Binder.java:388)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:543)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder$Context.access$200(Binder.java:486)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.bindAggregate(Binder.java:388)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:349)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:293)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$4(Binder.java:421)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:88)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:77)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:54)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$5(Binder.java:425)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:543)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder$Context.withDataObject(Binder.java:529)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder$Context.access$500(Binder.java:486)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.bindDataObject(Binder.java:423)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:364)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:293)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$4(Binder.java:421)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:88)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:77)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:54)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$5(Binder.java:425)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:543)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder$Context.withDataObject(Binder.java:529)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder$Context.access$500(Binder.java:486)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.bindDataObject(Binder.java:423)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:364)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:293)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:281)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:211)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:198)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.ConfigurationPropertiesBinder.bind(ConfigurationPropertiesBinder.java:89)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.bind(ConfigurationPropertiesBindingPostProcessor.java:107)
[INFO] [talledLocalContainer]   ... 60 more
[INFO] [talledLocalContainer] Caused by: java.lang.IllegalStateException: Failed to extract parameter names for com.example.ResultMap(com.example.ResultMap$1)
[INFO] [talledLocalContainer]   at org.springframework.util.Assert.state(Assert.java:94)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.ValueObjectBinder$DefaultValueObject.parseConstructorParameters(ValueObjectBinder.java:178)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.ValueObjectBinder$DefaultValueObject.<init>(ValueObjectBinder.java:173)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.ValueObjectBinder$DefaultValueObject.get(ValueObjectBinder.java:218)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.ValueObjectBinder$DefaultValueObject.get(ValueObjectBinder.java:206)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.ValueObjectBinder$ValueObject.get(ValueObjectBinder.java:112)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.ValueObjectBinder.bind(ValueObjectBinder.java:51)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$5(Binder.java:425)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:543)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder$Context.withDataObject(Binder.java:529)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder$Context.access$500(Binder.java:486)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.bindDataObject(Binder.java:423)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:364)
[INFO] [talledLocalContainer]   at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:293)

Related Issue

  • 17098

Reproduce project

How to reproduce

$ ./mvnw package corgo:run

Comment From: kazuki43zoo

I've attached a small reproduce project.

Comment From: slavus

I have the same issue. Does someone have a workaround ?

Comment From: kazuki43zoo

@slavus

If you does not use the JDNI feature, probably you can prevent this issue by disabled the Spring's JNDI feature. How to disabled, See https://github.com/mybatis/spring-boot-starter/issues/350#issuecomment-500480499.

Comment From: slavus

Unfortunately our app is deployed to an application server and it utilize jndi for external configuration, so this workaround does not help us.

Comment From: mbhave

@kazuki43zoo Thank you for the sample. To get the sample to fail, I had to add my.configuration.result-maps[0].name to application.properties. However, I wasn't able to get it to work even with 2.2.0.M4. The error I get if I change the version to 2.2.0M4 is

Description:

Failed to bind properties under 'my.configuration.result-maps[0]' to com.example.ResultMap:

    Property: my.configuration.result-maps[0].name
    Value: hello
    Origin: class path resource [application.properties]:2:38
    Reason: No setter found for property: name

I'm not sure there's anything we can do here if no setter is found for the property. Adding for: team-attention to see what the rest of the team thinks.

Comment From: kazuki43zoo

@mbhave

Is deploying on the application server(e.g. Tomcat)? The demo can run on Tomcat using Cargo maven plugin as follow:

$ ./mvnw package cargo:run

It is failing without adding 'my.configuration.result-maps[0]' on application.properties.

Comment From: mbhave

Thanks @kazuki43zoo we were able to reproduce the behavior you mentioned when deploying it as a war. There are a couple of issues here that got introduced when constructor binding was added.

  1. Since ResultMap has a private constructor and a nested class, there is a synthetic constructor that ends up being created with an anonymous class as the first parameter. (Thanks to @wilkinsona for digging into why that happens). This causes constructor binding to kick in for ResultMap which fails because it can't find the parameters for that synthetic constructor. We should we able to get around this by ignoring synthetic constructors.

  2. Constructor binding should not have been attempted with ResultMap in the first place since there is no @ConstructorBinding annotation specified on the top-level properties or the nested properties.

Comment From: kazuki43zoo

@mbhave @philwebb I've confirmed to fix this issue using my demo app. Thanks for your work.

Comment From: Zhoutzzz

Actually I also meet this issue at spring -boot version 2.7.18 when it comes to I use mybatis as my ORM framework at my private project, that's wired! I recreate a new project just dependency the required dependencies with spring-boot 2.7.18 and mybatis-spring-boot-starter that the version match spring-boot. finally I can not reproduce this issue. And how should I do if I can not rewrite mybatis code to add @ConstructorBinding annotation for it and I want to fix this issue at the same time I also can not decline spring boot and mybatis version? By the way, Does anyone know why there is a synthetic constructor that ends up being created with an anonymous class as the first parameter?

Comment From: bclozel

@Zhoutzzz Spring Boot 2.7.x is not supported anymore for OSS users. Please upgrade to a supported version.

Comment From: Zhoutzzz

@bclozel But the version already fixed, we don't have permission to change it, we must be need to find other way to resolve this issue. By the way we meet this issue because we use the code like this

// this method implement the method of EnvironmentAware interface
@Override
public void setEnvironment(Environment environment) {
Binder bind = Binder.get(environment);
this.global = bind.bind("mybatis", MybatisProperties.class);
}