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:
parent
f09134180e
commit
a9f69e86be
|
@ -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 {
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue