From a9f69e86be283e2d968d6e4bbb50f5a63d4db414 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 1 Jun 2015 13:22:51 -0700 Subject: [PATCH] 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 --- .../developertools/restart/RestartScope.java | 41 +++++++++ .../restart/RestartScopeInitializer.java | 68 +++++++++++++++ .../developertools/restart/Restarter.java | 20 +++++ .../main/resources/META-INF/spring.factories | 4 + .../developertools/restart/MockRestarter.java | 22 +++++ .../restart/RestartScopeInitializerTests.java | 86 +++++++++++++++++++ .../restart/RestarterTests.java | 27 ++++++ 7 files changed, 268 insertions(+) create mode 100644 spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/restart/RestartScope.java create mode 100644 spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/restart/RestartScopeInitializer.java create mode 100644 spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/RestartScopeInitializerTests.java diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/restart/RestartScope.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/restart/RestartScope.java new file mode 100644 index 00000000000..557d5287936 --- /dev/null +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/restart/RestartScope.java @@ -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 { + +} diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/restart/RestartScopeInitializer.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/restart/RestartScopeInitializer.java new file mode 100644 index 00000000000..ca7a133c001 --- /dev/null +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/restart/RestartScopeInitializer.java @@ -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 { + + @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; + } + } + +} diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/restart/Restarter.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/restart/Restarter.java index bdf57ee1725..a0017f705e7 100644 --- a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/restart/Restarter.java +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/restart/Restarter.java @@ -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 attributes = new HashMap(); + private final BlockingDeque leakSafeThreads = new LinkedBlockingDeque(); 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} diff --git a/spring-boot-developer-tools/src/main/resources/META-INF/spring.factories b/spring-boot-developer-tools/src/main/resources/META-INF/spring.factories index 5dcd08d49d7..88d7a6e0f16 100644 --- a/spring-boot-developer-tools/src/main/resources/META-INF/spring.factories +++ b/spring-boot-developer-tools/src/main/resources/META-INF/spring.factories @@ -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 diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/MockRestarter.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/MockRestarter.java index b5034595086..f13a3f43344 100644 --- a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/MockRestarter.java +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/MockRestarter.java @@ -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() { + + @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 diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/RestartScopeInitializerTests.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/RestartScopeInitializerTests.java new file mode 100644 index 00000000000..943f55d41e7 --- /dev/null +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/RestartScopeInitializerTests.java @@ -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 { + + public ScopeTestBean() { + createCount.incrementAndGet(); + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + refreshCount.incrementAndGet(); + } + + } + +} diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/RestarterTests.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/RestarterTests.java index a51c69908f0..b0cac5be79c 100644 --- a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/RestarterTests.java +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/RestarterTests.java @@ -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() { + @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();