Add RestartScope and Restart attributes

Add a "restart" Spring `Scope` that remains between application
restarts. Allows beans such as the livereload server to remain active
during restarts and not disconnect clients.

See gh-3085
This commit is contained in:
Phillip Webb 2015-06-01 13:22:51 -07:00
parent f09134180e
commit a9f69e86be
7 changed files with 268 additions and 0 deletions

View File

@ -0,0 +1,41 @@
/*
* Copyright 2012-2015 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.boot.developertools.restart;
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;
/**
* Restart {@code @Scope} Annotation used to indicate that a bean shoul remain beteen
* restarts.
*
* @author Phillip Webb
* @since 1.3.0
* @see RestartScopeInitializer
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope("restart")
public @interface RestartScope {
}

View File

@ -0,0 +1,68 @@
/*
* Copyright 2012-2015 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.boot.developertools.restart;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
/**
* Support for a 'restart' {@link Scope} that allows beans to remain between restarts.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class RestartScopeInitializer implements
ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
applicationContext.getBeanFactory().registerScope("restart", new RestartScope());
}
/**
* {@link Scope} that stores beans as {@link Restarter} attributes.
*/
private static class RestartScope implements Scope {
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
return Restarter.getInstance().getOrAddAttribute(name, objectFactory);
}
@Override
public Object remove(String name) {
return Restarter.getInstance().removeAttribute(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return null;
}
}
}

View File

@ -22,6 +22,7 @@ import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedList;
@ -38,6 +39,7 @@ import java.util.concurrent.locks.ReentrantLock;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.CachedIntrospectionResults;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.developertools.restart.classloader.RestartClassLoader;
import org.springframework.boot.logging.DeferredLog;
@ -89,6 +91,8 @@ public class Restarter {
private final UncaughtExceptionHandler exceptionHandler;
private final Map<String, Object> attributes = new HashMap<String, Object>();
private final BlockingDeque<LeakSafeThread> leakSafeThreads = new LinkedBlockingDeque<LeakSafeThread>();
private boolean finished = false;
@ -344,6 +348,22 @@ public class Restarter {
}
}
public Object getOrAddAttribute(final String name,
final ObjectFactory<?> objectFactory) {
synchronized (this.attributes) {
if (!this.attributes.containsKey(name)) {
this.attributes.put(name, objectFactory.getObject());
}
return this.attributes.get(name);
}
}
public Object removeAttribute(String name) {
synchronized (this.attributes) {
return this.attributes.remove(name);
}
}
/**
* Return the initial set of URLs as configured by the {@link RestartInitializer}.
* @return the initial URLs or {@code null}

View File

@ -1,3 +1,7 @@
# Application Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.developertools.restart.RestartScopeInitializer
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.developertools.restart.RestartApplicationListener

View File

@ -24,8 +24,13 @@ import java.util.concurrent.ThreadFactory;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.ObjectFactory;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
/**
@ -53,9 +58,26 @@ public class MockRestarter implements TestRule {
};
}
@SuppressWarnings("rawtypes")
private void setup() {
Restarter.setInstance(this.mock);
given(this.mock.getInitialUrls()).willReturn(new URL[] {});
given(this.mock.getOrAddAttribute(anyString(), (ObjectFactory) any()))
.willAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
String name = (String) invocation.getArguments()[0];
ObjectFactory factory = (ObjectFactory) invocation.getArguments()[1];
Object attribute = MockRestarter.this.attributes.get(name);
if (attribute == null) {
attribute = factory.getObject();
MockRestarter.this.attributes.put(name, attribute);
}
return attribute;
}
});
given(this.mock.getThreadFactory()).willReturn(new ThreadFactory() {
@Override

View File

@ -0,0 +1,86 @@
/*
* Copyright 2012-2015 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.boot.developertools.restart;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link RestartScopeInitializer}.
*
* @author Phillip Webb
*/
public class RestartScopeInitializerTests {
private static AtomicInteger createCount;
private static AtomicInteger refreshCount;
@Test
public void restartScope() throws Exception {
createCount = new AtomicInteger();
refreshCount = new AtomicInteger();
ConfigurableApplicationContext context = runApplication();
context.close();
context = runApplication();
context.close();
assertThat(createCount.get(), equalTo(1));
assertThat(refreshCount.get(), equalTo(2));
}
private ConfigurableApplicationContext runApplication() {
SpringApplication application = new SpringApplication(Config.class);
application.setWebEnvironment(false);
return application.run();
}
@Configuration
public static class Config {
@Bean
@RestartScope
public ScopeTestBean scopeTestBean() {
return new ScopeTestBean();
}
}
public static class ScopeTestBean implements
ApplicationListener<ContextRefreshedEvent> {
public ScopeTestBean() {
createCount.incrementAndGet();
}
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
refreshCount.incrementAndGet();
}
}
}

View File

@ -25,6 +25,8 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.test.OutputCapture;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.scheduling.annotation.EnableScheduling;
@ -38,6 +40,7 @@ import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* Tests for {@link Restarter}.
@ -88,6 +91,30 @@ public class RestarterTests {
assertThat(StringUtils.countOccurrencesOf(output, "Tick 1"), greaterThan(2));
}
@Test
@SuppressWarnings("rawtypes")
public void getOrAddAttributeWithNewAttribute() throws Exception {
ObjectFactory objectFactory = mock(ObjectFactory.class);
given(objectFactory.getObject()).willReturn("abc");
Object attribute = Restarter.getInstance().getOrAddAttribute("x", objectFactory);
assertThat(attribute, equalTo((Object) "abc"));
}
@Test
@SuppressWarnings("rawtypes")
public void getOrAddAttributeWithExistingAttribute() throws Exception {
Restarter.getInstance().getOrAddAttribute("x", new ObjectFactory<String>() {
@Override
public String getObject() throws BeansException {
return "abc";
}
});
ObjectFactory objectFactory = mock(ObjectFactory.class);
Object attribute = Restarter.getInstance().getOrAddAttribute("x", objectFactory);
assertThat(attribute, equalTo((Object) "abc"));
verifyZeroInteractions(objectFactory);
}
@Test
public void getThreadFactory() throws Exception {
final ClassLoader parentLoader = Thread.currentThread().getContextClassLoader();