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
This commit is contained in:
@@ -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<String> refreshables = new HashSet<>(
|
||||
Arrays.asList("com.zaxxer.hikari.HikariDataSource"));
|
||||
|
||||
private Environment environment;
|
||||
|
||||
public Set<String> getRefreshable() {
|
||||
return this.refreshables;
|
||||
}
|
||||
|
||||
public void setRefreshable(Set<String> refreshables) {
|
||||
if (this.refreshables != refreshables) {
|
||||
this.refreshables.clear();
|
||||
}
|
||||
this.refreshables.addAll(refreshables);
|
||||
}
|
||||
|
||||
public void setExtraRefreshable(Set<String> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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<java.lang.String>",
|
||||
"description": "Additional class names for beans to post process into refresh scope.",
|
||||
"defaultValue": true
|
||||
}
|
||||
]}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user