Make it illegal to add @RefreshScope to @Configuration

There are still some problematic areas (see SPR-12486) where users
might be surprised, but at least there now is an error message for the
specific case of @RefreshScope on @Configuration.

Fixes gh-43
This commit is contained in:
Dave Syer
2014-12-01 09:47:58 +00:00
parent 0af301e7c2
commit 0ae34b06fe
6 changed files with 272 additions and 176 deletions

View File

@@ -35,16 +35,19 @@ import org.springframework.beans.factory.config.Scope;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.cloud.context.config.BeanLifecycleDecorator;
import org.springframework.cloud.context.config.BeanLifecycleDecorator.Context;
import org.springframework.cloud.context.config.StandardBeanLifecycleDecorator;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.expression.BeanFactoryAccessor;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParseException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.cloud.context.config.BeanLifecycleDecorator;
import org.springframework.cloud.context.config.BeanLifecycleDecorator.Context;
import org.springframework.cloud.context.config.StandardBeanLifecycleDecorator;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
@@ -58,8 +61,7 @@ import org.springframework.util.StringValueResolver;
* @since 3.1
*
*/
public class GenericScope implements Scope, BeanFactoryPostProcessor,
DisposableBean {
public class GenericScope implements Scope, BeanFactoryPostProcessor, DisposableBean {
private static final Log logger = LogFactory.getLog(GenericScope.class);
@@ -81,12 +83,10 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor,
private BeanLifecycleDecorator<?> lifecycle;
/**
* Manual override for the serialization id that will be used to identify
* the bean factory. The default is a unique key based on the bean names in
* the bean factory.
* Manual override for the serialization id that will be used to identify the bean
* factory. The default is a unique key based on the bean names in the bean factory.
*
* @param id
* the id to set
* @param id the id to set
*/
public void setId(String id) {
this.id = id;
@@ -95,36 +95,32 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor,
/**
* The name of this scope. Default "refresh".
*
* @param name
* the name value to set
* @param name the name value to set
*/
public void setName(String name) {
this.name = name;
}
/**
* Flag to indicate that proxies should be created for the concrete type,
* not just the interfaces, of the scoped beans.
* Flag to indicate that proxies should be created for the concrete type, not just the
* interfaces, of the scoped beans.
*
* @param proxyTargetClass
* the flag value to set
* @param proxyTargetClass the flag value to set
*/
public void setProxyTargetClass(boolean proxyTargetClass) {
this.proxyTargetClass = proxyTargetClass;
}
/**
* Flag to indicate that all scoped beans should automatically be proxied.
* If true then scoped beans can be injected as dependencies of another
* component and the concrete target will only be instantiated when it is
* used. Proxying is a huge advantage if the context storage for the scope
* cache is not available at configuration time (e.g. for thread-based, or
* other transient scopes). If this flag is false you can expect maybe to
* have to add extra meta-data to the bean definitions individually (e.g.
* &lt;aop:scoped-proxy/&gt; for an XML configuration).
* Flag to indicate that all scoped beans should automatically be proxied. If true
* then scoped beans can be injected as dependencies of another component and the
* concrete target will only be instantiated when it is used. Proxying is a huge
* advantage if the context storage for the scope cache is not available at
* configuration time (e.g. for thread-based, or other transient scopes). If this flag
* is false you can expect maybe to have to add extra meta-data to the bean
* definitions individually (e.g. &lt;aop:scoped-proxy/&gt; for an XML configuration).
*
* @param autoProxy
* the flag value to set, default is true
* @param autoProxy the flag value to set, default is true
*/
public void setAutoProxy(boolean autoProxy) {
this.autoProxy = autoProxy;
@@ -133,8 +129,7 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor,
/**
* The cache implementation to use for bean instances in this scope.
*
* @param cache
* the cache to use
* @param cache the cache to use
*/
public void setScopeCache(ScopeCache cache) {
this.cache = new BeanLifecycleWrapperCache(cache);
@@ -143,8 +138,7 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor,
/**
* Helper to manage the creation and destruction of beans.
*
* @param lifecycle
* the bean lifecycle to set
* @param lifecycle the bean lifecycle to set
*/
public void setBeanLifecycleManager(BeanLifecycleDecorator<?> lifecycle) {
this.lifecycle = lifecycle;
@@ -156,7 +150,8 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor,
for (BeanLifecycleWrapper wrapper : wrappers) {
try {
wrapper.destroy();
} catch (RuntimeException e) {
}
catch (RuntimeException e) {
errors.add(e);
}
}
@@ -176,8 +171,8 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor,
if (lifecycle == null) {
lifecycle = new StandardBeanLifecycleDecorator(proxyTargetClass);
}
BeanLifecycleWrapper value = cache.put(name, new BeanLifecycleWrapper(
name, objectFactory, lifecycle));
BeanLifecycleWrapper value = cache.put(name, new BeanLifecycleWrapper(name,
objectFactory, lifecycle));
return value.getBean();
}
@@ -214,18 +209,19 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor,
ExpressionParser parser = new SpelExpressionParser();
try {
return parser.parseExpression(input);
} catch (ParseException e) {
throw new IllegalArgumentException("Cannot parse expression: "
+ input, e);
}
catch (ParseException e) {
throw new IllegalArgumentException("Cannot parse expression: " + input, e);
}
} else {
}
else {
return null;
}
}
public void postProcessBeanFactory(
ConfigurableListableBeanFactory beanFactory) throws BeansException {
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
beanFactory.registerScope(name, this);
setSerializationId(beanFactory);
@@ -249,26 +245,23 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor,
// Replace this or any of its inner beans with scoped proxy if it
// has this scope
boolean scoped = name.equals(definition.getScope());
Scopifier scopifier = new Scopifier(registry, name,
proxyTargetClass, scoped);
Scopifier scopifier = new Scopifier(registry, name, proxyTargetClass, scoped);
scopifier.visitBeanDefinition(definition);
if (scoped) {
createScopedProxy(beanName, definition, registry,
proxyTargetClass);
createScopedProxy(beanName, definition, registry, proxyTargetClass);
}
}
}
/**
* If the bean factory is a DefaultListableBeanFactory then it can serialize
* scoped beans and deserialize them in another context (even in another
* JVM), as long as the ids of the bean factories match. This method sets up
* the serialization id to be either the id provided to the scope instance,
* or if that is null, a hash of all the bean names.
* If the bean factory is a DefaultListableBeanFactory then it can serialize scoped
* beans and deserialize them in another context (even in another JVM), as long as the
* ids of the bean factories match. This method sets up the serialization id to be
* either the id provided to the scope instance, or if that is null, a hash of all the
* bean names.
*
* @param beanFactory
* the bean factory to configure
* @param beanFactory the bean factory to configure
*/
private void setSerializationId(ConfigurableListableBeanFactory beanFactory) {
@@ -276,8 +269,8 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor,
String id = this.id;
if (id == null) {
String names = Arrays.asList(
beanFactory.getBeanDefinitionNames()).toString();
String names = Arrays.asList(beanFactory.getBeanDefinitionNames())
.toString();
logger.debug("Generating bean factory id from names: " + names);
id = UUID.nameUUIDFromBytes(names.getBytes()).toString();
}
@@ -285,7 +278,8 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor,
logger.info("BeanFactory id=" + id);
((DefaultListableBeanFactory) beanFactory).setSerializationId(id);
} else {
}
else {
logger.warn("BeanFactory was not a DefaultListableBeanFactory, so RefreshScope beans "
+ "cannot be serialized reliably and passed to a remote JVM.");
}
@@ -308,14 +302,13 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor,
BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(
new BeanDefinitionHolder(definition, beanName), registry,
proxyTargetClass);
registry.registerBeanDefinition(beanName,
proxyHolder.getBeanDefinition());
registry.registerBeanDefinition(beanName, proxyHolder.getBeanDefinition());
return proxyHolder;
}
/**
* Helper class to scan a bean definition hierarchy and force the use of
* auto-proxy for scoped beans.
* Helper class to scan a bean definition hierarchy and force the use of auto-proxy
* for scoped beans.
*
* @author Dave Syer
*
@@ -343,6 +336,27 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor,
this.scoped = scoped;
}
@Override
protected void visitBeanClassName(BeanDefinition beanDefinition) {
String className = beanDefinition.getBeanClassName();
if (className!=null) {
Class<?> type = ClassUtils.resolveClassName(className, null);
Assert.state(
!beanDefinition.getScope().equals(scope)
|| AnnotationUtils.findAnnotation(type,
Configuration.class) == null,
"Scoped proxies not allowed on @Configuration (for '" + scope
+ "' scope) on bean of type " + type);
org.springframework.context.annotation.Scope beanScope = AnnotationUtils
.findAnnotation(type,
org.springframework.context.annotation.Scope.class);
if (beanScope != null && !scoped && beanScope.value().equals(scope)) {
beanDefinition.setScope(scope);
}
}
super.visitBeanClassName(beanDefinition);
}
@Override
protected Object resolveValue(Object value) {
@@ -350,9 +364,10 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor,
String beanName = null;
if (value instanceof BeanDefinition) {
definition = (BeanDefinition) value;
beanName = BeanDefinitionReaderUtils.generateBeanName(
definition, registry);
} else if (value instanceof BeanDefinitionHolder) {
beanName = BeanDefinitionReaderUtils.generateBeanName(definition,
registry);
}
else if (value instanceof BeanDefinitionHolder) {
BeanDefinitionHolder holder = (BeanDefinitionHolder) value;
definition = holder.getBeanDefinition();
beanName = holder.getBeanName();
@@ -409,9 +424,9 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor,
}
/**
* Wrapper for a bean instance and any destruction callback (DisposableBean
* etc.) that is registered for it. Also decorates the bean to optionally
* guard it from concurrent access (for instance).
* Wrapper for a bean instance and any destruction callback (DisposableBean etc.) that
* is registered for it. Also decorates the bean to optionally guard it from
* concurrent access (for instance).
*
* @author Dave Syer
*
@@ -430,8 +445,8 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor,
private final ObjectFactory<?> objectFactory;
@SuppressWarnings("rawtypes")
public BeanLifecycleWrapper(String name,
ObjectFactory<?> objectFactory, BeanLifecycleDecorator lifecycle) {
public BeanLifecycleWrapper(String name, ObjectFactory<?> objectFactory,
BeanLifecycleDecorator lifecycle) {
this.name = name;
this.objectFactory = objectFactory;
this.lifecycle = lifecycle;
@@ -444,18 +459,17 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor,
@SuppressWarnings("unchecked")
public Object getBean() {
if (bean == null) {
bean = lifecycle.decorateBean(objectFactory.getObject(),
context);
bean = lifecycle.decorateBean(objectFactory.getObject(), context);
}
return bean;
}
public void destroy() {
if (context==null) {
if (context == null) {
return;
}
Runnable callback = context.getCallback();
if (callback!=null) {
if (callback != null) {
callback.run();
}
}
@@ -480,7 +494,8 @@ public class GenericScope implements Scope, BeanFactoryPostProcessor,
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
}
else if (!name.equals(other.name))
return false;
return true;
}

View File

@@ -32,6 +32,7 @@ import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.context.scope.refresh.RefreshEndpointIntegrationTests.ClientApp;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -56,7 +57,7 @@ public class RefreshEndpointIntegrationTests {
@Value("${local.server.port}")
private int port;
@Test
public void webAccess() throws Exception {
TestRestTemplate template = new TestRestTemplate();
@@ -82,9 +83,22 @@ public class RefreshEndpointIntegrationTests {
@Configuration
@EnableAutoConfiguration
@RestController
@RefreshScope
protected static class ClientApp {
@Bean
@RefreshScope
public Controller controller() {
return new Controller();
}
public static void main(String[] args) {
SpringApplication.run(ClientApp.class, args);
}
}
@RestController
protected static class Controller {
@Value("${message:Hello World!}")
String message;
@@ -94,10 +108,7 @@ public class RefreshEndpointIntegrationTests {
return message;
}
public static void main(String[] args) {
SpringApplication.run(ClientApp.class, args);
}
}
}

View File

@@ -1,95 +0,0 @@
/*
* 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.scope.refresh;
import static org.junit.Assert.assertEquals;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.context.environment.EnvironmentManager;
import org.springframework.cloud.context.scope.refresh.RefreshScopeConfigurationIntegrationTests.Application;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author Dave Syer
*
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Application.class)
public class RefreshScopeConfigurationIntegrationTests {
@Autowired
private org.springframework.cloud.context.scope.refresh.RefreshScope scope;
@Autowired
private EnvironmentManager environmentManager;
@Autowired
private Application application;
@Autowired
private ConfigurableListableBeanFactory beanFactory;
@Test
public void scopeOnBeanDefinition() throws Exception {
assertEquals("refresh", beanFactory.getBeanDefinition("scopedTarget.application")
.getScope());
}
/**
* See gh-43
*/
@Test
@Ignore
public void beanAccess() throws Exception {
// Comment out this line and it works!
application.hello();
scope.refresh("application");
String message = application.hello();
assertEquals("Hello World", message);
}
@Configuration("application")
// @Component("application")
@RefreshScope
@Import({ PropertyPlaceholderAutoConfiguration.class, RefreshAutoConfiguration.class })
protected static class Application {
String message = "Hello World";
@RequestMapping("/")
public String hello() {
return message;
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
}

View File

@@ -0,0 +1,165 @@
/*
* 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.scope.refresh;
import static org.junit.Assert.assertEquals;
import org.junit.After;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.context.environment.EnvironmentManager;
import org.springframework.cloud.context.scope.refresh.RefreshScopeConfigurationTests.NestedApp.NestedController;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Dave Syer
*
*/
public class RefreshScopeConfigurationTests {
private ConfigurableApplicationContext context;
@Rule
public ExpectedException expected = ExpectedException.none();
@After
public void init() {
if (context!=null) {
context.close();
}
}
/**
* See gh-43
*/
@Test
public void configurationWithRefreshScope() throws Exception {
expected.expectMessage("Scoped proxies not allowed on @Configuration");
context = new AnnotationConfigApplicationContext(Application.class,
PropertyPlaceholderAutoConfiguration.class, RefreshAutoConfiguration.class);
}
@Test
public void refreshScopeOnBean() throws Exception {
context = new AnnotationConfigApplicationContext(ClientApp.class,
PropertyPlaceholderAutoConfiguration.class, RefreshAutoConfiguration.class);
Controller application = context.getBean(Controller.class);
application.hello();
refresh();
String message = application.hello();
assertEquals("Hello Dave!", message);
}
@Test
@Ignore("SPR-12486")
public void refreshScopeOnNested() throws Exception {
context = new AnnotationConfigApplicationContext(NestedApp.class,
PropertyPlaceholderAutoConfiguration.class, RefreshAutoConfiguration.class);
NestedController application = context.getBean(NestedController.class);
application.hello();
refresh();
String message = application.hello();
assertEquals("Hello Dave!", message);
}
private void refresh() {
EnvironmentManager environmentManager = context.getBean(EnvironmentManager.class);
environmentManager.setProperty("message", "Hello Dave!");
org.springframework.cloud.context.scope.refresh.RefreshScope scope = context.getBean(org.springframework.cloud.context.scope.refresh.RefreshScope.class);
scope .refreshAll();
}
@Configuration
@RefreshScope
protected static class Application {
String message = "Hello World";
@RequestMapping("/")
public String hello() {
return message;
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Configuration
protected static class ClientApp {
@Bean
@RefreshScope
public Controller controller() {
return new Controller();
}
public static void main(String[] args) {
SpringApplication.run(ClientApp.class, args);
}
}
@RestController
protected static class Controller {
@Value("${message:Hello World!}")
String message;
@RequestMapping("/")
public String hello() {
return message;
}
}
@Configuration
protected static class NestedApp {
@RestController
@RefreshScope
protected static class NestedController {
@Value("${message:Hello World!}")
String message;
@RequestMapping("/")
public String hello() {
return message;
}
}
public static void main(String[] args) {
SpringApplication.run(ClientApp.class, args);
}
}
}

View File

@@ -27,7 +27,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.context.environment.EnvironmentManager;
import org.springframework.cloud.context.scope.refresh.RefreshScopeNotConfigurationIntegrationTests.Application;
import org.springframework.cloud.context.scope.refresh.RefreshScopeWebIntegrationTests.Application;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -40,7 +40,7 @@ import org.springframework.web.bind.annotation.RestController;
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class RefreshScopeNotConfigurationIntegrationTests {
public class RefreshScopeWebIntegrationTests {
@Autowired
private org.springframework.cloud.context.scope.refresh.RefreshScope scope;

View File

@@ -2,4 +2,4 @@ message: Hello scope!
delay: 0
# debug: true
#logging.level.org.springframework.web: DEBUG
logging.level.org.springframework.context.annotation: DEBUG
#logging.level.org.springframework.context.annotation: DEBUG