From dcc822626bf536dbe015a01585f10ff95e2a3735 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 2 Mar 2018 14:03:54 +0000 Subject: [PATCH] Add a configurable of beans to postprocess into refresh scope Allows you to apply @RefreshScope to beans that you do not control or define. The list is configurable as spring.cloud.refresh.refreshable and by default has only HikariDataSource. Fixes gh-318 --- .../RefreshAutoConfiguration.java | 97 ++++++++++++++++++- .../cloud/context/scope/GenericScope.java | 7 +- .../context/scope/refresh/RefreshScope.java | 19 ++-- ...itional-spring-configuration-metadata.json | 6 ++ .../RefreshAutoConfigurationTests.java | 55 +++++++++-- 5 files changed, 164 insertions(+), 20 deletions(-) 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 f9de157d..1d4d89ab 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,8 +17,15 @@ package org.springframework.cloud.autoconfigure; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.aop.scope.ScopedProxyUtils; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -28,6 +35,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.web.servlet.WebMvcAutoConfiguration; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.cloud.context.refresh.ContextRefresher; import org.springframework.cloud.context.scope.refresh.RefreshScope; import org.springframework.cloud.endpoint.event.RefreshEventListener; @@ -35,6 +44,8 @@ import org.springframework.cloud.logging.LoggingRebinder; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; import org.springframework.stereotype.Component; /** @@ -47,7 +58,7 @@ import org.springframework.stereotype.Component; @Configuration @ConditionalOnClass(RefreshScope.class) @ConditionalOnProperty(name = "spring.cloud.refresh.enabled", matchIfMissing = true) -//TODO: support reactive +// TODO: support reactive @AutoConfigureAfter(WebMvcAutoConfiguration.class) public class RefreshAutoConfiguration { @@ -71,6 +82,87 @@ public class RefreshAutoConfiguration { } } + @Component + protected static class RefreshScopeBeanDefinitionEnhancer + implements BeanDefinitionRegistryPostProcessor { + + /** + * Class names for beans to post process into refresh scope. Useful when you don't + * control the bean definition (e.g. it came from auto-configuration). + */ + private Set refreshables = new HashSet<>( + Arrays.asList("com.zaxxer.hikari.HikariDataSource")); + + private Environment environment; + + public Set getRefreshable() { + return this.refreshables; + } + + public void setRefreshable(Set refreshables) { + if (this.refreshables != refreshables) { + this.refreshables.clear(); + } + this.refreshables.addAll(refreshables); + } + + public void setExtraRefreshable(Set refreshables) { + this.refreshables.addAll(refreshables); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) + throws BeansException { + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) + throws BeansException { + for (String name : registry.getBeanDefinitionNames()) { + BeanDefinition definition = registry.getBeanDefinition(name); + if (isApplicable(registry, name, definition)) { + BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, + name); + BeanDefinitionHolder proxy = ScopedProxyUtils + .createScopedProxy(holder, registry, true); + definition.setScope("refresh"); + registry.registerBeanDefinition(proxy.getBeanName(), + proxy.getBeanDefinition()); + } + } + } + + private boolean isApplicable(BeanDefinitionRegistry registry, String name, + BeanDefinition definition) { + String scope = definition.getScope(); + if ("refresh".equals(scope)) { + // Already refresh scoped + return false; + } + String type = definition.getBeanClassName(); + if (registry instanceof BeanFactory) { + Class cls = ((BeanFactory) registry).getType(name); + if (cls != null) { + type = cls.getName(); + } + } + if (type != null) { + if (this.environment == null && registry instanceof BeanFactory) { + this.environment = ((BeanFactory) registry) + .getBean(Environment.class); + } + if (this.environment == null) { + this.environment = new StandardEnvironment(); + } + Binder.get(environment).bind("spring.cloud.refresh", + Bindable.ofInstance(this)); + return this.refreshables.contains(type); + } + return false; + } + + } + @Bean @ConditionalOnMissingBean public static LoggingRebinder loggingRebinder() { @@ -85,8 +177,7 @@ public class RefreshAutoConfiguration { } @Bean - public RefreshEventListener refreshEventListener( - ContextRefresher contextRefresher) { + public RefreshEventListener refreshEventListener(ContextRefresher contextRefresher) { return new RefreshEventListener(contextRefresher); } 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 d0fa13bc..fcad5b9d 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 @@ -32,6 +32,7 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.aop.framework.Advised; import org.springframework.aop.scope.ScopedObject; import org.springframework.aop.scope.ScopedProxyFactoryBean; @@ -256,6 +257,9 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor, if (getName().equals(root.getDecoratedDefinition().getBeanDefinition() .getScope())) { root.setBeanClass(LockedScopedProxyFactoryBean.class); + // surprising that a scoped proxy bean definition is not already + // marked as synthetic? + root.setSynthetic(true); } } } @@ -456,8 +460,7 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor, @Override public Object invoke(MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod(); - if (AopUtils.isEqualsMethod(method) - || AopUtils.isToStringMethod(method) + if (AopUtils.isEqualsMethod(method) || AopUtils.isToStringMethod(method) || AopUtils.isHashCodeMethod(method) || isScopedObjectGetTargetObject(method)) { return invocation.proceed(); 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 c9aaaf01..f85fa5a1 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 @@ -114,20 +114,21 @@ public class RefreshScope extends GenericScope public void start(ContextRefreshedEvent event) { if (event.getApplicationContext() == this.context && this.eager && this.registry != null) { - eagerlyInitialize(); + eagerlyInitialize(); } } private void eagerlyInitialize() { for (String name : this.context.getBeanDefinitionNames()) { - BeanDefinition definition = this.registry.getBeanDefinition(name); - if (this.getName().equals(definition.getScope()) && !definition.isLazyInit()) { - Object bean = this.context.getBean(name); - if (bean != null) { - bean.getClass(); - } - } - } + BeanDefinition definition = this.registry.getBeanDefinition(name); + if (this.getName().equals(definition.getScope()) + && !definition.isLazyInit()) { + Object bean = this.context.getBean(name); + if (bean != null) { + bean.getClass(); + } + } + } } @ManagedOperation(description = "Dispose of the current instance of bean name provided and force a refresh on next method execution.") diff --git a/spring-cloud-context/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-context/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 300dd37e..b8b03f05 100644 --- a/spring-cloud-context/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-context/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -34,6 +34,12 @@ "type": "java.lang.Boolean", "description": "Enable the /resume endpoint (to send Lifecycle.start()).", "defaultValue": true + }, + { + "name": "spring.cloud.refresh.extra-refreshable", + "type": "java.util.Set", + "description": "Additional class names for beans to post process into refresh scope.", + "defaultValue": true } ]} diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationTests.java index 49b1a210..7512f2ef 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationTests.java @@ -5,14 +5,17 @@ import org.junit.Test; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.rule.OutputCapture; +import org.springframework.cloud.context.refresh.ContextRefresher; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; - /** * @author Dave Syer */ @@ -24,7 +27,7 @@ public class RefreshAutoConfigurationTests { @Test public void noWarnings() { try (ConfigurableApplicationContext context = getApplicationContext( - WebApplicationType.NONE, Config.class)) { + WebApplicationType.NONE, Config.class)) { assertThat(context.containsBean("refreshScope")).isTrue(); assertThat(output.toString()).doesNotContain("WARN"); } @@ -33,19 +36,59 @@ public class RefreshAutoConfigurationTests { @Test public void disabled() { try (ConfigurableApplicationContext context = getApplicationContext( - WebApplicationType.SERVLET, Config.class, "spring.cloud.refresh.enabled:false")) { + WebApplicationType.SERVLET, Config.class, + "spring.cloud.refresh.enabled:false")) { assertThat(context.containsBean("refreshScope")).isFalse(); } } + @Test + public void refreshables() { + try (ConfigurableApplicationContext context = getApplicationContext( + WebApplicationType.NONE, Config.class, "config.foo=bar", + "spring.cloud.refresh.refreshable:" + ConfigProps.class.getName())) { + context.getBean(ContextRefresher.class).refresh(); + } + } + + @Test + public void extraRefreshables() { + try (ConfigurableApplicationContext context = getApplicationContext( + WebApplicationType.NONE, Config.class, "config.foo=bar", + "spring.cloud.refresh.extra-refreshable:" + + ConfigProps.class.getName())) { + context.getBean(ContextRefresher.class).refresh(); + } + } + private static ConfigurableApplicationContext getApplicationContext( - WebApplicationType type, Class configuration, String... properties) { - return new SpringApplicationBuilder(configuration).web(type).properties(properties).properties("server.port=0").run(); + WebApplicationType type, Class configuration, String... properties) { + return new SpringApplicationBuilder(configuration).web(type) + .properties(properties).properties("server.port=0").run(); } @Configuration - @EnableAutoConfiguration + @EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class) + @EnableConfigurationProperties(ConfigProps.class) static class Config { } + + @ConfigurationProperties("config") + static class ConfigProps { + private String foo; + private boolean sealed; + + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + if (this.sealed) { + throw new IllegalStateException("Cannot set sealed property"); + } + this.foo = foo; + this.sealed = true; + } + } }