diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinder.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinder.java index ff78d709f..ca3340a96 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinder.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinder.java @@ -196,11 +196,16 @@ private boolean rebind(String name, ApplicationContext appContext) { return false; } + public static boolean skipReset = false; + /** * Reset bean properties to their class-level defaults so that removed properties do * not retain stale values after rebinding. */ private void resetBeanToDefaults(Object bean) { + if (skipReset) { + return; + } Class targetClass = AopUtils.getTargetClass(bean); if (!hasDefaultConstructor(targetClass)) { // Beans that have no default constructor (for example constructor-bound beans diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinderLifecycleIntegrationTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinderLifecycleIntegrationTests.java index 525dee9f0..2bf93fa7d 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinderLifecycleIntegrationTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinderLifecycleIntegrationTests.java @@ -16,11 +16,15 @@ package org.springframework.cloud.context.properties; +import org.assertj.core.api.AtomicBooleanAssert; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -36,6 +40,9 @@ import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.test.annotation.DirtiesContext; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.BDDAssertions.then; @SpringBootTest(classes = TestConfiguration.class) @@ -50,6 +57,45 @@ public class ConfigurationPropertiesRebinderLifecycleIntegrationTests { @Autowired private ConfigurableEnvironment environment; + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void racy_properties(boolean skipReset) throws Exception { + ConfigurationPropertiesRebinder.skipReset = skipReset; + int violations = 0; + var run = new AtomicBoolean(true); + + // message is not null at start of test + then(this.properties.getMessage()).isEqualTo("Hello scope!"); + + // run the test for 1 second + new Thread(() -> { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + run.set(false); + } + }).start(); + + // a thread calls rebind() in a loop + new Thread(() -> { + while (run.get()) { + rebinder.rebind(); + } + }).start(); + + // read the properties in a loop + while (run.get()) { + String message = properties.getMessage(); + // we expect that message is never null as we don't change the properties + if (message == null) { + violations++; + } + } + assertThat(violations).isEqualTo(0); + } + @Test @DirtiesContext public void testRefresh() {