diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java b/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java index dc579ab7..82f51c73 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java @@ -17,6 +17,7 @@ package org.springframework.cloud.autoconfigure; import java.util.HashSet; +import java.util.List; import java.util.Set; import javax.annotation.PostConstruct; @@ -36,6 +37,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.cloud.context.refresh.ConfigDataContextRefresher; @@ -53,6 +56,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.weaving.LoadTimeWeaverAware; import org.springframework.core.env.Environment; import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.style.ToStringCreator; import org.springframework.instrument.classloading.LoadTimeWeaver; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -68,6 +72,7 @@ import org.springframework.util.StringUtils; @ConditionalOnClass(RefreshScope.class) @ConditionalOnProperty(name = RefreshAutoConfiguration.REFRESH_SCOPE_ENABLED, matchIfMissing = true) @AutoConfigureBefore(HibernateJpaAutoConfiguration.class) +@EnableConfigurationProperties(RefreshAutoConfiguration.RefreshProperties.class) public class RefreshAutoConfiguration { /** @@ -100,16 +105,17 @@ public class RefreshAutoConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnBootstrapEnabled - public LegacyContextRefresher legacyContextRefresher(ConfigurableApplicationContext context, RefreshScope scope) { - return new LegacyContextRefresher(context, scope); + public LegacyContextRefresher legacyContextRefresher(ConfigurableApplicationContext context, RefreshScope scope, + RefreshProperties properties) { + return new LegacyContextRefresher(context, scope, properties); } @Bean @ConditionalOnMissingBean @ConditionalOnBootstrapDisabled public ConfigDataContextRefresher configDataContextRefresher(ConfigurableApplicationContext context, - RefreshScope scope) { - return new ConfigDataContextRefresher(context, scope); + RefreshScope scope, RefreshProperties properties) { + return new ConfigDataContextRefresher(context, scope, properties); } @Bean @@ -117,6 +123,33 @@ public class RefreshAutoConfiguration { return new RefreshEventListener(contextRefresher); } + @ConfigurationProperties("spring.cloud.refresh") + public static class RefreshProperties { + + /** + * Additional property sources to retain during a refresh. Typically only system + * property sources are retained. This property allows property sources, such as + * property sources created by EnvironmentPostProcessors to be retained as well. + */ + private List additionalPropertySourcesToRetain; + + public List getAdditionalPropertySourcesToRetain() { + return this.additionalPropertySourcesToRetain; + } + + public void setAdditionalPropertySourcesToRetain(List additionalPropertySourcesToRetain) { + this.additionalPropertySourcesToRetain = additionalPropertySourcesToRetain; + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("additionalPropertySourcesToRetain", additionalPropertySourcesToRetain).toString(); + + } + + } + @Configuration(proxyBeanMethods = false) @ConditionalOnClass(name = "javax.persistence.EntityManagerFactory") protected static class JpaInvokerConfiguration implements LoadTimeWeaverAware { diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ConfigDataContextRefresher.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ConfigDataContextRefresher.java index f95d9f28..19fac06c 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ConfigDataContextRefresher.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ConfigDataContextRefresher.java @@ -18,6 +18,7 @@ package org.springframework.cloud.context.refresh; import org.springframework.boot.DefaultBootstrapContext; import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor; +import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; import org.springframework.cloud.context.scope.refresh.RefreshScope; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.MutablePropertySources; @@ -31,10 +32,16 @@ import org.springframework.core.io.DefaultResourceLoader; */ public class ConfigDataContextRefresher extends ContextRefresher { + @Deprecated public ConfigDataContextRefresher(ConfigurableApplicationContext context, RefreshScope scope) { super(context, scope); } + public ConfigDataContextRefresher(ConfigurableApplicationContext context, RefreshScope scope, + RefreshAutoConfiguration.RefreshProperties properties) { + super(context, scope, properties); + } + @Override protected void updateEnvironment() { if (logger.isTraceEnabled()) { diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ContextRefresher.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ContextRefresher.java index 7380b82e..36156566 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ContextRefresher.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ContextRefresher.java @@ -27,6 +27,7 @@ import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; import org.springframework.cloud.context.environment.EnvironmentChangeEvent; import org.springframework.cloud.context.scope.refresh.RefreshScope; import org.springframework.context.ConfigurableApplicationContext; @@ -39,6 +40,7 @@ import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; import org.springframework.core.env.StandardEnvironment; +import org.springframework.util.CollectionUtils; import org.springframework.web.context.support.StandardServletEnvironment; /** @@ -62,13 +64,23 @@ public abstract class ContextRefresher { StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME, StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME, "configurationProperties")); + protected final List additionalPropertySourcesToRetain; + private ConfigurableApplicationContext context; private RefreshScope scope; + @Deprecated protected ContextRefresher(ConfigurableApplicationContext context, RefreshScope scope) { + this(context, scope, new RefreshAutoConfiguration.RefreshProperties()); + } + + @SuppressWarnings("unchecked") + protected ContextRefresher(ConfigurableApplicationContext context, RefreshScope scope, + RefreshAutoConfiguration.RefreshProperties properties) { this.context = context; this.scope = scope; + additionalPropertySourcesToRetain = properties.getAdditionalPropertySourcesToRetain(); } protected ConfigurableApplicationContext getContext() { @@ -102,7 +114,12 @@ public abstract class ContextRefresher { MutablePropertySources capturedPropertySources = environment.getPropertySources(); // Only copy the default property source(s) and the profiles over from the main // environment (everything else should be pristine, just like it was on startup). - for (String name : DEFAULT_PROPERTY_SOURCES) { + List propertySourcesToRetain = new ArrayList<>(Arrays.asList(DEFAULT_PROPERTY_SOURCES)); + if (!CollectionUtils.isEmpty(additionalPropertySourcesToRetain)) { + propertySourcesToRetain.addAll(additionalPropertySourcesToRetain); + } + + for (String name : propertySourcesToRetain) { if (input.getPropertySources().contains(name)) { if (capturedPropertySources.contains(name)) { capturedPropertySources.replace(name, input.getPropertySources().get(name)); diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/LegacyContextRefresher.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/LegacyContextRefresher.java index c9980712..534ff0e2 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/LegacyContextRefresher.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/LegacyContextRefresher.java @@ -21,6 +21,7 @@ import java.util.Arrays; import org.springframework.boot.Banner; import org.springframework.boot.WebApplicationType; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; import org.springframework.cloud.bootstrap.BootstrapApplicationListener; import org.springframework.cloud.bootstrap.BootstrapConfigFileApplicationListener; import org.springframework.cloud.context.scope.refresh.RefreshScope; @@ -37,10 +38,16 @@ import static org.springframework.cloud.util.PropertyUtils.BOOTSTRAP_ENABLED_PRO */ public class LegacyContextRefresher extends ContextRefresher { + @Deprecated public LegacyContextRefresher(ConfigurableApplicationContext context, RefreshScope scope) { super(context, scope); } + public LegacyContextRefresher(ConfigurableApplicationContext context, RefreshScope scope, + RefreshAutoConfiguration.RefreshProperties properties) { + super(context, scope, properties); + } + @Override protected void updateEnvironment() { addConfigFilesToEnvironment(); diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/context/refresh/ConfigDataContextRefresherIntegrationTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/context/refresh/ConfigDataContextRefresherIntegrationTests.java new file mode 100644 index 00000000..39e77707 --- /dev/null +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/context/refresh/ConfigDataContextRefresherIntegrationTests.java @@ -0,0 +1,262 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.context.refresh; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.context.config.ConfigData; +import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor; +import org.springframework.boot.context.config.ConfigDataLoader; +import org.springframework.boot.context.config.ConfigDataLoaderContext; +import org.springframework.boot.context.config.ConfigDataLocation; +import org.springframework.boot.context.config.ConfigDataLocationNotFoundException; +import org.springframework.boot.context.config.ConfigDataLocationResolver; +import org.springframework.boot.context.config.ConfigDataLocationResolverContext; +import org.springframework.boot.context.config.ConfigDataResource; +import org.springframework.boot.context.config.ConfigDataResourceNotFoundException; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; + +import static org.assertj.core.api.BDDAssertions.then; + +public class ConfigDataContextRefresherIntegrationTests { + + private static final String EPP_VALUE = "configdatarefresh.epp.count"; + + private static final String EPP_ENABLED = "configdatarefresh.epp.enabled"; + + private TestProperties properties; + + private ConfigurableEnvironment environment; + + private ContextRefresher refresher; + + private ConfigurableApplicationContext context; + + @BeforeEach + public void setup() { + context = new SpringApplication(TestConfiguration.class).run("--spring.datasource.hikari.read-only=false", + "--spring.profiles.active=configdatarefresh", "--" + EPP_ENABLED + "=true", "--server.port=0"); + properties = context.getBean(TestProperties.class); + environment = context.getBean(ConfigurableEnvironment.class); + refresher = context.getBean(ContextRefresher.class); + } + + @AfterEach + public void after() { + if (context != null) { + context.close(); + } + } + + @Test + public void testSimpleProperties() { + then(this.properties.getMessage()).isEqualTo("Hello scope!"); + // Change the dynamic property source... + this.properties.setMessage("Foo"); + // ...but don't refresh, so the bean stays the same: + then(this.properties.getMessage()).isEqualTo("Foo"); + } + + @Test + public void testAdditionalPropertySourcesToRetain() { + then(environment.getProperty(EPP_VALUE)).isEqualTo("1"); + // ...and then refresh, to see if property source is retained during refresh + // that means an updated test datasource with EPP_VALUE set to 10 + TestConfigDataLocationResolver.count.set(10); + this.refresher.refresh(); + then(environment.getProperty(EPP_VALUE)).isEqualTo("10"); + } + + @Test + public void testRefreshBean() { + then(this.properties.getMessage()).isEqualTo("Hello scope!"); + // Change the dynamic property source... + this.properties.setMessage("Foo"); + // ...and then refresh, so the bean is re-initialized: + this.refresher.refresh(); + then(this.properties.getMessage()).isEqualTo("Hello scope!"); + } + + @Test + public void testUpdateHikari() { + then(this.properties.getMessage()).isEqualTo("Hello scope!"); + TestPropertyValues.of("spring.datasource.hikari.read-only=true").applyTo(this.environment); + // ...and then refresh, so the bean is re-initialized: + this.refresher.refresh(); + then(this.properties.getMessage()).isEqualTo("Hello scope!"); + } + + @Test + @Disabled + public void testCachedRandom() { + // long cachedRandomLong = properties.getCachedRandomLong(); + long randomLong = properties.randomLong(); + // then(cachedRandomLong).isNotNull(); + this.refresher.refresh(); + then(randomLong).isNotEqualTo(properties.randomLong()); + // then(cachedRandomLong).isEqualTo(properties.cachedRandomLong); + } + + protected static class TestEnvPostProcessor implements EnvironmentPostProcessor, Ordered { + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + if (environment.getProperty(EPP_ENABLED, Boolean.class, false)) { + Map source = new HashMap<>(); + source.put("spring.cloud.refresh.additional-property-sources-to-retain", getClass().getSimpleName()); + source.put("spring.config.import", "testdatasource:"); + MapPropertySource propertySource = new MapPropertySource(getClass().getSimpleName(), source); + environment.getPropertySources().addFirst(propertySource); + } + } + + @Override + public int getOrder() { + return ConfigDataEnvironmentPostProcessor.ORDER - 1; + } + + } + + protected static class TestConfigDataResource extends ConfigDataResource { + + private final int count; + + public TestConfigDataResource(int count) { + this.count = count; + } + + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TestConfigDataResource that = (TestConfigDataResource) o; + return Objects.equals(this.count, that.count); + } + + @Override + public int hashCode() { + return Objects.hash(this.count); + } + + } + + protected static class TestConfigDataLocationResolver + implements ConfigDataLocationResolver { + + static AtomicInteger count = new AtomicInteger(1); + + @Override + public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) { + return location.hasPrefix("testdatasource:"); + } + + @Override + public List resolve(ConfigDataLocationResolverContext context, + ConfigDataLocation location) + throws ConfigDataLocationNotFoundException, ConfigDataResourceNotFoundException { + return Collections.singletonList(new TestConfigDataResource(count.get())); + } + + } + + protected static class TestConfigDataLoader implements ConfigDataLoader { + + @Override + public ConfigData load(ConfigDataLoaderContext context, TestConfigDataResource resource) + throws ConfigDataResourceNotFoundException { + Map stringStringMap = Collections.singletonMap(EPP_VALUE, resource.count); + return new ConfigData( + Collections.singletonList(new MapPropertySource("testconfigdatadatasource", stringStringMap))); + } + + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(TestProperties.class) + @EnableAutoConfiguration + protected static class TestConfiguration { + + } + + @ConfigurationProperties + protected static class TestProperties { + + private String message; + + private int delay; + + // private Long cachedRandomLong; + + private Long randomLong; + + public String getMessage() { + return this.message; + } + + public void setMessage(String message) { + this.message = message; + } + + public int getDelay() { + return this.delay; + } + + public void setDelay(int delay) { + this.delay = delay; + } + + /* + * public long getCachedRandomLong() { return cachedRandomLong; } + * + * public void setCachedRandomLong(long cachedRandomLong) { this.cachedRandomLong + * = cachedRandomLong; } + */ + + public long randomLong() { + return randomLong; + } + + public void setRandomLong(long randomLong) { + this.randomLong = randomLong; + } + + } + +} diff --git a/spring-cloud-context/src/test/resources/META-INF/spring.factories b/spring-cloud-context/src/test/resources/META-INF/spring.factories index 5cae1c05..0dacb8dc 100644 --- a/spring-cloud-context/src/test/resources/META-INF/spring.factories +++ b/spring-cloud-context/src/test/resources/META-INF/spring.factories @@ -4,3 +4,13 @@ org.springframework.cloud.bootstrap.TestBootstrapConfiguration,\ org.springframework.cloud.bootstrap.TestHigherPriorityBootstrapConfiguration,\ org.springframework.cloud.util.random.CachedRandomPropertySourceAutoConfiguration +org.springframework.boot.env.EnvironmentPostProcessor=\ +org.springframework.cloud.context.refresh.ConfigDataContextRefresherIntegrationTests.TestEnvPostProcessor + +# ConfigData Location Resolvers +org.springframework.boot.context.config.ConfigDataLocationResolver=\ +org.springframework.cloud.context.refresh.ConfigDataContextRefresherIntegrationTests.TestConfigDataLocationResolver + +# ConfigData Loaders +org.springframework.boot.context.config.ConfigDataLoader=\ +org.springframework.cloud.context.refresh.ConfigDataContextRefresherIntegrationTests.TestConfigDataLoader \ No newline at end of file