diff --git a/docs/src/main/asciidoc/spring-cloud-commons.adoc b/docs/src/main/asciidoc/spring-cloud-commons.adoc index 848de8db..7a2e012a 100644 --- a/docs/src/main/asciidoc/spring-cloud-commons.adoc +++ b/docs/src/main/asciidoc/spring-cloud-commons.adoc @@ -1026,6 +1026,21 @@ public class MyConfiguration { include::spring-cloud-circuitbreaker.adoc[leveloffset=+1] +== CachedRandomPropertySource + +Spring Cloud Context provides a `PropertySource` that caches random values based on a key. Outside of the caching +functionality it works the same as Spring Boot's https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/RandomValuePropertySource.java[`RandomValuePropertySource`]. +This random value might be useful in the case where you want a random value that is consistent even after the Spring Application +context restarts. The property value takes the form of `cachedrandom.[yourkey].[type]` where `yourkey` is the key in the cache. The `type` value can +be any type supported by Spring Boot's `RandomValuePropertySource`. + +==== +[source,properties,indent=0] +---- +myrandom=${cachedrandom.appname.value} +---- +==== + == Configuration Properties To see the list of all Spring Cloud Commons related configuration properties please check link:appendix.html[the Appendix page]. diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/util/random/CachedRandomPropertySource.java b/spring-cloud-context/src/main/java/org/springframework/cloud/util/random/CachedRandomPropertySource.java new file mode 100644 index 00000000..f276632d --- /dev/null +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/util/random/CachedRandomPropertySource.java @@ -0,0 +1,109 @@ +/* + * 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.util.random; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.boot.env.RandomValuePropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.util.StringUtils; + +/** + * @author Ryan Baxter + */ +public class CachedRandomPropertySource + extends PropertySource { + + private static final String NAME = "cachedrandom"; + + private static final String PREFIX = NAME + "."; + + private static Map> cache = new ConcurrentHashMap<>(); + + public CachedRandomPropertySource( + RandomValuePropertySource randomValuePropertySource) { + super(NAME, randomValuePropertySource); + + } + + CachedRandomPropertySource(RandomValuePropertySource randomValuePropertySource, + Map> cache) { + super(NAME, randomValuePropertySource); + this.cache = cache; + } + + @Override + public Object getProperty(String name) { + if (!name.startsWith(PREFIX) || name.length() == PREFIX.length()) { + return null; + } + else { + if (logger.isTraceEnabled()) { + logger.trace("Generating random property for '" + name + "'"); + } + // TO avoid any weirdness from the type or key including a "." we look for the + // last "." and substring everything instead of splitting on the "." + String keyAndType = name.substring(PREFIX.length()); + int lastIndexOfDot = keyAndType.lastIndexOf("."); + if (lastIndexOfDot < 0) { + return null; + } + String key = keyAndType.substring(0, lastIndexOfDot); + String type = keyAndType.substring(lastIndexOfDot + 1); + if (StringUtils.hasText(key) && StringUtils.hasText(type)) { + return getRandom(type, key); + } + else { + return null; + } + } + } + + private Object getRandom(String type, String key) { + Map randomValueCache = getCacheForKey(key); + if (logger.isDebugEnabled()) { + logger.debug("Looking in random cache for key " + key + " with type " + type); + } + return randomValueCache.computeIfAbsent(type, (theType) -> { + if (logger.isDebugEnabled()) { + logger.debug( + "No random value found in cache for key and value, generating a new value"); + } + return getSource().getProperty("random." + type); + }); + } + + private Map getCacheForKey(String key) { + if (logger.isDebugEnabled()) { + logger.debug("Looking in random cache for key: " + key); + } + return cache.computeIfAbsent(key, theKey -> { + if (logger.isDebugEnabled()) { + logger.debug("No cached value found for key: " + key); + } + return new ConcurrentHashMap<>(); + }); + } + + public static void clearCache() { + if (cache != null) { + cache.clear(); + } + } + +} diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/util/random/CachedRandomPropertySourceAutoConfiguration.java b/spring-cloud-context/src/main/java/org/springframework/cloud/util/random/CachedRandomPropertySourceAutoConfiguration.java new file mode 100644 index 00000000..5158e9e9 --- /dev/null +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/util/random/CachedRandomPropertySourceAutoConfiguration.java @@ -0,0 +1,48 @@ +/* + * 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.util.random; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.env.RandomValuePropertySource; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; + +/** + * @author Ryan Baxter + */ +@Configuration(proxyBeanMethods = false) +public class CachedRandomPropertySourceAutoConfiguration { + + @Autowired + ConfigurableEnvironment environment; + + @PostConstruct + public void initialize() { + MutablePropertySources propertySources = environment.getPropertySources(); + PropertySource propertySource = propertySources + .get(RandomValuePropertySource.RANDOM_PROPERTY_SOURCE_NAME); + if (propertySource != null) { + propertySources.addLast(new CachedRandomPropertySource( + RandomValuePropertySource.class.cast(propertySource))); + } + } + +} diff --git a/spring-cloud-context/src/main/resources/META-INF/spring.factories b/spring-cloud-context/src/main/resources/META-INF/spring.factories index fb151720..0fde0d1b 100644 --- a/spring-cloud-context/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-context/src/main/resources/META-INF/spring.factories @@ -15,4 +15,5 @@ org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration,\ org.springframework.cloud.bootstrap.encrypt.EncryptionBootstrapConfiguration,\ org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration,\ -org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration +org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\ +org.springframework.cloud.util.random.CachedRandomPropertySourceAutoConfiguration diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/context/refresh/ContextRefresherIntegrationTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/context/refresh/ContextRefresherIntegrationTests.java index a2064144..c9fb4d35 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/context/refresh/ContextRefresherIntegrationTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/context/refresh/ContextRefresherIntegrationTests.java @@ -37,7 +37,7 @@ import static org.assertj.core.api.BDDAssertions.then; @RunWith(SpringRunner.class) @SpringBootTest(classes = TestConfiguration.class, - properties = { "spring.datasource.hikari.read-only=false" }) + properties = { "spring.datasource.hikari.read-only=false", "debug=true" }) public class ContextRefresherIntegrationTests { @Autowired @@ -81,6 +81,17 @@ public class ContextRefresherIntegrationTests { then(this.properties.getMessage()).isEqualTo("Hello scope!"); } + @Test + @DirtiesContext + 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); + } + @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(TestProperties.class) @EnableAutoConfiguration @@ -96,6 +107,10 @@ public class ContextRefresherIntegrationTests { private int delay; + private Long cachedRandomLong; + + private Long randomLong; + @ManagedAttribute public String getMessage() { return this.message; @@ -114,6 +129,22 @@ public class ContextRefresherIntegrationTests { 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/java/org/springframework/cloud/util/random/CachedRandomPropertySourceTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/util/random/CachedRandomPropertySourceTests.java new file mode 100644 index 00000000..9de4fcb4 --- /dev/null +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/util/random/CachedRandomPropertySourceTests.java @@ -0,0 +1,79 @@ +/* + * 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.util.random; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.boot.env.RandomValuePropertySource; +import org.springframework.test.annotation.DirtiesContext; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author Ryan Baxter + */ +@RunWith(MockitoJUnitRunner.class) +@DirtiesContext +public class CachedRandomPropertySourceTests { + + @Mock + RandomValuePropertySource randomValuePropertySource; + + @Before + public void setup() { + + when(randomValuePropertySource.getProperty(eq("random.long"))) + .thenReturn(new Long(1234)); + } + + @Test + public void getProperty() { + Map> cache = new HashMap<>(); + Map typeCache = new HashMap<>(); + typeCache.put("long", new Long(5678)); + Map spyedTypeCache = spy(typeCache); + cache.put("foo", spyedTypeCache); + Map> spyedCache = spy(cache); + + CachedRandomPropertySource cachedRandomPropertySource = new CachedRandomPropertySource( + randomValuePropertySource, spyedCache); + then(cachedRandomPropertySource.getProperty("foo.app.long")).isNull(); + then(cachedRandomPropertySource.getProperty("cachedrandom.app")).isNull(); + + then(cachedRandomPropertySource.getProperty("cachedrandom.app.long")) + .isEqualTo(new Long(1234)); + then(cachedRandomPropertySource.getProperty("cachedrandom.foo.long")) + .isEqualTo(new Long(5678)); + verify(spyedCache, times(1)).computeIfAbsent(eq("app"), isA(Function.class)); + verify(spyedTypeCache, times(1)).computeIfAbsent(eq("long"), isA(Function.class)); + } + +} 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 8c44cd81..f13cefa2 100644 --- a/spring-cloud-context/src/test/resources/META-INF/spring.factories +++ b/spring-cloud-context/src/test/resources/META-INF/spring.factories @@ -1,7 +1,8 @@ # Bootstrap components org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.bootstrap.TestBootstrapConfiguration,\ -org.springframework.cloud.bootstrap.TestHigherPriorityBootstrapConfiguration +org.springframework.cloud.bootstrap.TestHigherPriorityBootstrapConfiguration,\ +org.springframework.cloud.util.random.CachedRandomPropertySourceAutoConfiguration org.springframework.boot.env.EnvironmentPostProcessor=\ diff --git a/spring-cloud-context/src/test/resources/application.properties b/spring-cloud-context/src/test/resources/application.properties index 80038522..16118aeb 100644 --- a/spring-cloud-context/src/test/resources/application.properties +++ b/spring-cloud-context/src/test/resources/application.properties @@ -1,5 +1,7 @@ message:Hello scope! delay:0 +cachedRandomLong: ${cachedrandom.app.long} +randomLong: ${random.long} debug:true #logging.level.org.springframework.web: DEBUG #logging.level.org.springframework.context.annotation: DEBUG