Skip to content

Binding fails in presence of a synthetic constructor #18670

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
kazuki43zoo opened this issue Oct 21, 2019 · 11 comments
Closed

Binding fails in presence of a synthetic constructor #18670

kazuki43zoo opened this issue Oct 21, 2019 · 11 comments
Assignees
Labels
type: bug A general bug
Milestone

Comments

@kazuki43zoo
Copy link
Contributor

kazuki43zoo commented Oct 21, 2019

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

Reproduce project

How to reproduce

$ ./mvnw package corgo:run
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Oct 21, 2019
@kazuki43zoo
Copy link
Contributor Author

I've attached a small reproduce project.

@slavus
Copy link

slavus commented Oct 25, 2019

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

@kazuki43zoo
Copy link
Contributor Author

kazuki43zoo commented Oct 25, 2019

@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 mybatis/spring-boot-starter#350 (comment).

@slavus
Copy link

slavus commented Oct 28, 2019

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

@mbhave mbhave self-assigned this Oct 29, 2019
@mbhave
Copy link
Contributor

mbhave commented Oct 29, 2019

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

@mbhave mbhave added the for: team-attention An issue we'd like other members of the team to review label Oct 29, 2019
@kazuki43zoo
Copy link
Contributor Author

kazuki43zoo commented Oct 29, 2019

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

@philwebb philwebb removed the for: team-attention An issue we'd like other members of the team to review label Oct 31, 2019
@mbhave
Copy link
Contributor

mbhave commented Oct 31, 2019

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.

@mbhave mbhave changed the title Fail binding when holds builder pattern collection object Binding fails in presence of a synthetic constructor Nov 5, 2019
@mbhave mbhave closed this as completed in f9785d2 Nov 5, 2019
@mbhave mbhave modified the milestones: 2.2.x, 2.2.1 Nov 5, 2019
@kazuki43zoo
Copy link
Contributor Author

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

@Zhoutzzz
Copy link

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?

@bclozel
Copy link
Member

bclozel commented Jan 14, 2025

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

@Zhoutzzz
Copy link

Zhoutzzz commented Jan 14, 2025

@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);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: bug A general bug
Projects
None yet
Development

No branches or pull requests

7 participants