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:
Dave Syer
2018-03-02 14:03:54 +00:00
parent d14e8ffa1a
commit dcc822626b
5 changed files with 164 additions and 20 deletions

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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.")

View File

@@ -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
}
]}

View File

@@ -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;
}
}
}