From 4df29bdfe785495b72a8964ba3b817cd4f267cf4 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Mon, 20 Jul 2015 16:53:52 +0100 Subject: [PATCH] Suppress rebinding of configuration properties for refresh scope If a @ConfigurationProperties @Bean is in @RefreshScope then it doesn't need to be rebound on an environment change event (it will re-initialize on refresh). See https://github.com/spring-cloud/spring-cloud-config/issues/183 --- .../ConfigurationPropertiesRebinder.java | 32 +++++ ...sRebinderRefreshScopeIntegrationTests.java | 125 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 spring-cloud-context/src/test/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinderRefreshScopeIntegrationTests.java 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 c0799857..f6d0e201 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 @@ -22,6 +22,7 @@ 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; @@ -67,10 +68,20 @@ public class ConfigurationPropertiesRebinder implements BeanPostProcessor, private ApplicationContext applicationContext; + private ConfigurableListableBeanFactory beanFactory; + + private String refreshScope; + + private boolean refreshScopeInitialized; + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; + if (applicationContext.getAutowireCapableBeanFactory() instanceof ConfigurableListableBeanFactory) { + this.beanFactory = (ConfigurableListableBeanFactory) applicationContext + .getAutowireCapableBeanFactory(); + } } /** @@ -83,6 +94,9 @@ public class ConfigurationPropertiesRebinder implements BeanPostProcessor, @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) { @@ -98,6 +112,24 @@ public class ConfigurationPropertiesRebinder implements BeanPostProcessor, return bean; } + private boolean isRefreshScoped(String beanName) { + if (refreshScope == null && !refreshScopeInitialized) { + refreshScopeInitialized = true; + for (String scope : beanFactory.getRegisteredScopeNames()) { + if (beanFactory.getRegisteredScope(scope) instanceof org.springframework.cloud.context.scope.refresh.RefreshScope) { + this.refreshScope = scope; + break; + } + } + } + if (refreshScope == null) { + return false; + } + return beanFactory.containsBeanDefinition(beanName) + && refreshScope + .equals(beanFactory.getBeanDefinition(beanName).getScope()); + } + @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinderRefreshScopeIntegrationTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinderRefreshScopeIntegrationTests.java new file mode 100644 index 00000000..1ee6945c --- /dev/null +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinderRefreshScopeIntegrationTests.java @@ -0,0 +1,125 @@ +/* + * Copyright 2006-2007 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 static org.junit.Assert.assertEquals; + +import javax.annotation.PostConstruct; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinderRefreshScopeIntegrationTests.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@SpringApplicationConfiguration(classes=TestConfiguration.class) +@RunWith(SpringJUnit4ClassRunner.class) +public class ConfigurationPropertiesRebinderRefreshScopeIntegrationTests { + + @Autowired + private TestProperties properties; + + @Autowired + private ConfigurationPropertiesRebinder rebinder; + + @Autowired + private org.springframework.cloud.context.scope.refresh.RefreshScope refreshScope; + + @Autowired + private ConfigurableEnvironment environment; + + @Test + @DirtiesContext + public void testSimpleProperties() throws Exception { + assertEquals("Hello scope!", properties.getMessage()); + // Change the dynamic property source... + EnvironmentTestUtils.addEnvironment(environment, "message:Foo"); + // ...but don't refresh, so the bean stays the same: + assertEquals("Hello scope!", properties.getMessage()); + assertEquals(1, properties.getCount()); + } + + @Test + @DirtiesContext + public void testRefresh() throws Exception { + assertEquals(1, properties.getCount()); + assertEquals("Hello scope!", properties.getMessage()); + assertEquals(1, properties.getCount()); + // Change the dynamic property source... + EnvironmentTestUtils.addEnvironment(environment, "message:Foo"); + // ...rebind, but the bean is not re-initialized: + rebinder.rebind(); + assertEquals("Hello scope!", properties.getMessage()); + assertEquals(1, properties.getCount()); + // ...and then refresh, so the bean is re-initialized: + refreshScope.refreshAll(); + assertEquals("Foo", properties.getMessage()); + // It's a new instance so the initialization count is 1 + assertEquals(1, properties.getCount()); + } + + @Configuration + @EnableConfigurationProperties + @Import({RefreshAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class}) + protected static class TestConfiguration { + + @Bean + @RefreshScope + protected TestProperties properties() { + return new TestProperties(); + } + + } + + @ConfigurationProperties + protected static class TestProperties { + private String message; + private int delay; + private int count = 0; + public int getCount() { + return count; + } + public String getMessage() { + return message; + } + public void setMessage(String message) { + this.message = message; + } + public int getDelay() { + return delay; + } + public void setDelay(int delay) { + this.delay = delay; + } + @PostConstruct + public void init() { + this.count ++; + } + } + +}