From b022a39ba83fff8f14571ecf4884edd47f9d0d54 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 21 Aug 2015 12:22:31 +0100 Subject: [PATCH] Rebind configuration properties on startup in case environment changed The main driver for this was a config server use case, where the server wants to configure its own properties in the remote repo. It was working up to a point, but the beans in bootstrap context were not refreshed after the Environment was built so you had to manually refresh them to get them to point to (e.g.) config repos configured remotely. Fixes https://github.com/spring-cloud/spring-cloud-config/issues/139 --- .../RefreshAutoConfiguration.java | 50 ++++--- ...ationPropertiesBootstrapConfiguration.java | 52 +++++++ .../PropertySourceBootstrapConfiguration.java | 11 +- .../EncryptionBootstrapConfiguration.java | 17 --- .../ConfigurationPropertiesBeans.java | 132 ++++++++++++++++++ .../ConfigurationPropertiesRebinder.java | 109 +++------------ .../cloud/context/scope/GenericScope.java | 10 +- .../context/scope/refresh/RefreshScope.java | 75 +++++----- .../main/resources/META-INF/spring.factories | 1 + ...ionPropertiesRebinderIntegrationTests.java | 27 ++-- 10 files changed, 315 insertions(+), 169 deletions(-) create mode 100644 spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/ConfigurationPropertiesBootstrapConfiguration.java create mode 100644 spring-cloud-context/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesBeans.java 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 c141f301..c9d0516a 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,12 +17,12 @@ package org.springframework.cloud.autoconfigure; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration; @@ -44,10 +44,13 @@ import org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfigu import org.springframework.cloud.bootstrap.config.RefreshEndpoint; import org.springframework.cloud.context.environment.EnvironmentChangeEvent; import org.springframework.cloud.context.environment.EnvironmentManager; +import org.springframework.cloud.context.properties.ConfigurationPropertiesBeans; import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder; import org.springframework.cloud.context.restart.RestartEndpoint; import org.springframework.cloud.context.scope.refresh.RefreshScope; import org.springframework.cloud.logging.LoggingRebinder; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; @@ -138,33 +141,48 @@ public class RefreshAutoConfiguration { @Configuration @ConditionalOnBean(ConfigurationPropertiesBindingPostProcessor.class) protected static class ConfigurationPropertiesRebinderConfiguration implements - BeanFactoryAware { + ApplicationContextAware, SmartInitializingSingleton { - private BeanFactory context; + private ApplicationContext context; @Override - public void setBeanFactory(BeanFactory applicationContext) throws BeansException { + public void setApplicationContext(ApplicationContext applicationContext) { this.context = applicationContext; } @Bean - @ConditionalOnMissingBean(search=SearchStrategy.CURRENT) - public ConfigurationPropertiesRebinder configurationPropertiesRebinder() { - // Since this is a BeanPostProcessor we have to be super careful not to cause - // a cascade of bean instantiation. Knowing the *name* of the beans we need is - // super optimal, but a little brittle (unfortunately we have no choice). - ConfigurationPropertiesBindingPostProcessor binder = this.context - .getBean( - ConfigurationPropertiesBindingPostProcessorRegistrar.BINDER_BEAN_NAME, - ConfigurationPropertiesBindingPostProcessor.class); + @ConditionalOnMissingBean(search = SearchStrategy.CURRENT) + public ConfigurationPropertiesBeans configurationPropertiesBeans() { + // Since this is a BeanPostProcessor we have to be super careful not to + // cause a cascade of bean instantiation. Knowing the *name* of the beans we + // need is super optimal, but a little brittle (unfortunately we have no + // choice). ConfigurationBeanFactoryMetaData metaData = this.context.getBean( ConfigurationPropertiesBindingPostProcessorRegistrar.BINDER_BEAN_NAME + ".store", ConfigurationBeanFactoryMetaData.class); + ConfigurationPropertiesBeans beans = new ConfigurationPropertiesBeans(); + beans.setBeanMetaDataStore(metaData); + return beans; + } + + @Bean + @ConditionalOnMissingBean(search = SearchStrategy.CURRENT) + public ConfigurationPropertiesRebinder configurationPropertiesRebinder( + ConfigurationPropertiesBeans beans, + ConfigurationPropertiesBindingPostProcessor binder) { ConfigurationPropertiesRebinder rebinder = new ConfigurationPropertiesRebinder( - binder); - rebinder.setBeanMetaDataStore(metaData); + binder, beans); return rebinder; } + + @Override + public void afterSingletonsInstantiated() { + // After all beans are initialized send a pre-emptive EnvironmentChangeEvent + // so that anything that needs to rebind gets a chance now (especially for + // beans in the parent context) + this.context.publishEvent(new EnvironmentChangeEvent(Collections + . emptySet())); + } } @ConditionalOnClass(Endpoint.class) diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/ConfigurationPropertiesBootstrapConfiguration.java b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/ConfigurationPropertiesBootstrapConfiguration.java new file mode 100644 index 00000000..9087bc98 --- /dev/null +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/ConfigurationPropertiesBootstrapConfiguration.java @@ -0,0 +1,52 @@ +/* + * Copyright 2015 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 + * + * http://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.bootstrap.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.SearchStrategy; +import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetaData; +import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessorRegistrar; +import org.springframework.cloud.context.properties.ConfigurationPropertiesBeans; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Dave Syer + * + */ +@Configuration +public class ConfigurationPropertiesBootstrapConfiguration { + + @Bean + @ConditionalOnMissingBean(search = SearchStrategy.CURRENT) + public ConfigurationPropertiesBeans configurationPropertiesBeans( + ApplicationContext context) { + // Since this is a BeanPostProcessor we have to be super careful not to + // cause a cascade of bean instantiation. Knowing the *name* of the beans we + // need is super optimal, but a little brittle (unfortunately we have no + // choice). + ConfigurationBeanFactoryMetaData metaData = context.getBean( + ConfigurationPropertiesBindingPostProcessorRegistrar.BINDER_BEAN_NAME + + ".store", ConfigurationBeanFactoryMetaData.class); + ConfigurationPropertiesBeans rebinder = new ConfigurationPropertiesBeans(); + rebinder.setBeanMetaDataStore(metaData); + return rebinder; + } + + +} diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapConfiguration.java b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapConfiguration.java index 5db3d02a..ba74f23b 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapConfiguration.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapConfiguration.java @@ -53,7 +53,7 @@ import org.springframework.util.StringUtils; @Configuration @EnableConfigurationProperties(PropertySourceBootstrapProperties.class) public class PropertySourceBootstrapConfiguration implements - ApplicationContextInitializer { +ApplicationContextInitializer { private static final String BOOTSTRAP_PROPERTY_SOURCE_NAME = BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME; @@ -112,12 +112,13 @@ public class PropertySourceBootstrapConfiguration implements try { ResourceUtils.getURL(logConfig).openStream().close(); LogFile logFile = LogFile.get(environment); - system.initialize(new LoggingInitializationContext(environment), logConfig, logFile); + system.initialize(new LoggingInitializationContext(environment), + logConfig, logFile); } catch (Exception ex) { PropertySourceBootstrapConfiguration.logger - .warn("Logging config file location '" + logConfig - + "' cannot be opened and will be ignored"); + .warn("Logging config file location '" + logConfig + + "' cannot be opened and will be ignored"); } } } @@ -137,7 +138,7 @@ public class PropertySourceBootstrapConfiguration implements incoming.addFirst(composite); PropertySourceBootstrapProperties remoteProperties = new PropertySourceBootstrapProperties(); new RelaxedDataBinder(remoteProperties, "spring.cloud.config") - .bind(new PropertySourcesPropertyValues(incoming)); + .bind(new PropertySourcesPropertyValues(incoming)); if (!remoteProperties.isAllowOverride() || (!remoteProperties.isOverrideNone() && remoteProperties .isOverrideSystemProperties())) { diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/EncryptionBootstrapConfiguration.java b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/EncryptionBootstrapConfiguration.java index 5fbdf56b..43156933 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/EncryptionBootstrapConfiguration.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/EncryptionBootstrapConfiguration.java @@ -21,14 +21,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetaData; -import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor; -import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessorRegistrar; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.bootstrap.encrypt.KeyProperties.KeyStore; import org.springframework.cloud.context.encrypt.EncryptorFactory; -import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder; -import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.Conditional; @@ -96,18 +91,6 @@ public class EncryptionBootstrapConfiguration { } - @Bean - public ConfigurationPropertiesRebinder configurationPropertiesRebinder( - ApplicationContext context, ConfigurationPropertiesBindingPostProcessor binder) { - ConfigurationBeanFactoryMetaData metaData = context.getBean( - ConfigurationPropertiesBindingPostProcessorRegistrar.BINDER_BEAN_NAME - + ".store", ConfigurationBeanFactoryMetaData.class); - ConfigurationPropertiesRebinder rebinder = new ConfigurationPropertiesRebinder( - binder); - rebinder.setBeanMetaDataStore(metaData); - return rebinder; - } - @Bean public EnvironmentDecryptApplicationInitializer environmentDecryptApplicationListener() { if (this.encryptor == null) { diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesBeans.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesBeans.java new file mode 100644 index 00000000..04eb0f86 --- /dev/null +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesBeans.java @@ -0,0 +1,132 @@ +/* + * Copyright 2013-2014 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 + * + * http://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.properties; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetaData; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Component; + +/** + * Collects references to @ConfigurationProperties beans in the context and + * its parent. + * + * @author Dave Syer + * + */ +@Component +public class ConfigurationPropertiesBeans implements BeanPostProcessor, +ApplicationContextAware { + + private ConfigurationBeanFactoryMetaData metaData; + + private Map beans = new HashMap(); + + private ConfigurableListableBeanFactory beanFactory; + + private String refreshScope; + + private boolean refreshScopeInitialized; + + private ConfigurationPropertiesBeans parent; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + if (applicationContext.getAutowireCapableBeanFactory() instanceof ConfigurableListableBeanFactory) { + this.beanFactory = (ConfigurableListableBeanFactory) applicationContext + .getAutowireCapableBeanFactory(); + } + if (applicationContext.getParent() != null + && applicationContext.getParent().getAutowireCapableBeanFactory() instanceof ConfigurableListableBeanFactory) { + ConfigurableListableBeanFactory listable = (ConfigurableListableBeanFactory) applicationContext + .getParent().getAutowireCapableBeanFactory(); + String[] names = listable + .getBeanNamesForType(ConfigurationPropertiesBeans.class); + if (names.length == 1) { + this.parent = (ConfigurationPropertiesBeans) listable.getBean(names[0]); + this.beans.putAll(this.parent.beans); + } + } + } + + /** + * @param beans the bean meta data to set + */ + public void setBeanMetaDataStore(ConfigurationBeanFactoryMetaData beans) { + this.metaData = beans; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + if (isRefreshScoped(beanName)) { + return bean; + } + ConfigurationProperties annotation = AnnotationUtils.findAnnotation( + bean.getClass(), ConfigurationProperties.class); + if (annotation != null) { + this.beans.put(beanName, bean); + } + else if (this.metaData != null) { + annotation = this.metaData.findFactoryAnnotation(beanName, + ConfigurationProperties.class); + if (annotation != null) { + this.beans.put(beanName, bean); + } + } + return bean; + } + + private boolean isRefreshScoped(String beanName) { + if (this.refreshScope == null && !this.refreshScopeInitialized) { + this.refreshScopeInitialized = true; + for (String scope : this.beanFactory.getRegisteredScopeNames()) { + if (this.beanFactory.getRegisteredScope(scope) instanceof org.springframework.cloud.context.scope.refresh.RefreshScope) { + this.refreshScope = scope; + break; + } + } + } + if (beanName == null || this.refreshScope == null) { + return false; + } + return this.beanFactory.containsBeanDefinition(beanName) + && this.refreshScope.equals(this.beanFactory.getBeanDefinition(beanName) + .getScope()); + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) + throws BeansException { + return bean; + } + + public Set getBeanNames() { + return new HashSet(this.beans.keySet()); + } + +} 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 8d6dab67..9500675f 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 @@ -15,15 +15,10 @@ */ package org.springframework.cloud.context.properties; -import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.Set; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetaData; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor; import org.springframework.cloud.context.config.annotation.RefreshScope; @@ -31,7 +26,6 @@ import org.springframework.cloud.context.environment.EnvironmentChangeEvent; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.env.Environment; import org.springframework.jmx.export.annotation.ManagedAttribute; import org.springframework.jmx.export.annotation.ManagedOperation; @@ -52,116 +46,53 @@ import org.springframework.stereotype.Component; */ @Component @ManagedResource -public class ConfigurationPropertiesRebinder implements BeanPostProcessor, -ApplicationListener, ApplicationContextAware { +public class ConfigurationPropertiesRebinder implements ApplicationContextAware, +ApplicationListener { - private ConfigurationBeanFactoryMetaData metaData; + private ConfigurationPropertiesBeans beans; private ConfigurationPropertiesBindingPostProcessor binder; - public ConfigurationPropertiesRebinder( - ConfigurationPropertiesBindingPostProcessor binder) { - this.binder = binder; - } - - private Map beans = new HashMap(); - private ApplicationContext applicationContext; - private ConfigurableListableBeanFactory beanFactory; - - private String refreshScope; - - private boolean refreshScopeInitialized; + public ConfigurationPropertiesRebinder( + ConfigurationPropertiesBindingPostProcessor binder, + ConfigurationPropertiesBeans beans) { + this.binder = binder; + this.beans = beans; + } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; - if (applicationContext.getAutowireCapableBeanFactory() instanceof ConfigurableListableBeanFactory) { - this.beanFactory = (ConfigurableListableBeanFactory) applicationContext - .getAutowireCapableBeanFactory(); - } - } - - /** - * @param beans the bean meta data to set - */ - public void setBeanMetaDataStore(ConfigurationBeanFactoryMetaData beans) { - this.metaData = beans; - } - - @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) - throws BeansException { - if (isRefreshScoped(beanName)) { - return bean; - } - ConfigurationProperties annotation = AnnotationUtils.findAnnotation( - bean.getClass(), ConfigurationProperties.class); - if (annotation != null) { - this.beans.put(beanName, bean); - } - else if (this.metaData != null) { - annotation = this.metaData.findFactoryAnnotation(beanName, - ConfigurationProperties.class); - if (annotation != null) { - this.beans.put(beanName, bean); - } - } - return bean; - } - - private boolean isRefreshScoped(String beanName) { - if (this.refreshScope == null && !this.refreshScopeInitialized) { - this.refreshScopeInitialized = true; - for (String scope : this.beanFactory.getRegisteredScopeNames()) { - if (this.beanFactory.getRegisteredScope(scope) instanceof org.springframework.cloud.context.scope.refresh.RefreshScope) { - this.refreshScope = scope; - break; - } - } - } - if (beanName == null || this.refreshScope == null) { - return false; - } - return this.beanFactory.containsBeanDefinition(beanName) - && this.refreshScope - .equals(this.beanFactory.getBeanDefinition(beanName).getScope()); - } - - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) - throws BeansException { - return bean; } @ManagedOperation public void rebind() { - for (String name : this.beans.keySet()) { + for (String name : this.beans.getBeanNames()) { rebind(name); } } @ManagedOperation - public void rebind(String name) { - if (!this.applicationContext.containsBean(name)) { - return; + public boolean rebind(String name) { + if (!this.beans.getBeanNames().contains(name)) { + return false; } - if (isRefreshScoped(name)) { - return; - } - Object bean = this.applicationContext.getBean(name); - this.binder.postProcessBeforeInitialization(bean, name); if (this.applicationContext != null) { - this.applicationContext.getAutowireCapableBeanFactory().initializeBean( - bean, name); + Object bean = this.applicationContext.getBean(name); + this.binder.postProcessBeforeInitialization(bean, name); + this.applicationContext.getAutowireCapableBeanFactory().initializeBean(bean, + name); + return true; } + return false; } @ManagedAttribute public Set getBeanNames() { - return new HashSet(this.beans.keySet()); + return new HashSet(this.beans.getBeanNames()); } @Override diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/GenericScope.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/GenericScope.java index ac940e4a..2b02f11e 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/GenericScope.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/GenericScope.java @@ -134,11 +134,19 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor, Disposable } } - protected void destroy(String name) { + /** + * Destroy the named bean (i.e. flush it from the cache by default). + * + * @param name the bean name to flush + * @return true if the bean was already cached, false otherwise + */ + protected boolean destroy(String name) { BeanLifecycleWrapper wrapper = this.cache.remove(name); if (wrapper != null) { wrapper.destroy(); + return true; } + return false; } @Override diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/refresh/RefreshScope.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/refresh/RefreshScope.java index 953e5103..30c155ab 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/refresh/RefreshScope.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/refresh/RefreshScope.java @@ -1,11 +1,11 @@ /* * Copyright 2002-2009 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 - * + * * http://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. @@ -24,37 +24,44 @@ import org.springframework.jmx.export.annotation.ManagedResource; /** *

- * A Scope implementation that allows for beans to be refreshed dynamically at runtime (see {@link #refresh(String)} and - * {@link #refreshAll()}). If a bean is refreshed then the next time the bean is accessed (i.e. a method is executed) a - * new instance is created. All lifecycle methods are applied to the bean instances, so any destruction callbacks that - * were registered in the bean factory are called when it is refreshed, and then the initialization callbacks are - * invoked as normal when the new instance is created. A new bean instance is created from the original bean definition, - * so any externalized content (property placeholders or expressions in string literals) is re-evaluated when it is - * created. + * A Scope implementation that allows for beans to be refreshed dynamically at runtime + * (see {@link #refresh(String)} and {@link #refreshAll()}). If a bean is refreshed then + * the next time the bean is accessed (i.e. a method is executed) a new instance is + * created. All lifecycle methods are applied to the bean instances, so any destruction + * callbacks that were registered in the bean factory are called when it is refreshed, and + * then the initialization callbacks are invoked as normal when the new instance is + * created. A new bean instance is created from the original bean definition, so any + * externalized content (property placeholders or expressions in string literals) is + * re-evaluated when it is created. *

- * + * *

- * Note that all beans in this scope are only initialized when first accessed, so the scope forces lazy - * initialization semantics. The implementation involves creating a proxy for every bean in the scope, so there is a - * flag {@link #setProxyTargetClass(boolean) proxyTargetClass} which controls the proxy creation, defaulting to JDK - * dynamic proxies and therefore only exposing the interfaces implemented by a bean. If callers need access to other - * methods then the flag needs to be set (and CGLib present on the classpath). Because this scope automatically proxies - * all its beans, there is no need to add <aop:auto-proxy/> to any bean definitions. + * Note that all beans in this scope are only initialized when first accessed, so + * the scope forces lazy initialization semantics. The implementation involves creating a + * proxy for every bean in the scope, so there is a flag + * {@link #setProxyTargetClass(boolean) proxyTargetClass} which controls the proxy + * creation, defaulting to JDK dynamic proxies and therefore only exposing the interfaces + * implemented by a bean. If callers need access to other methods then the flag needs to + * be set (and CGLib present on the classpath). Because this scope automatically proxies + * all its beans, there is no need to add <aop:auto-proxy/> to any bean + * definitions. *

- * + * *

- * The scoped proxy approach adopted here has a side benefit that bean instances are automatically {@link Serializable}, - * and can be sent across the wire as long as the receiver has an identical application context on the other side. To - * ensure that the two contexts agree that they are identical they have to have the same serialization id. One will be - * generated automatically by default from the bean names, so two contexts with the same bean names are by default able - * to exchange beans by name. If you need to override the default id then provide an explicit {@link #setId(String) id} - * when the Scope is declared. + * The scoped proxy approach adopted here has a side benefit that bean instances are + * automatically {@link Serializable}, and can be sent across the wire as long as the + * receiver has an identical application context on the other side. To ensure that the two + * contexts agree that they are identical they have to have the same serialization id. One + * will be generated automatically by default from the bean names, so two contexts with + * the same bean names are by default able to exchange beans by name. If you need to + * override the default id then provide an explicit {@link #setId(String) id} when the + * Scope is declared. *

- * + * * @author Dave Syer - * + * * @since 3.1 - * + * */ @ManagedResource public class RefreshScope extends GenericScope implements ApplicationContextAware { @@ -70,20 +77,24 @@ public class RefreshScope extends GenericScope implements ApplicationContextAwar } @ManagedOperation(description = "Dispose of the current instance of bean name provided and force a refresh on next method execution.") - public void refresh(String name) { + public boolean refresh(String name) { if (!name.startsWith(SCOPED_TARGET_PREFIX)) { - // User wants to refresh the bean with this name but that isn't the one in the cache... + // User wants to refresh the bean with this name but that isn't the one in the + // cache... name = SCOPED_TARGET_PREFIX + name; } // Ensure lifecycle is finished if bean was disposable - super.destroy(name); - context.publishEvent(new RefreshScopeRefreshedEvent(name)); + if (super.destroy(name)) { + this.context.publishEvent(new RefreshScopeRefreshedEvent(name)); + return true; + } + return false; } @ManagedOperation(description = "Dispose of the current instance of all beans in this scope and force a refresh on next method execution.") public void refreshAll() { super.destroy(); - context.publishEvent(new RefreshScopeRefreshedEvent()); + this.context.publishEvent(new RefreshScopeRefreshedEvent()); } @Override 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 f49c0d6e..5be1f276 100644 --- a/spring-cloud-context/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-context/src/main/resources/META-INF/spring.factories @@ -10,6 +10,7 @@ org.springframework.cloud.context.restart.RestartListener # Bootstrap components org.springframework.cloud.bootstrap.BootstrapConfiguration=\ +org.springframework.cloud.bootstrap.config.ConfigurationPropertiesBootstrapConfiguration,\ org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration,\ org.springframework.cloud.bootstrap.encrypt.EncryptionBootstrapConfiguration,\ org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinderIntegrationTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinderIntegrationTests.java index d76cdbff..926d4256 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinderIntegrationTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinderIntegrationTests.java @@ -36,7 +36,7 @@ import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -@SpringApplicationConfiguration(classes=TestConfiguration.class) +@SpringApplicationConfiguration(classes = TestConfiguration.class) @RunWith(SpringJUnit4ClassRunner.class) public class ConfigurationPropertiesRebinderIntegrationTests { @@ -53,42 +53,44 @@ public class ConfigurationPropertiesRebinderIntegrationTests { @DirtiesContext public void testSimpleProperties() throws Exception { assertEquals("Hello scope!", this.properties.getMessage()); + assertEquals(2, this.properties.getCount()); // Change the dynamic property source... EnvironmentTestUtils.addEnvironment(this.environment, "message:Foo"); // ...but don't refresh, so the bean stays the same: assertEquals("Hello scope!", this.properties.getMessage()); - assertEquals(1, this.properties.getCount()); + assertEquals(2, this.properties.getCount()); } @Test @DirtiesContext public void testRefresh() throws Exception { - assertEquals(1, this.properties.getCount()); + assertEquals(2, this.properties.getCount()); assertEquals("Hello scope!", this.properties.getMessage()); // Change the dynamic property source... EnvironmentTestUtils.addEnvironment(this.environment, "message:Foo"); // ...and then refresh, so the bean is re-initialized: this.rebinder.rebind(); assertEquals("Foo", this.properties.getMessage()); - assertEquals(2, this.properties.getCount()); + assertEquals(3, this.properties.getCount()); } @Test @DirtiesContext public void testRefreshByName() throws Exception { - assertEquals(1, this.properties.getCount()); + assertEquals(2, this.properties.getCount()); assertEquals("Hello scope!", this.properties.getMessage()); // Change the dynamic property source... EnvironmentTestUtils.addEnvironment(this.environment, "message:Foo"); // ...and then refresh, so the bean is re-initialized: this.rebinder.rebind("properties"); assertEquals("Foo", this.properties.getMessage()); - assertEquals(2, this.properties.getCount()); + assertEquals(3, this.properties.getCount()); } @Configuration @EnableConfigurationProperties - @Import({RefreshConfiguration.RebinderConfiguration.class, PropertyPlaceholderAutoConfiguration.class}) + @Import({ RefreshConfiguration.RebinderConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) protected static class TestConfiguration { @Bean @@ -101,7 +103,8 @@ public class ConfigurationPropertiesRebinderIntegrationTests { // Hack out a protected inner class for testing protected static class RefreshConfiguration extends RefreshAutoConfiguration { @Configuration - protected static class RebinderConfiguration extends ConfigurationPropertiesRebinderConfiguration { + protected static class RebinderConfiguration extends + ConfigurationPropertiesRebinderConfiguration { } } @@ -111,24 +114,30 @@ public class ConfigurationPropertiesRebinderIntegrationTests { private String message; private int delay; private int count = 0; + public int getCount() { return this.count; } + 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; } + @PostConstruct public void init() { - this.count ++; + this.count++; } }