[spring-projects/spring-boot]@ConfigurationProperties 意外的基于构造函数的初始化会导致行为不一致

2024-04-10 335 views
5

考虑以下测试用例:

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = { BindingTest.Config.class })
    @EnableConfigurationProperties
    @TestPropertySource
    public class BindingTest {

        @Autowired FooProperties fooProperties;

        @Test
        public void shouldInjectBar() {
            assertThat(fooProperties.getBar()).isEqualTo("baz");
        }

        @Configuration
        public static class Config {
            @Bean
            public FooProperties fooProperties() {
                return new FooProperties();
            }
        }

        @ConfigurationProperties("foo")
        @Validated
        public static class FooProperties {
            @NotNull @Getter @Setter
            private String bar;

            public FooProperties() {}

            public FooProperties(String bar) { // (1)
                 this.bar = bar;
            }
        }
    }

BindingTest.properties:

foo.bar=baz
foo=unrelated # (2)

我预计fooProperties.bar会被填充,或者至少@NotNull会抛出验证错误。

但是,在存在单参数构造函数 (1) 和冲突属性 (2) 的情况下(例如,它可能是完全不相关的系统环境变量),我得到以下结果:

  • fooProperties.barnull
  • 没有抛出验证错误

发生这种情况是因为Binder决定使用其单参数构造ObjectToObjectConverter函数进行初始化FooProperties(尽管事实上FooProperties是在方法中显式创建的@Bean),创建 的新实例FooProperties并对它应用验证,但ConfigurationPropertiesBindingPostProcessor忽略该新实例。

这适用于 Spring Boot 2.0.x、2.1.x、2.2.x。

回答

8

感谢您的测试用例。为了供我们将来参考,这里有一个稍微修改过的版本,它是独立的并且不依赖于 Lombok:

package com.example.demo;

import static org.assertj.core.api.Assertions.assertThat;

import javax.validation.constraints.NotNull;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.validation.annotation.Validated;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = { BindingTest.Config.class })
@EnableConfigurationProperties
@TestPropertySource(properties = {"foo.bar=baz", "foo=unrelated"})
public class BindingTest {

    @Autowired
    FooProperties fooProperties;

    @Test
    public void shouldInjectBar() {
        assertThat(fooProperties.getBar()).isEqualTo("baz");
    }

    @Configuration
    public static class Config {

        @Bean
        public FooProperties fooProperties() {
            return new FooProperties();
        }

    }

    @ConfigurationProperties("foo")
    @Validated
    public static class FooProperties {

        @NotNull
        private String bar;

        public FooProperties() {}

        public FooProperties(String bar) { // (1)
            this.bar = bar;
        }

        public String getBar() {
            return bar;
        }

        public void setBar(String bar) {
            this.bar = bar;
        }

    }

}

关于该示例的一些评论:

@ConfigurationPropertiesa有构造函数是不寻常的。它们通常被认为是具有默认构造函数以及 getter 和 setter 的 JavaBean。构造函数的目的是什么?

同时拥有foofoo.bar作为属性将导致 YAML 出现问题,因为无法解析以下内容:

foo: unrelated
  bar: baz

我们自己过去也犯过这个错误,并打算在 2.2 中纠正它。有关详细信息,请参阅https://github.com/spring-projects/spring-boot/issues/12510 。

foo话虽如此,我认为绑定器在绑定到用作foo其前缀的内容时忽略指定的属性是有意义的。我们可能应该只绑定其后代的属性foo并忽略foo其自身。

7

@wilkinsona我想为此努力。请让我知道我是否可以开始工作?

8

@Raheela1024 谢谢你的提议。如果您需要帮助,请继续告诉我们。

1

@Raheela1024 怎么样?如果有什么需要我们帮忙的,请告诉我们。

5

@mbhave 抱歉,我很忙,但现在正在研究,很快就会更新。

6

@mbhave 如果我们需要恢复,我需要一些有关修复的帮助

“更名logging.filelogging.file.name

或者我们需要修复到ConfigurationPropertiesBinderbind方法中。

9

@Raheela1024 我想提供帮助,但我没有看到链接。我们不应该恢复更改,这是持续努力的一部分,属性名称不应与组名称匹配。您能详细说明一下问题是什么吗?你可以分享你的叉子上有什么。

4

@snicoll 感谢您的回复。我ConfigurationPropertiesAutoConfigurationTests通过将环境设置为文件来将测试用例更改为文件来调试该问题,"foo:"并观察到在 Binder 类 containsNoDescendantOf方法中返回 true,而我没有提供后代字段。我是否需要更新此方法,我的方向是否正确?

7

@Raheela1024 正如 @wilkinsona在这里@ConfigurationProperties提到的,我们希望忽略与我们绑定的 bean前缀匹配的任何属性。例如,我们想要绑定foo.*到带有前缀的东西,但如果设置了属性则foo忽略该属性。foo这将是班级的一个变化Binder。看起来更改比我们最初想象的要复杂一些,因为需要更新相当多的测试。我们非常感谢您提供帮助,但我们想自己解决这个问题。

1

我们可能需要考虑做一些事情,以便此更改仅影响@ConfigurationProperties.活页夹中的更改看起来将是活页夹行为的根本性更改,其中涉及大量测试更改。

7

我已经解决了这个问题,但它包括 API 更改,所以我只将其应用到 2.5

5

重新开放以考虑嵌套属性

4

嵌套属性示例

public class JavaBeanWithPublicConstructor {

    private String value;

    private Bar bar = new Bar();

    public JavaBeanWithPublicConstructor() {
    }

    public JavaBeanWithPublicConstructor(String value) {
        setValue(value);
    }

    public String getValue() {
        return this.value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public Bar getBar() {
        return bar;
    }

    public void setBar(Bar bar) {
        this.bar = bar;
    }

    static class Bar {
        private String baz;

        public Bar() {
        }

        public Bar(String baz) {
            this.baz = baz;
        }

        public String getBaz() {
            return baz;
        }

        public void setBaz(String baz) {
            this.baz = baz;
        }
    }
}
5

我们将在#25368 中查看嵌套的