Skip to content

Instantly share code, notes, and snippets.

@dsyer
Created December 30, 2010 15:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dsyer/759898 to your computer and use it in GitHub Desktop.
Save dsyer/759898 to your computer and use it in GitHub Desktop.
target
bin
.classpath
.project
.settings
.springBeans
.DS_Store
*.sw*

EnvironmentManager and EnvironmentManagerEndpoint

The EnvironmentManager adds a PropertySource with high priority to the Spring Environment (so properties set there will take precedence). If you have the Actuator and are in a webapp then the EnvironmentManagerEndpoint will expose "/env" with POST for updating the EnvironmentManager remotely. E.g. run the app locally:

@Configuration
@EnableAutoConfiguration
public class Application {
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

then interact with the Environment:

$ curl localhost:8080/env/message
Hello
$ curl localhost:8080/env -d message=Foo
$ curl localhost:8080/env/message
Foo

Doing this (or the equivalent operation over JMX) will trigger an event that leads to rebinding of @ConfigurationProperties (see below).

ConfigurationPropertiesRebinder

Spring beans that are @ConfigurationProperties can be rebound if the Environment changes. Example:

	@ConfigurationProperties
	protected static class ServiceProperties {
		private String message;
		public String getMessage() {
			return message;
		}
		public void setMessage(String message) {
			this.message = message;
		}
	}

then inject ServiceProperties into an application component using @Autowired and use it to govern the behaviour. When the Environment changes do this

    @Autowired
    private ConfigurationPropertiesRebinder rebinder;
    
    ...
    
		rebinder.rebind();

The ConfigurationPropertiesRebinder is also a @ManagedResource so you can ping the rebind() operation remotely.

RefreshScope

Spring beans in scope="refresh" are re-initialized on the next method call after a refresh. Example:

	@Configuration
	@EnableConfigurationProperties(TestProperties.class)
	@EnableAutoConfiguration
	protected static class TestConfiguration {
		
		@Autowired
		private TestProperties properties;
		
		@Bean
		@RefreshScope
		public ExampleService service() {
			ExampleService service = new ExampleService();
			service.setMessage(properties.getMessage());
			service.setDelay(properties.getDelay());
			return service;
		}
		
	}

then update the TestProperties (e.g. via JMX or something), and

	@Autowired
	private RefreshScope scope;

...

		// ...and then refresh, so the bean is re-initialized:
		scope.refresh("service");
message: Hello scope!
delay: 0
# debug: true
/*
* Copyright 2002-2011 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.platform.context.config;
/**
* A helper interface providing optional decoration of bean instances and their
* destruction callbacks. Users can supply custom implementations of this
* strategy if they want tighter control over method invocation on the bean or
* its destruction callback.
*
* @param <T>
* the type of auxiliary context object that can be passed between
* methods. Implementations can choose what type of data to supply as
* it is passed around unchanged by the caller.
*
* @author Dave Syer
*
*/
public interface BeanLifecycleDecorator<T> {
/**
* Optionally decorate and provide a new instance of a compatible bean for
* the caller to use instead of the input.
*
* @param bean
* the bean to optionally decorate
* @param context
* the context as created by
* {@link #decorateDestructionCallback(Runnable)}
* @return the replacement bean for the caller to use
*/
Object decorateBean(Object bean, Context<T> context);
/**
* Optionally decorate the destruction callback provided, and also return
* some context that can be used later by the
* {@link #decorateBean(Object, Context)} method.
*
* @param callback
* the destruction callback that will be used by the container
* @return a context wrapper
*/
Context<T> decorateDestructionCallback(Runnable callback);
static class Context<T> {
private final T auxiliary;
private final Runnable callback;
public Context(Runnable callback, T auxiliary) {
this.callback = callback;
this.auxiliary = auxiliary;
}
public Runnable getCallback() {
return callback;
}
public T getAuxiliary() {
return auxiliary;
}
}
}
/*
* 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.platform.context.properties;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetaData;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor;
import org.springframework.context.ApplicationListener;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.platform.context.environment.EnvironmentChangeEvent;
/**
* @author Dave Syer
*
*/
@ManagedResource
public class ConfigurationPropertiesRebinder implements BeanPostProcessor, ApplicationListener<EnvironmentChangeEvent> {
private ConfigurationBeanFactoryMetaData metaData;
private ConfigurationPropertiesBindingPostProcessor binder;
public ConfigurationPropertiesRebinder(
ConfigurationPropertiesBindingPostProcessor binder) {
this.binder = binder;
}
private Map<String, Object> beans = new HashMap<String, Object>();
/**
* @param beans the bean meta data to set
*/
public void setBeanMetaDataStore(ConfigurationBeanFactoryMetaData beans) {
this.metaData = beans;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
ConfigurationProperties annotation = AnnotationUtils.findAnnotation(
bean.getClass(), ConfigurationProperties.class);
if (annotation != null) {
beans.put(beanName, bean);
}
else if (metaData != null) {
annotation = this.metaData.findFactoryAnnotation(beanName,
ConfigurationProperties.class);
if (annotation != null) {
beans.put(beanName, bean);
}
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
@ManagedOperation
public void rebind() {
for (String name : beans.keySet()) {
rebind(name);
}
}
@ManagedOperation
public void rebind(String name) {
binder.postProcessBeforeInitialization(beans.get(name), name);
}
@ManagedAttribute
public Set<String> getBeanNames() {
return beans.keySet();
}
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
rebind();
}
}
/*
* 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.platform.context.properties;
import static org.junit.Assert.assertEquals;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.platform.context.autoconfigure.RefreshAutoConfiguration;
import org.springframework.platform.context.properties.ConfigurationPropertiesRebinderIntegrationTests.TestConfiguration;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@SpringApplicationConfiguration(classes=TestConfiguration.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class ConfigurationPropertiesRebinderIntegrationTests {
@Autowired
private TestProperties properties;
@Autowired
private ConfigurationPropertiesRebinder rebinder;
@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());
}
@Test
@DirtiesContext
public void testRefresh() throws Exception {
assertEquals("Hello scope!", properties.getMessage());
// Change the dynamic property source...
EnvironmentTestUtils.addEnvironment(environment, "message:Foo");
// ...and then refresh, so the bean is re-initialized:
rebinder.rebind();
assertEquals("Foo", properties.getMessage());
}
@Configuration
@EnableConfigurationProperties
@Import({RefreshAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class})
protected static class TestConfiguration {
@Bean
protected TestProperties properties() {
return new TestProperties();
}
}
@ConfigurationProperties
protected static class TestProperties {
private String message;
private int delay;
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;
}
}
}
/*
* 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.platform.context.environment;
import java.util.Set;
import org.springframework.context.ApplicationEvent;
/**
* @author Dave Syer
*
*/
@SuppressWarnings("serial")
public class EnvironmentChangeEvent extends ApplicationEvent {
private Set<String> keys;
public EnvironmentChangeEvent(Set<String> keys) {
super(keys);
this.keys = keys;
}
/**
* @return the keys
*/
public Set<String> getKeys() {
return keys;
}
}
/*
* 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.platform.context.environment;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;
/**
* @author Dave Syer
*
*/
@ManagedResource
public class EnvironmentManager implements ApplicationEventPublisherAware {
private static final String MANAGER_PROPERTY_SOURCE = "manager";
private Map<String, Object> map = new LinkedHashMap<String, Object>();
private ConfigurableEnvironment environment;
private ApplicationEventPublisher publisher;
public EnvironmentManager(ConfigurableEnvironment environment) {
this.environment = environment;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
@ManagedOperation
public void reset() {
if (!map.isEmpty()) {
Set<String> keys = map.keySet();
map.clear();
publish(new EnvironmentChangeEvent(keys));
}
}
@ManagedOperation
public void setProperty(String name, String value) {
if (!environment.getPropertySources().contains(MANAGER_PROPERTY_SOURCE)) {
synchronized (map) {
if (!environment.getPropertySources().contains(MANAGER_PROPERTY_SOURCE)) {
MapPropertySource source = new MapPropertySource(
MANAGER_PROPERTY_SOURCE, map);
environment.getPropertySources().addFirst(source);
}
}
}
if (!value.equals(environment.getProperty(name))) {
map.put(name, value);
publish(new EnvironmentChangeEvent(Collections.singleton(name)));
}
}
@ManagedOperation
public Object getProperty(String name) {
return environment.getProperty(name);
}
private void publish(EnvironmentChangeEvent environmentChangeEvent) {
if (publisher !=null ) {
publisher.publishEvent(environmentChangeEvent);
}
}
}
/*
* 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.platform.context.environment;
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.platform.context.environment.EnvironmentManagerIntegrationTests.TestConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringApplicationConfiguration(classes = TestConfiguration.class)
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
public class EnvironmentManagerIntegrationTests {
@Autowired
private TestProperties properties;
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@Before
public void setUp() {
this.mvc = MockMvcBuilders.webAppContextSetup(this.context).build();
}
@Test
public void testRefresh() throws Exception {
assertEquals("Hello scope!", properties.getMessage());
// Change the dynamic property source...
this.mvc.perform(post("/env").param("message", "Foo")).andExpect(status().isOk())
.andExpect(content().string("{\"message\":\"Foo\"}"));
assertEquals("Foo", properties.getMessage());
}
public static void main(String[] args) {
SpringApplication.run(TestConfiguration.class, args);
}
@Configuration
@EnableAutoConfiguration
protected static class TestConfiguration {
@Bean
protected TestProperties properties() {
return new TestProperties();
}
}
@ConfigurationProperties
protected static class TestProperties {
private String message;
private int delay;
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;
}
}
}
/*
* 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.platform.context.environment;
import java.util.Map;
import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.actuate.endpoint.EnvironmentEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author Dave Syer
*
*/
public class EnvironmentManagerMvcEndpoint implements MvcEndpoint {
private EnvironmentManager environment;
private EnvironmentEndpoint delegate;
public EnvironmentManagerMvcEndpoint(EnvironmentEndpoint delegate, EnvironmentManager enviroment) {
this.delegate = delegate;
environment = enviroment;
}
@RequestMapping(value = "", method = RequestMethod.POST)
@ResponseBody
public Object value(@RequestParam Map<String,String> params) {
for (String name : params.keySet()) {
environment.setProperty(name, params.get(name));
}
return params;
}
public void setEnvironmentManager(EnvironmentManager environment) {
this.environment = environment;
}
@Override
public String getPath() {
return "/" + this.delegate.getId();
}
@Override
public boolean isSensitive() {
return this.delegate.isSensitive();
}
@Override
@SuppressWarnings("rawtypes")
public Class<? extends Endpoint> getEndpointType() {
return this.delegate.getClass();
}
}
/*
* Copyright 2002-2009 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.platform.context.scope;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.UUID;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.scope.ScopedProxyUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.BeanDefinitionVisitor;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
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.context.expression.BeanFactoryAccessor;
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.platform.context.config.BeanLifecycleDecorator;
import org.springframework.platform.context.config.StandardBeanLifecycleDecorator;
import org.springframework.platform.context.config.BeanLifecycleDecorator.Context;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
/**
* <p>
* A generic Scope implementation.
* </p>
*
* @author Dave Syer
*
* @since 3.1
*
*/
public class GenericScope implements Scope, BeanFactoryPostProcessor,
DisposableBean {
private static final Log logger = LogFactory.getLog(GenericScope.class);
private BeanLifecycleWrapperCache cache = new BeanLifecycleWrapperCache(
new StandardScopeCache());
private String name = "generic";
private boolean proxyTargetClass = true;
private boolean autoProxy = true;
private ConfigurableListableBeanFactory beanFactory;
private StandardEvaluationContext evaluationContext;
private String id;
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.
*
* @param id
* the id to set
*/
public void setId(String id) {
this.id = id;
}
/**
* The name of this scope. Default "refresh".
*
* @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.
*
* @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).
*
* @param autoProxy
* the flag value to set, default is true
*/
public void setAutoProxy(boolean autoProxy) {
this.autoProxy = autoProxy;
}
/**
* The cache implementation to use for bean instances in this scope.
*
* @param cache
* the cache to use
*/
public void setScopeCache(ScopeCache cache) {
this.cache = new BeanLifecycleWrapperCache(cache);
}
/**
* Helper to manage the creation and destruction of beans.
*
* @param lifecycle
* the bean lifecycle to set
*/
public void setBeanLifecycleManager(BeanLifecycleDecorator<?> lifecycle) {
this.lifecycle = lifecycle;
}
public void destroy() {
List<Throwable> errors = new ArrayList<Throwable>();
Collection<BeanLifecycleWrapper> wrappers = cache.clear();
for (BeanLifecycleWrapper wrapper : wrappers) {
try {
wrapper.destroy();
} catch (RuntimeException e) {
errors.add(e);
}
}
if (!errors.isEmpty()) {
throw wrapIfNecessary(errors.get(0));
}
}
protected void destroy(String name) {
BeanLifecycleWrapper wrapper = cache.remove(name);
if (wrapper != null) {
wrapper.destroy();
}
}
public Object get(String name, ObjectFactory<?> objectFactory) {
if (lifecycle == null) {
lifecycle = new StandardBeanLifecycleDecorator(proxyTargetClass);
}
BeanLifecycleWrapper value = cache.put(name, new BeanLifecycleWrapper(
name, objectFactory, lifecycle));
return value.getBean();
}
public String getConversationId() {
return name;
}
public void registerDestructionCallback(String name, Runnable callback) {
BeanLifecycleWrapper value = cache.get(name);
if (value == null) {
return;
}
value.setDestroyCallback(callback);
}
public Object remove(String name) {
BeanLifecycleWrapper value = cache.remove(name);
if (value == null) {
return null;
}
// Someone might have added another object with the same key, but we
// keep the method contract by removing the
// value we found anyway
return value.getBean();
}
public Object resolveContextualObject(String key) {
Expression expression = parseExpression(key);
return expression.getValue(evaluationContext, beanFactory);
}
private Expression parseExpression(String input) {
if (StringUtils.hasText(input)) {
ExpressionParser parser = new SpelExpressionParser();
try {
return parser.parseExpression(input);
} catch (ParseException e) {
throw new IllegalArgumentException("Cannot parse expression: "
+ input, e);
}
} else {
return null;
}
}
public void postProcessBeanFactory(
ConfigurableListableBeanFactory beanFactory) throws BeansException {
beanFactory.registerScope(name, this);
setSerializationId(beanFactory);
this.beanFactory = beanFactory;
evaluationContext = new StandardEvaluationContext();
evaluationContext.addPropertyAccessor(new BeanFactoryAccessor());
if (!autoProxy) {
// No need to try and create proxies
return;
}
Assert.state(beanFactory instanceof BeanDefinitionRegistry,
"BeanFactory was not a BeanDefinitionRegistry, so RefreshScope cannot be used.");
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
for (String beanName : beanFactory.getBeanDefinitionNames()) {
BeanDefinition definition = beanFactory.getBeanDefinition(beanName);
// 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.visitBeanDefinition(definition);
if (scoped) {
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.
*
* @param beanFactory
* the bean factory to configure
*/
private void setSerializationId(ConfigurableListableBeanFactory beanFactory) {
if (beanFactory instanceof DefaultListableBeanFactory) {
String id = this.id;
if (id == null) {
String names = Arrays.asList(
beanFactory.getBeanDefinitionNames()).toString();
logger.debug("Generating bean factory id from names: " + names);
id = UUID.nameUUIDFromBytes(names.getBytes()).toString();
}
logger.info("BeanFactory id=" + id);
((DefaultListableBeanFactory) beanFactory).setSerializationId(id);
} else {
logger.warn("BeanFactory was not a DefaultListableBeanFactory, so RefreshScope beans "
+ "cannot be serialized reliably and passed to a remote JVM.");
}
}
static RuntimeException wrapIfNecessary(Throwable throwable) {
if (throwable instanceof RuntimeException) {
return (RuntimeException) throwable;
}
if (throwable instanceof Error) {
throw (Error) throwable;
}
return new IllegalStateException(throwable);
}
private static BeanDefinitionHolder createScopedProxy(String beanName,
BeanDefinition definition, BeanDefinitionRegistry registry,
boolean proxyTargetClass) {
BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(
new BeanDefinitionHolder(definition, beanName), registry,
proxyTargetClass);
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.
*
* @author Dave Syer
*
*/
private static class Scopifier extends BeanDefinitionVisitor {
private final boolean proxyTargetClass;
private final BeanDefinitionRegistry registry;
private final String scope;
private final boolean scoped;
public Scopifier(BeanDefinitionRegistry registry, String scope,
boolean proxyTargetClass, boolean scoped) {
super(new StringValueResolver() {
public String resolveStringValue(String value) {
return value;
}
});
this.registry = registry;
this.proxyTargetClass = proxyTargetClass;
this.scope = scope;
this.scoped = scoped;
}
@Override
protected Object resolveValue(Object value) {
BeanDefinition definition = null;
String beanName = null;
if (value instanceof BeanDefinition) {
definition = (BeanDefinition) value;
beanName = BeanDefinitionReaderUtils.generateBeanName(
definition, registry);
} else if (value instanceof BeanDefinitionHolder) {
BeanDefinitionHolder holder = (BeanDefinitionHolder) value;
definition = holder.getBeanDefinition();
beanName = holder.getBeanName();
}
if (definition != null) {
boolean nestedScoped = scope.equals(definition.getScope());
boolean scopeChangeRequiresProxy = !scoped && nestedScoped;
if (scopeChangeRequiresProxy) {
// Exit here so that nested inner bean definitions are not
// analysed
return createScopedProxy(beanName, definition, registry,
proxyTargetClass);
}
}
// Nested inner bean definitions are recursively analysed here
value = super.resolveValue(value);
return value;
}
}
private static class BeanLifecycleWrapperCache {
private final ScopeCache cache;
public BeanLifecycleWrapperCache(ScopeCache cache) {
this.cache = cache;
}
public BeanLifecycleWrapper remove(String name) {
return (BeanLifecycleWrapper) cache.remove(name);
}
public Collection<BeanLifecycleWrapper> clear() {
Collection<Object> values = cache.clear();
Collection<BeanLifecycleWrapper> wrappers = new LinkedHashSet<BeanLifecycleWrapper>();
for (Object object : values) {
wrappers.add((BeanLifecycleWrapper) object);
}
return wrappers;
}
public BeanLifecycleWrapper get(String name) {
return (BeanLifecycleWrapper) cache.get(name);
}
public BeanLifecycleWrapper put(String name, BeanLifecycleWrapper value) {
return (BeanLifecycleWrapper) cache.put(name, (Object) value);
}
}
/**
* 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
*
*/
private static class BeanLifecycleWrapper {
private Object bean;
private Context<?> context;
private final String name;
@SuppressWarnings("rawtypes")
private final BeanLifecycleDecorator lifecycle;
private final ObjectFactory<?> objectFactory;
@SuppressWarnings("rawtypes")
public BeanLifecycleWrapper(String name,
ObjectFactory<?> objectFactory, BeanLifecycleDecorator lifecycle) {
this.name = name;
this.objectFactory = objectFactory;
this.lifecycle = lifecycle;
}
public void setDestroyCallback(Runnable callback) {
this.context = lifecycle.decorateDestructionCallback(callback);
}
@SuppressWarnings("unchecked")
public Object getBean() {
if (bean == null) {
bean = lifecycle.decorateBean(objectFactory.getObject(),
context);
}
return bean;
}
public void destroy() {
context.getCallback().run();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
BeanLifecycleWrapper other = (BeanLifecycleWrapper) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
}
/*
* 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.platform.context.scope.refresh;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.aop.framework.Advised;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.platform.context.autoconfigure.RefreshAutoConfiguration;
import org.springframework.platform.context.config.annotation.RefreshScope;
import org.springframework.platform.context.scope.refresh.MoreRefreshScopeIntegrationTests.TestConfiguration;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@SpringApplicationConfiguration(classes=TestConfiguration.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class MoreRefreshScopeIntegrationTests {
@Autowired
private TestService service;
@Autowired
private TestProperties properties;
@Autowired
private org.springframework.platform.context.scope.refresh.RefreshScope scope;
@Autowired
private ConfigurableEnvironment environment;
@Before
public void init() {
TestService.reset();
}
@Test
@DirtiesContext
public void testSimpleProperties() throws Exception {
assertEquals("Hello scope!", service.getMessage());
assertTrue(service instanceof Advised);
// Change the dynamic property source...
EnvironmentTestUtils.addEnvironment(environment, "message:Foo");
// ...but don't refresh, so the bean stays the same:
assertEquals("Hello scope!", service.getMessage());
assertEquals(1, TestService.getInitCount());
assertEquals(0, TestService.getDestroyCount());
}
@Test
@DirtiesContext
public void testRefresh() throws Exception {
assertEquals("Hello scope!", service.getMessage());
String id1 = service.toString();
// Change the dynamic property source...
EnvironmentTestUtils.addEnvironment(environment, "message:Foo");
// ...and then refresh, so the bean is re-initialized:
scope.refreshAll();
String id2 = service.toString();
assertEquals("Foo", service.getMessage());
assertEquals(2, TestService.getInitCount());
assertEquals(1, TestService.getDestroyCount());
assertNotSame(id1, id2);
}
public static class TestService implements InitializingBean, DisposableBean {
private static Log logger = LogFactory.getLog(TestService.class);
private volatile static int initCount = 0;
private volatile static int destroyCount = 0;
private String message = null;
private volatile long delay = 0;
public void setDelay(long delay) {
this.delay = delay;
}
public void afterPropertiesSet() throws Exception {
logger.debug("Initializing message: " + message);
initCount++;
}
public void destroy() throws Exception {
logger.debug("Destroying message: " + message);
destroyCount++;
message = null;
}
public static void reset() {
initCount = 0;
destroyCount = 0;
}
public static int getInitCount() {
return initCount;
}
public static int getDestroyCount() {
return destroyCount;
}
public void setMessage(String message) {
logger.debug("Setting message: " + message);
this.message = message;
}
public String getMessage() {
logger.debug("Getting message: " + message);
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
logger.info("Returning message: " + message);
return message;
}
}
@Configuration
@EnableConfigurationProperties
@Import({RefreshAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class})
protected static class TestConfiguration {
@Bean
@RefreshScope
protected TestProperties properties() {
return new TestProperties();
}
@Bean
@RefreshScope
public TestService service() {
TestService service = new TestService();
service.setMessage(properties().getMessage());
service.setDelay(properties().getDelay());
return service;
}
}
@ConfigurationProperties
@ManagedResource
protected static class TestProperties {
private String message;
private int delay;
@ManagedAttribute
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@ManagedAttribute
public int getDelay() {
return delay;
}
public void setDelay(int delay) {
this.delay = delay;
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-utility</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Spring Utility</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.0.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<url>http://spring.io</url>
<description>
<![CDATA[
This project is a minimal jar utility with Spring configuration.
]]>
</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
/*
* 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.platform.context.autoconfigure;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration;
import org.springframework.boot.actuate.endpoint.EnvironmentEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetaData;
import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.platform.context.environment.EnvironmentManager;
import org.springframework.platform.context.environment.EnvironmentManagerMvcEndpoint;
import org.springframework.platform.context.properties.ConfigurationPropertiesRebinder;
import org.springframework.platform.context.scope.refresh.RefreshScope;
@Configuration
@ConditionalOnClass(RefreshScope.class)
@AutoConfigureAfter(EndpointAutoConfiguration.class)
public class RefreshAutoConfiguration {
@Autowired
private ConfigurationPropertiesBindingPostProcessor binder;
@Autowired
private ConfigurationBeanFactoryMetaData metaData;
@Bean
@ConditionalOnMissingBean
public static RefreshScope refreshScope() {
return new RefreshScope();
}
@Bean
@ConditionalOnMissingBean
public EnvironmentManager environmentManager(ConfigurableEnvironment environment) {
return new EnvironmentManager(environment);
}
@Bean
@ConditionalOnMissingBean
public ConfigurationPropertiesRebinder configurationPropertiesRebinder() {
ConfigurationPropertiesRebinder rebinder = new ConfigurationPropertiesRebinder(binder);
rebinder.setBeanMetaDataStore(metaData);
return rebinder;
}
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass(EnvironmentEndpoint.class)
@ConditionalOnExpression("${endpoints.env.enabled:true}")
@ConditionalOnBean(EnvironmentEndpoint.class)
protected static class EnvironmentEndpointConfiguration {
@Bean
public EnvironmentManagerMvcEndpoint environmentManagerEndpoint(EnvironmentEndpoint delegate, EnvironmentManager environment) {
return new EnvironmentManagerMvcEndpoint(delegate, environment);
}
}
}
/*
* 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.platform.context.config.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
/**
* @see Scope#proxyMode()
*/
ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
}
/*
* Copyright 2002-2009 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.platform.context.scope.refresh;
import java.io.Serializable;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.platform.context.scope.GenericScope;
/**
* <p>
* A Scope implementation that allows for beans to be refreshed dynamically at runtime (see {@link #refresh(String)} and
* {@link #refreshAll()}). If a bean is refreshed then the next time the bean is accessed (i.e. a method is executed) a
* new instance is created. All lifecycle methods are applied to the bean instances, so any destruction callbacks that
* were registered in the bean factory are called when it is refreshed, and then the initialization callbacks are
* invoked as normal when the new instance is created. A new bean instance is created from the original bean definition,
* so any externalized content (property placeholders or expressions in string literals) is re-evaluated when it is
* created.
* </p>
*
* <p>
* Note that all beans in this scope are <em>only</em> initialized when first accessed, so the scope forces lazy
* initialization semantics. The implementation involves creating a proxy for every bean in the scope, so there is a
* flag {@link #setProxyTargetClass(boolean) proxyTargetClass} which controls the proxy creation, defaulting to JDK
* dynamic proxies and therefore only exposing the interfaces implemented by a bean. If callers need access to other
* methods then the flag needs to be set (and CGLib present on the classpath). Because this scope automatically proxies
* all its beans, there is no need to add <code>&lt;aop:auto-proxy/&gt;</code> to any bean definitions.
* </p>
*
* <p>
* The scoped proxy approach adopted here has a side benefit that bean instances are automatically {@link Serializable},
* and can be sent across the wire as long as the receiver has an identical application context on the other side. To
* ensure that the two contexts agree that they are identical they have to have the same serialization id. One will be
* generated automatically by default from the bean names, so two contexts with the same bean names are by default able
* to exchange beans by name. If you need to override the default id then provide an explicit {@link #setId(String) id}
* when the Scope is declared.
* </p>
*
* @author Dave Syer
*
* @since 3.1
*
*/
@ManagedResource
public class RefreshScope extends GenericScope {
/**
* Create a scope instance and give it the default name: "refresh".
*/
public RefreshScope() {
super();
super.setName("refresh");
}
@ManagedOperation(description = "Dispose of the current instance of bean name provided and force a refresh on next method execution.")
public void refresh(String name) {
if (!name.startsWith("scopedTarget.")) {
// User wants to refresh the bean with this name but that isn't the one in the cache...
name = "scopedTarget." + name;
}
// Ensure lifecycle is finished if bean was disposable
super.destroy(name);
}
@ManagedOperation(description = "Dispose of the current instance of all beans in this scope and force a refresh on next method execution.")
public void refreshAll() {
super.destroy();
}
}
/*
* 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.platform.context.scope.refresh;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.aop.framework.Advised;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
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.SpringApplicationConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.platform.context.autoconfigure.RefreshAutoConfiguration;
import org.springframework.platform.context.config.annotation.RefreshScope;
import org.springframework.platform.context.scope.refresh.RefreshScopeIntegrationTests.TestConfiguration;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.Repeat;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@SpringApplicationConfiguration(classes=TestConfiguration.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class RefreshScopeIntegrationTests {
private static Log logger = LogFactory.getLog(RefreshScopeIntegrationTests.class);
private ExecutorService executor = Executors.newSingleThreadExecutor();
@Autowired
private Service service;
@Autowired
private TestProperties properties;
@Autowired
private org.springframework.platform.context.scope.refresh.RefreshScope scope;
@Before
public void init() {
ExampleService.reset();
}
@Test
@DirtiesContext
public void testSimpleProperties() throws Exception {
assertEquals("Hello scope!", service.getMessage());
assertTrue(service instanceof Advised);
// Change the dynamic property source...
properties.setMessage("Foo");
// ...but don't refresh, so the bean stays the same:
assertEquals("Hello scope!", service.getMessage());
assertEquals(1, ExampleService.getInitCount());
assertEquals(0, ExampleService.getDestroyCount());
}
@Test
@DirtiesContext
public void testRefresh() throws Exception {
assertEquals("Hello scope!", service.getMessage());
String id1 = service.toString();
// Change the dynamic property source...
properties.setMessage("Foo");
// ...and then refresh, so the bean is re-initialized:
scope.refreshAll();
String id2 = service.toString();
assertEquals("Foo", service.getMessage());
assertEquals(2, ExampleService.getInitCount());
assertEquals(1, ExampleService.getDestroyCount());
assertNotSame(id1, id2);
}
@Test
@DirtiesContext
public void testRefreshBean() throws Exception {
assertEquals("Hello scope!", service.getMessage());
String id1 = service.toString();
// Change the dynamic property source...
properties.setMessage("Foo");
// ...and then refresh, so the bean is re-initialized:
scope.refresh("service");
String id2 = service.toString();
assertEquals("Foo", service.getMessage());
assertEquals(2, ExampleService.getInitCount());
assertEquals(1, ExampleService.getDestroyCount());
assertNotSame(id1, id2);
}
@Test
@Repeat(10)
@DirtiesContext
public void testConcurrentRefresh() throws Exception {
ExampleService.reset();
assertEquals("Hello scope!", service.getMessage());
properties.setMessage("Foo");
properties.setDelay(500);
final CountDownLatch latch = new CountDownLatch(1);
Future<String> result = executor.submit(new Callable<String>() {
public String call() throws Exception {
logger.debug("Background started.");
try {
latch.countDown();
return service.getMessage();
} finally {
logger.debug("Background done.");
}
}
});
assertTrue(latch.await(500, TimeUnit.MILLISECONDS));
logger.info("Refreshing");
scope.refreshAll();
assertEquals("Foo", service.getMessage());
/*
* This is the most important assertion: we don't want a null value because that means the bean was destroyed
* and not re-initialized before we accessed it.
*/
assertNotNull(result.get());
assertEquals("Hello scope!", result.get());
assertTrue(Arrays.asList(2, 3).contains(ExampleService.getInitCount()));
assertTrue(Arrays.asList(1, 2).contains(ExampleService.getDestroyCount()));
}
public static interface Service {
String getMessage();
}
public static class ExampleService implements Service, InitializingBean, DisposableBean {
private static Log logger = LogFactory.getLog(ExampleService.class);
private volatile static int initCount = 0;
private volatile static int destroyCount = 0;
private String message = null;
private volatile long delay = 0;
public void setDelay(long delay) {
this.delay = delay;
}
public void afterPropertiesSet() throws Exception {
logger.debug("Initializing message: " + message);
initCount++;
}
public void destroy() throws Exception {
logger.debug("Destroying message: " + message);
destroyCount++;
message = null;
}
public static void reset() {
initCount = 0;
destroyCount = 0;
}
public static int getInitCount() {
return initCount;
}
public static int getDestroyCount() {
return destroyCount;
}
public void setMessage(String message) {
logger.debug("Setting message: " + message);
this.message = message;
}
public String getMessage() {
logger.debug("Getting message: " + message);
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
logger.info("Returning message: " + message);
return message;
}
}
@Configuration
@EnableConfigurationProperties(TestProperties.class)
@Import({RefreshAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class})
protected static class TestConfiguration {
@Autowired
private TestProperties properties;
@Bean
@RefreshScope
public ExampleService service() {
ExampleService service = new ExampleService();
service.setMessage(properties.getMessage());
service.setDelay(properties.getDelay());
return service;
}
}
@ConfigurationProperties
@ManagedResource
protected static class TestProperties {
private String message;
private int delay;
@ManagedAttribute
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@ManagedAttribute
public int getDelay() {
return delay;
}
public void setDelay(int delay) {
this.delay = delay;
}
}
}
/*
* Copyright 2002-2011 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.platform.context.scope;
import java.util.Collection;
/**
* A special purpose cache interface specifically for the {@link GenericScope} to use to manage cached bean instances.
* Implementations generally fall into two categories: those that store values "globally" (i.e. one instance per key),
* and those that store potentially multiple instances per key based on context (e.g. via a thread local). All
* implementations should be thread safe.
*
* @author Dave Syer
*
*/
public interface ScopeCache {
/**
* Remove the object with this name from the cache.
*
* @param name the object name
* @return the object removed or null if there was none
*/
Object remove(String name);
/**
* Clear the cache and return all objects in an unmodifiable collection.
*
* @return all objects stored in the cache
*/
Collection<Object> clear();
/**
* Get the named object from the cache.
*
* @param name the name of the object
* @return the object with that name or null if there is none
*/
Object get(String name);
/**
* Put a value in the cache if the key is not already used. If one is already present with the name provided, it is
* not replaced, but is returned to the caller.
*
* @param name the key
* @param value the new candidate value
* @return the value that is in the cache at the end of the operation
*/
Object put(String name, Object value);
}
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.platform.context.autoconfigure.RefreshAutoConfiguration
/*
* Copyright 2002-2011 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.platform.context.config;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactory;
/**
* A {@link BeanLifecycleDecorator} that tries to protect against concurrent access to a bean during its own destruction.
* A read-write lock is used, and method access is protected using the read lock, while the destruction callback is
* protected more strictly with the write lock. In this way concurrent access is possible to the bean as long as it is
* not being destroyed, in which case only one thread has access. If the bean has no destruction callback the lock and
* associated proxies are never created.
*
* @author Dave Syer
*
*/
public class StandardBeanLifecycleDecorator implements BeanLifecycleDecorator<ReadWriteLock> {
private final boolean proxyTargetClass;
public StandardBeanLifecycleDecorator(boolean proxyTargetClass) {
this.proxyTargetClass = proxyTargetClass;
}
public Object decorateBean(Object bean, Context<ReadWriteLock> context) {
if (context != null) {
bean = getDisposalLockProxy(bean, context.getAuxiliary().readLock());
}
return bean;
}
public Context<ReadWriteLock> decorateDestructionCallback(final Runnable callback) {
if (callback == null) {
return null;
}
final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
return new Context<ReadWriteLock>(new Runnable() {
public void run() {
Lock lock = readWriteLock.writeLock();
lock.lock();
try {
callback.run();
} finally {
lock.unlock();
}
}
}, readWriteLock);
}
/**
* Apply a lock (preferably a read lock allowing multiple concurrent access) to the bean. Callers should replace the
* bean input with the output.
*
* @param bean the bean to lock
* @param lock the lock to apply
* @return a proxy that locks while its methods are executed
*/
private Object getDisposalLockProxy(Object bean, final Lock lock) {
ProxyFactory factory = new ProxyFactory(bean);
factory.setProxyTargetClass(proxyTargetClass);
factory.addAdvice(new MethodInterceptor() {
public Object invoke(MethodInvocation invocation) throws Throwable {
lock.lock();
try {
return invocation.proceed();
} finally {
lock.unlock();
}
}
});
return factory.getProxy();
}
}
/*
* Copyright 2002-2011 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.platform.context.scope;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* A simple cache implementation backed by a concurrent map.
*
* @author Dave Syer
*
*/
public class StandardScopeCache implements ScopeCache {
private final ConcurrentMap<String, Object> cache = new ConcurrentHashMap<String, Object>();
public Object remove(String name) {
return cache.remove(name);
}
public Collection<Object> clear() {
Collection<Object> values = new ArrayList<Object>(cache.values());
cache.clear();
return values;
}
public Object get(String name) {
return cache.get(name);
}
public Object put(String name, Object value) {
Object result = cache.putIfAbsent(name, value);
if (result!=null) {
return result;
}
return value;
}
}
/*
* Copyright 2002-2011 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.platform.context.scope.thread;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.springframework.platform.context.scope.ScopeCache;
/**
* @author Dave Syer
*
*/
public class ThreadLocalScopeCache implements ScopeCache {
private ThreadLocal<ConcurrentMap<String, Object>> data = new ThreadLocal<ConcurrentMap<String, Object>>() {
protected ConcurrentMap<String, Object> initialValue() {
return new ConcurrentHashMap<String, Object>();
}
};
public Object remove(String name) {
return data.get().remove(name);
}
public Collection<Object> clear() {
ConcurrentMap<String, Object> map = data.get();
Collection<Object> values = new ArrayList<Object>(map.values());
map.clear();
return values;
}
public Object get(String name) {
return data.get().get(name);
}
public Object put(String name, Object value) {
Object result = data.get().putIfAbsent(name, value);
if (result!=null) {
return result;
}
return value;
}
}
/*
* Copyright 2002-2009 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.platform.context.scope.thread;
import org.springframework.platform.context.scope.GenericScope;
/**
*
* @author Dave Syer
*
* @since 3.1
*
*/
public class ThreadScope extends GenericScope {
/**
* Create a scope instance and give it the default name: "thread".
*/
public ThreadScope() {
super();
super.setName("thread");
super.setScopeCache(new ThreadLocalScopeCache());
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment