spring-projects / spring-boot

Spring Boot
https://spring.io/projects/spring-boot
Apache License 2.0
74.56k stars 40.55k forks source link

Support null values when binding properties #24133

Open chengaofeng opened 3 years ago

chengaofeng commented 3 years ago

Application has a ben annotated with@ConfigurationProperties("test") that has a List<String> attribute called users. at the beginning, one item listed in the external configuration file

test:
  users:
  - Andy

as the application is running, change the configuration file to the following

test:
  users:

or

test:
  users: null

what I expect is users=null or users =[], while the result is still users[0]=Andy

it‘s ok for the following pattern :

test:
  users: ""

Here's a test that fails with Spring Boot 2.1.17.RELEASE

@SpringBootTest(classes=ConfigPropertiesTest.class)
@RunWith(SpringRunner.class)
@EnableConfigurationProperties(ConfigPropertiesTest.TestProperties.class)
@TestPropertySource(properties = {"test.users[0]=Andy"})
public class ConfigPropertiesTest {

    @Autowired
    private TestProperties properties;

    @Autowired
    private ConfigurationPropertiesBindingPostProcessor processor;

    @Autowired
    private ConfigurableEnvironment environment;

    @Test
    public void liveRebindToNull() throws Exception {
        assertEquals(properties.getUsers().get(0),"Andy");
        setNullProperties("test.users");
    assertTrue(environment.getPropertySources().get("test").containsProperty("test.users"));;
        assertNull(environment.getPropertySources().get("test").getProperty("test.users"));
        processor.postProcessBeforeInitialization(properties, "testProperties");
        assertEquals(properties.getUsers().size(), 0); //**_TEST NG_**
    }

    /**
     * TestPropertyValues has no public method set null value, using reflect to do it
     * 
     * @param prefix
     * @throws Exception
     */
    private void setNullProperties(String prefix) throws Exception {
        TestPropertyValues testPropertyValues =  TestPropertyValues.empty();
        Method method = TestPropertyValues.class.getDeclaredMethod("and", Stream.class);
        method.setAccessible(true);

        TestPropertyValues.Pair pair = new TestPropertyValues.Pair(prefix,null);
        testPropertyValues = (TestPropertyValues) method.invoke(testPropertyValues, Stream.of(pair));

        testPropertyValues.applyTo(environment);
    }

    @ConfigurationProperties("test")
    public static class TestProperties {

        private List<String> users;

        public List<String> getUsers() {
            return users;
        }

        public void setUsers(List<String> users) {
            this.users = users;
        }
    }
}
wilkinsona commented 3 years ago

Spring Boot does not support the reloading of properties at runtime. You may be interested in Spring Cloud's support for changing the environment and its refresh scope.

chengaofeng commented 3 years ago

I am sorry not make myself clear.My project is indeed a spring cloud application, The Properties bean is also annotated by @RefreshScope.While The config server is alibaba‘s nacos. When I change the external config file as above(set the value to null), the properties's value didn't change as expect.

In debug mode, when the external config file changed from

test:
  users:
  - Andy

to

test:
  users:

I can find that the propertySource in Environment#propertySources#propertySourceList changed to test.users => null, and then an EnvironmentChangeEvent published. but after the ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization method invoked, the value of the users in the Properties bean is still users[0]=Andy

Then I dig into the source of

org.springframework.boot.context.properties.source.SpringConfigurationPropertySource#find(PropertyMapping mapping)

private ConfigurationProperty find(PropertyMapping mapping) {
    String propertySourceName = mapping.getPropertySourceName();
    Object value = getPropertySource().getProperty(propertySourceName);
    if (value == null) {
        return null;
    }
    ...
}   

when the value is null, the method is return null, which means no such item. while in my situation,set test.users=>null is different from not configuring at all

Spring Cloud version: Hoxton.SR8
Spring Boot version: 2.2.5.RELEASE

@wilkinsona,I'm appreciate that you could take a look at this problem again.

philwebb commented 3 years ago

See https://github.com/spring-projects/spring-framework/issues/25142 for background on null values in a MapPropertySource and https://github.com/spring-projects/spring-boot/issues/21542#issuecomment-635107511 for a previous Boot issue.

philwebb commented 3 years ago

This is a limitation of the way that null is handled both in Boot and Spring Framework. I'm afraid the fix for this will be quite involved and is unlikely to happen soon.

wilkinsona commented 2 years ago

28139 describes another use case for being able to clear the value of a property somehow.

wilkinsona commented 2 years ago

https://github.com/spring-projects/spring-boot/pull/31037 is another case of #28139.

Sroca3 commented 5 months ago

@philwebb do you know what all is needed to get this fixed? If https://github.com/spring-projects/spring-framework/issues/19986 is addressed, would it be easy for spring-boot to support nulls? Trying to see how to move this forward.

wilkinsona commented 5 months ago

It's not just https://github.com/spring-projects/spring-framework/issues/19986. Spring Framework's Environment abstraction would also have to be able to distinguish between an absent property and a property with a null value. https://github.com/spring-projects/spring-framework/issues/25142 was opened to explore this but it was effectively rejected and turned into a documentation issue.