Skip to content
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

@ConfigurationProperties on a @Bean method does not work when the bean has a constructor that is eligible for constructor binding #33710

Closed
StijnArnauts opened this issue Jan 6, 2023 · 2 comments
Assignees
Labels
type: regression A regression from a previous release
Milestone

Comments

@StijnArnauts
Copy link

StijnArnauts commented Jan 6, 2023

Using Spring Boot 3.0.1, I have the following POJO on which I wish to bind a certain property:

public class TestProperties {

    private String prop1;

    private String prop2;

    public TestProperties(String prop1, String prop2) {
        this.prop1 = prop1;
        this.prop2 = prop2;
    }

    public String getProp1() {
        return prop1;
    }

    public void setProp1(String prop1) {
        this.prop1 = prop1;
    }

    public String getProp2() {
        return prop2;
    }

    public void setProp2(String prop2) {
        this.prop2 = prop2;
    }

}

Note that this is a class from a third-party library not under my control. I need to make this available as a Bean, and I wish to set a property on it:

    # (in application.properties) testprop.prop1=something

    @Bean
    @ConfigurationProperties(prefix = "testprop")
    public TestProperties testProperties() {
        return new TestProperties("1", "2");
    }

The previous code fails silently, and my property is not set.

It seems as if the new Spring Boot 3 rules regarding automatic ConstructorBinding have something to do with this. If I remove the parameterized constructor from the TestProperties class, or if I add a second constructor, the problem goes away and my property is set correctly. However, since I return an already constructed object from the Bean method, I would expect Spring Boot to always use setter binding instead of constructor binding.

(This is a simplified example. In reality, TestProperties is constructed using a builder, and the parameterized constructor is package private, intended to be used only by that builder.)

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jan 6, 2023
@StijnArnauts StijnArnauts changed the title ConfigurationProperties fails if no applicable constructor can be found Setter binding fails on Bean method annotated with ConfigurationPoperties Jan 6, 2023
@StijnArnauts StijnArnauts changed the title Setter binding fails on Bean method annotated with ConfigurationPoperties Bean method annotated with ConfigurationProperties does not work consistently Jan 6, 2023
@wilkinsona wilkinsona self-assigned this Jan 6, 2023
@wilkinsona
Copy link
Member

wilkinsona commented Jan 6, 2023

Minimal repro:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

@EnableConfigurationProperties
public class Gh33710Application {
	
	@Bean
	@ConfigurationProperties("testprop")
	TestProperties testProperties() {
		return new TestProperties("1", "2");
	}

	public static void main(String[] args) {
		String prop1 = SpringApplication.run(Gh33710Application.class, "--testprop.prop1=something").getBean(TestProperties.class).getProp1();
		System.out.println(prop1);
	}
	
	static class TestProperties {

	    private String prop1;

	    private String prop2;

	    public TestProperties(String prop1, String prop2) {
	        this.prop1 = prop1;
	        this.prop2 = prop2;
	    }

	    public String getProp1() {
	        return prop1;
	    }

	    public void setProp1(String prop1) {
	        this.prop1 = prop1;
	    }

	    public String getProp2() {
	        return prop2;
	    }

	    public void setProp2(String prop2) {
	        this.prop2 = prop2;
	    }
	}

}

The ConfigurationPropertiesBindingPostProcessor correctly determines that the bind method should be JAVA_BEAN and calls the binder. The binder then incorrectly performs constructor binding and returns a new, constructor-bound instance which the post-processor ignores.

The problem can be worked around by adding @Autowired to the constructor of TestProperties. This prevents constructor binding, allowing Java bean-based binding to be used instead. Other than that, it has no effect. It is rather unintuitive though as the constructor will never actually be autowired as it's only ever called by application code.

@wilkinsona wilkinsona added type: regression A regression from a previous release and removed status: waiting-for-triage An issue we've not yet triaged labels Jan 6, 2023
@wilkinsona wilkinsona changed the title Bean method annotated with ConfigurationProperties does not work consistently @ConfigurationProperties on a @Bean method does not work when the bean has a constructor that is eligible for constructor binding Jan 6, 2023
@wilkinsona wilkinsona removed their assignment Jan 6, 2023
@StijnArnauts
Copy link
Author

Thank you for the workaround. Unfortunately, in my case TestProperties is a third party class which I cannot change. I've removed the use of ConfigurationProperties alltogether for now.

@StijnArnauts StijnArnauts reopened this Jan 6, 2023
@philwebb philwebb added this to the 3.0.x milestone Jan 6, 2023
@philwebb philwebb self-assigned this Jan 6, 2023
@philwebb philwebb modified the milestones: 3.0.x, 3.0.2 Jan 7, 2023
philwebb added a commit that referenced this issue Jan 10, 2023
Update `DefaultBindConstructorProvider` so that deduced constructors
are not used if there is an existing value.

Prior to this commit, constructor detection logic was not compatible
with earlier versions of Spring Boot. With Spring Boot 3.0.1, given
a class of the following form:

	@ConfigurationProperties(prefix = "example")
	public class ExampleProperties {

	    @NestedConfigurationProperty
	    private final NestedProperty nested = new NestedProperty(
	    		"Default", "default");

	    public NestedProperty getNested() {
	        return nested;
	    }

	}

If `NestedProperty` has a single constructor with arguments, constructor
binding would be used. In Spring Boot 2.x, setter injection would have
been used.

The updated code now only uses constructor injection if an explicit
`@ConstructorBinding` annotation is present, or if there is no existing
value.

Fixes gh-33409
See gh-33710
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: regression A regression from a previous release
Projects
None yet
Development

No branches or pull requests

4 participants