Add support for Restarting applications

Add Restarter class that can be used to restart a running application
when underlying class files change. The Restarter is automatically
initialized via a ApplicationListener and automatically detects
classpath URLs that are likely to change (when not running from a fat
jar).

See gh-3084
This commit is contained in:
Phillip Webb 2015-06-01 13:22:45 -07:00
parent da51785706
commit a5c56ca482
23 changed files with 2114 additions and 0 deletions

View File

@ -17,6 +17,7 @@
package org.springframework.boot.developertools.autoconfigure;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.developertools.restart.ConditionalOnInitializedRestarter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -27,6 +28,7 @@ import org.springframework.context.annotation.Configuration;
* @since 1.3.0
*/
@Configuration
@ConditionalOnInitializedRestarter
public class LocalDeveloperToolsAutoConfiguration {
@Bean

View File

@ -0,0 +1,111 @@
/*
* 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.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;
/**
* A filtered collections of URLs which can be change after the application has started.
*
* @author Phillip Webb
*/
class ChangeableUrls implements Iterable<URL> {
private static final String[] SKIPPED_PROJECTS = { "spring-boot",
"spring-boot-developer-tools", "spring-boot-autoconfigure",
"spring-boot-actuator", "spring-boot-starter" };
private static final Pattern STARTER_PATTERN = Pattern
.compile("\\/spring-boot-starter-[\\w-]+\\/");
private final List<URL> urls;
private ChangeableUrls(URL... urls) {
List<URL> reloadableUrls = new ArrayList<URL>(urls.length);
for (URL url : urls) {
if (isReloadable(url)) {
reloadableUrls.add(url);
}
}
this.urls = Collections.unmodifiableList(reloadableUrls);
}
private boolean isReloadable(URL url) {
String urlString = url.toString();
return isFolderUrl(urlString) && !isSkipped(urlString);
}
private boolean isFolderUrl(String urlString) {
return urlString.startsWith("file:") && urlString.endsWith("/");
}
private boolean isSkipped(String urlString) {
// Skip certain spring-boot projects to allow them to be imported in the same IDE
for (String skipped : SKIPPED_PROJECTS) {
if (urlString.contains("/" + skipped + "/target/classes/")) {
return true;
}
}
// Skip all starter projects
if (STARTER_PATTERN.matcher(urlString).find()) {
return true;
}
return false;
}
@Override
public Iterator<URL> iterator() {
return this.urls.iterator();
}
public int size() {
return this.urls.size();
}
public URL[] toArray() {
return this.urls.toArray(new URL[this.urls.size()]);
}
public List<URL> toList() {
return Collections.unmodifiableList(this.urls);
}
@Override
public String toString() {
return this.urls.toString();
}
public static ChangeableUrls fromUrlClassLoader(URLClassLoader classLoader) {
return fromUrls(classLoader.getURLs());
}
public static ChangeableUrls fromUrls(Collection<URL> urls) {
return fromUrls(new ArrayList<URL>(urls).toArray(new URL[urls.size()]));
}
public static ChangeableUrls fromUrls(URL... urls) {
return new ChangeableUrls(urls);
}
}

View File

@ -0,0 +1,40 @@
/*
* 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.Conditional;
/**
* {@link Conditional} that only matches when the {@link RestartInitializer} has been
* applied with non {@code null} URLs.
*
* @author Phillip Webb
* @since 1.3.0
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnInitializedRestarterCondition.class)
public @interface ConditionalOnInitializedRestarter {
}

View File

@ -0,0 +1,94 @@
/*
* 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.net.URL;
import java.net.URLClassLoader;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Default {@link RestartInitializer} that only enable initial restart when running a
* standard "main" method. Skips initialization when running "fat" jars (included
* exploded) or when running from a test.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class DefaultRestartInitializer implements RestartInitializer {
private static final Set<String> SKIPPED_STACK_ELEMENTS;
static {
Set<String> skipped = new LinkedHashSet<String>();
skipped.add("org.junit.runners.");
skipped.add("org.springframework.boot.test.");
SKIPPED_STACK_ELEMENTS = Collections.unmodifiableSet(skipped);
}
@Override
public URL[] getInitialUrls(Thread thread) {
if (!isMain(thread)) {
return null;
}
for (StackTraceElement element : thread.getStackTrace()) {
if (isSkippedStackElement(element)) {
return null;
}
}
return getUrls(thread);
}
/**
* Returns if the thread is for a main invocation. By default checks the name of the
* thread and the context classloader.
* @param thread the thread to check
* @return {@code true} if the thread is a main invocation
*/
protected boolean isMain(Thread thread) {
return thread.getName().equals("main")
&& thread.getContextClassLoader().getClass().getName()
.contains("AppClassLoader");
}
/**
* Checks if a specific {@link StackTraceElement} should cause the initializer to be
* skipped.
* @param element the stack element to check
* @return {@code true} if the stack element means that the initializer should be
* skipped
*/
protected boolean isSkippedStackElement(StackTraceElement element) {
for (String skipped : SKIPPED_STACK_ELEMENTS) {
if (element.getClassName().startsWith(skipped)) {
return true;
}
}
return false;
}
/**
* Return the URLs that should be used with initialization.
* @param thread the source thread
* @return the URLs
*/
protected URL[] getUrls(Thread thread) {
return ChangeableUrls.fromUrlClassLoader(
(URLClassLoader) thread.getContextClassLoader()).toArray();
}
}

View File

@ -0,0 +1,84 @@
/*
* 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.reflect.Method;
import java.lang.reflect.Modifier;
import org.springframework.util.Assert;
/**
* The "main" method located from a running thread.
*
* @author Phillip Webb
*/
class MainMethod {
private final Method method;
public MainMethod() {
this(Thread.currentThread());
}
public MainMethod(Thread thread) {
Assert.notNull(thread, "Thread must not be null");
this.method = getMainMethod(thread);
}
private Method getMainMethod(Thread thread) {
for (StackTraceElement element : thread.getStackTrace()) {
if ("main".equals(element.getMethodName())) {
Method method = getMainMethod(element);
if (method != null) {
return method;
}
}
}
throw new IllegalStateException("Unable to find main method");
}
private Method getMainMethod(StackTraceElement element) {
try {
Class<?> elementClass = Class.forName(element.getClassName());
Method method = elementClass.getDeclaredMethod("main", String[].class);
if (Modifier.isStatic(method.getModifiers())) {
return method;
}
}
catch (Exception ex) {
// Ignore
}
return null;
}
/**
* Returns the actual main method.
* @return the main method
*/
public Method getMethod() {
return this.method;
}
/**
* Return the name of the declaring class.
* @return the declaring class name
*/
public String getDeclaringClassName() {
return this.method.getDeclaringClass().getName();
}
}

View File

@ -0,0 +1,55 @@
/*
* 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.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* {@link Condition} that checks that a {@link Restarter} is available an initialized.
*
* @author Phillip Webb
* @see ConditionalOnInitializedRestarter
*/
class OnInitializedRestarterCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
Restarter restarter = getRestarter();
if (restarter == null) {
return ConditionOutcome.noMatch("Restarter unavailable");
}
if (restarter.getInitialUrls() == null) {
return ConditionOutcome.noMatch("Restarter initialized without URLs");
}
return ConditionOutcome.match("Restarter available and initialized");
}
private Restarter getRestarter() {
try {
return Restarter.getInstance();
}
catch (Exception ex) {
return null;
}
}
}

View File

@ -0,0 +1,62 @@
/*
* 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.boot.context.event.ApplicationFailedEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.Ordered;
/**
* {@link ApplicationListener} to initialize the {@link Restarter}.
*
* @author Phillip Webb
* @since 1.3.0
* @see Restarter
*/
public class RestartApplicationListener implements ApplicationListener<ApplicationEvent>,
Ordered {
private int order = HIGHEST_PRECEDENCE;
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationStartedEvent) {
Restarter.initialize(((ApplicationStartedEvent) event).getArgs());
}
if (event instanceof ApplicationReadyEvent
|| event instanceof ApplicationFailedEvent) {
Restarter.getInstance().finish();
}
}
@Override
public int getOrder() {
return this.order;
}
/**
* Set the order of the listener.
* @param order the order of the listener
*/
public void setOrder(int order) {
this.order = order;
}
}

View File

@ -0,0 +1,50 @@
/*
* 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.net.URL;
/**
* Strategy interface used to initialize a {@link Restarter}.
*
* @author Phillip Webb
* @since 1.3.0
* @see DefaultRestartInitializer
*/
public interface RestartInitializer {
/**
* {@link RestartInitializer} that doesn't return any URLs.
*/
public static final RestartInitializer NONE = new RestartInitializer() {
@Override
public URL[] getInitialUrls(Thread thread) {
return null;
}
};
/**
* Return the initial set of URLs for the {@link Restarter} or {@code null} if no
* initial restart is required.
* @param thread the source thread
* @return initial URLs or {@code null}
*/
URL[] getInitialUrls(Thread thread);
}

View File

@ -0,0 +1,54 @@
/*
* 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.reflect.Method;
/**
* Thread used to launch a restarted application.
*
* @author Phillip Webb
*/
class RestartLauncher extends Thread {
private final String mainClassName;
private final String[] args;
public RestartLauncher(ClassLoader classLoader, String mainClassName, String[] args,
UncaughtExceptionHandler exceptionHandler) {
this.mainClassName = mainClassName;
this.args = args;
setName("restartedMain");
setUncaughtExceptionHandler(exceptionHandler);
setDaemon(false);
setContextClassLoader(classLoader);
}
@Override
public void run() {
try {
Class<?> mainClass = getContextClassLoader().loadClass(this.mainClassName);
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { this.args });
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
}

View File

@ -0,0 +1,519 @@
/*
* 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.beans.Introspector;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.Callable;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.locks.Lock;
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.boot.SpringApplication;
import org.springframework.boot.developertools.restart.classloader.RestartClassLoader;
import org.springframework.boot.logging.DeferredLog;
import org.springframework.cglib.core.ClassNameReader;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* Allows a running application to be restarted with an updated classpath. The restarter
* works by creating a new application ClassLoader that is split into two parts. The top
* part contains static URLs that don't change (for example 3rd party libraries and Spring
* Boot itself) and the bottom part contains URLs where classes and resources might be
* updated.
* <p>
* The Restarter should be {@link #initialize(String[]) initialized} early to ensure that
* classes are loaded multiple times. Mostly the {@link RestartApplicationListener} can be
* relied upon to perform initialization, however, you may need to call
* {@link #initialize(String[])} directly if your SpringApplication arguments are not
* identical to your main method arguments.
* <p>
* By default, applications running in an IDE (i.e. those not packaged as "fat jars") will
* automatically detect URLs that can change. It's also possible to manually configure
* URLs or class file updates for remote restart scenarios.
*
* @author Phillip Webb
* @since 1.3.0
* @see RestartApplicationListener
* @see #initialize(String[])
* @see #getInstance()
* @see #restart()
*/
public class Restarter {
private static Restarter instance;
private Log logger = new DeferredLog();
private final boolean forceReferenceCleanup;
private URL[] urls;
private final String mainClassName;
private final ClassLoader applicationClassLoader;
private final String[] args;
private final UncaughtExceptionHandler exceptionHandler;
private final BlockingDeque<LeakSafeThread> leakSafeThreads = new LinkedBlockingDeque<LeakSafeThread>();
private boolean finished = false;
private Lock stopLock = new ReentrantLock();
/**
* Internal constructor to create a new {@link Restarter} instance.
* @param thread the source thread
* @param args the application arguments
* @param forceReferenceCleanup if soft/weak reference cleanup should be forced
* @param initializer
* @see #initialize(String[])
*/
protected Restarter(Thread thread, String[] args, boolean forceReferenceCleanup,
RestartInitializer initializer) {
Assert.notNull(thread, "Thread must not be null");
Assert.notNull(args, "Args must not be null");
Assert.notNull(initializer, "Initializer must not be null");
this.logger.debug("Creating new Restarter for thread " + thread);
SilentExitExceptionHandler.setup(thread);
this.forceReferenceCleanup = forceReferenceCleanup;
this.urls = initializer.getInitialUrls(thread);
this.mainClassName = getMainClassName(thread);
this.applicationClassLoader = thread.getContextClassLoader();
this.args = args;
this.exceptionHandler = thread.getUncaughtExceptionHandler();
this.leakSafeThreads.add(new LeakSafeThread());
}
private String getMainClassName(Thread thread) {
try {
return new MainMethod(thread).getDeclaringClassName();
}
catch (Exception ex) {
return null;
}
}
protected void initialize(boolean restartOnInitialize) {
preInitializeLeakyClasses();
if (this.urls != null) {
if (restartOnInitialize) {
this.logger.debug("Immediately restarting application");
immediateRestart();
}
}
}
private void immediateRestart() {
try {
getLeakSafeThread().callAndWait(new Callable<Void>() {
@Override
public Void call() throws Exception {
start();
return null;
}
});
}
catch (Exception ex) {
this.logger.warn("Unable to initialize restarter", ex);
}
SilentExitExceptionHandler.exitCurrentThread();
}
/**
* CGLIB has a private exception field which needs to initialized early to ensure that
* the stacktrace doesn't retain a reference to the RestartClassLoader.
*/
private void preInitializeLeakyClasses() {
try {
Class<?> readerClass = ClassNameReader.class;
Field field = readerClass.getDeclaredField("EARLY_EXIT");
field.setAccessible(true);
((Throwable) field.get(null)).fillInStackTrace();
}
catch (Exception ex) {
this.logger.warn("Unable to pre-initialize classes", ex);
}
}
/**
* Return a {@link ThreadFactory} that can be used to create leak safe threads.
* @return a leak safe thread factory
*/
public ThreadFactory getThreadFactory() {
return new LeakSafeThreadFactory();
}
/**
* Restart the running application.
*/
public void restart() {
this.logger.debug("Restarting application");
getLeakSafeThread().call(new Callable<Void>() {
@Override
public Void call() throws Exception {
Restarter.this.stop();
Restarter.this.start();
return null;
}
});
}
/**
* Start the application.
* @throws Exception
*/
protected void start() throws Exception {
Assert.notNull(this.mainClassName, "Unable to find the main class to restart");
ClassLoader parent = this.applicationClassLoader;
ClassLoader classLoader = new RestartClassLoader(parent, this.urls, this.logger);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Starting application " + this.mainClassName
+ " with URLs " + Arrays.asList(this.urls));
}
relaunch(classLoader);
}
/**
* Relaunch the application using the specified classloader.
* @param classLoader the classloader to use
* @throws Exception
*/
protected void relaunch(ClassLoader classLoader) throws Exception {
RestartLauncher launcher = new RestartLauncher(classLoader, this.mainClassName,
this.args, this.exceptionHandler);
launcher.start();
launcher.join();
}
/**
* Stop the application.
* @throws Exception
*/
protected void stop() throws Exception {
this.logger.debug("Stopping application");
this.stopLock.lock();
try {
triggerShutdownHooks();
cleanupCaches();
if (this.forceReferenceCleanup) {
forceReferenceCleanup();
}
}
finally {
this.stopLock.unlock();
}
System.gc();
System.runFinalization();
}
@SuppressWarnings("rawtypes")
private void triggerShutdownHooks() throws Exception {
Class<?> hooksClass = Class.forName("java.lang.ApplicationShutdownHooks");
Method runHooks = hooksClass.getDeclaredMethod("runHooks");
runHooks.setAccessible(true);
runHooks.invoke(null);
Field field = hooksClass.getDeclaredField("hooks");
field.setAccessible(true);
field.set(null, new IdentityHashMap());
}
private void cleanupCaches() throws Exception {
Introspector.flushCaches();
cleanupKnownCaches();
}
private void cleanupKnownCaches() throws Exception {
// Whilst not strictly necessary it helps to cleanup soft reference caches
// early rather than waiting for memory limits to be reached
clear(ResolvableType.class, "cache");
clear("org.springframework.core.SerializableTypeWrapper", "cache");
clear(CachedIntrospectionResults.class, "acceptedClassLoaders");
clear(CachedIntrospectionResults.class, "strongClassCache");
clear(CachedIntrospectionResults.class, "softClassCache");
clear(ReflectionUtils.class, "declaredFieldsCache");
clear(ReflectionUtils.class, "declaredMethodsCache");
clear(AnnotationUtils.class, "findAnnotationCache");
clear(AnnotationUtils.class, "annotatedInterfaceCache");
clear("com.sun.naming.internal.ResourceManager", "propertiesCache");
}
private void clear(String className, String fieldName) {
try {
clear(Class.forName(className), fieldName);
}
catch (Exception ex) {
this.logger.debug("Unable to clear field " + className + " " + fieldName, ex);
}
}
private void clear(Class<?> type, String fieldName) throws Exception {
Field field = type.getDeclaredField(fieldName);
field.setAccessible(true);
Object instance = field.get(null);
if (instance instanceof Set) {
((Set<?>) instance).clear();
}
if (instance instanceof Map) {
Map<?, ?> map = ((Map<?, ?>) instance);
for (Iterator<?> iterator = map.keySet().iterator(); iterator.hasNext();) {
Object value = iterator.next();
if (value instanceof Class
&& ((Class<?>) value).getClassLoader() instanceof RestartClassLoader) {
iterator.remove();
}
}
}
}
/**
* Cleanup any soft/weak references by forcing an {@link OutOfMemoryError} error.
*/
private void forceReferenceCleanup() {
try {
final List<long[]> memory = new LinkedList<long[]>();
while (true) {
memory.add(new long[102400]);
}
}
catch (final OutOfMemoryError ex) {
}
}
/**
* Called to finish {@link Restarter} initialization when application logging is
* available.
*/
synchronized void finish() {
if (!isFinished()) {
this.logger = DeferredLog.replay(this.logger, LogFactory.getLog(getClass()));
this.finished = true;
}
}
boolean isFinished() {
return this.finished;
}
private LeakSafeThread getLeakSafeThread() {
try {
return this.leakSafeThreads.takeFirst();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException(ex);
}
}
/**
* Return the initial set of URLs as configured by the {@link RestartInitializer}.
* @return the initial URLs or {@code null}
*/
public URL[] getInitialUrls() {
return this.urls;
}
/**
* Initialize restart support. See
* {@link #initialize(String[], boolean, RestartInitializer)} for details.
* @param args main application arguments
* @see #initialize(String[], boolean, RestartInitializer)
*/
public static void initialize(String[] args) {
initialize(args, false, new DefaultRestartInitializer());
}
/**
* Initialize restart support. See
* {@link #initialize(String[], boolean, RestartInitializer)} for details.
* @param args main application arguments
* @param initializer the restart initializer
* @see #initialize(String[], boolean, RestartInitializer)
*/
public static void initialize(String[] args, RestartInitializer initializer) {
initialize(args, false, initializer, true);
}
/**
* Initialize restart support. See
* {@link #initialize(String[], boolean, RestartInitializer)} for details.
* @param args main application arguments
* @param forceReferenceCleanup if forcing of soft/weak reference should happen on
* @see #initialize(String[], boolean, RestartInitializer)
*/
public static void initialize(String[] args, boolean forceReferenceCleanup) {
initialize(args, forceReferenceCleanup, new DefaultRestartInitializer());
}
/**
* Initialize restart support. See
* {@link #initialize(String[], boolean, RestartInitializer)} for details.
* @param args main application arguments
* @param forceReferenceCleanup if forcing of soft/weak reference should happen on
* @param initializer the restart initializer
* @see #initialize(String[], boolean, RestartInitializer)
*/
public static void initialize(String[] args, boolean forceReferenceCleanup,
RestartInitializer initializer) {
initialize(args, forceReferenceCleanup, initializer, true);
}
/**
* Initialize restart support for the current application. Called automatically by
* {@link RestartApplicationListener} but can also be called directly if main
* application arguments are not the same as those passed to the
* {@link SpringApplication}.
* @param args main application arguments
* @param forceReferenceCleanup if forcing of soft/weak reference should happen on
* each restart. This will slow down restarts and is intended primarily for testing
* @param initializer the restart initializer
* @param restartOnInitialize if the restarter should be restarted immediately when
* the {@link RestartInitializer} returns non {@code null} results
*/
public static void initialize(String[] args, boolean forceReferenceCleanup,
RestartInitializer initializer, boolean restartOnInitialize) {
if (instance == null) {
synchronized (Restarter.class) {
instance = new Restarter(Thread.currentThread(), args,
forceReferenceCleanup, initializer);
}
instance.initialize(restartOnInitialize);
}
}
/**
* Return the active {@link Restarter} instance. Cannot be called before
* {@link #initialize(String[]) initialization}.
* @return the restarter
*/
public synchronized static Restarter getInstance() {
Assert.state(instance != null, "Restarter has not been initialized");
return instance;
}
/**
* Set the restarter instance (useful for testing).
* @param instance the instance to set
*/
final static void setInstance(Restarter instance) {
Restarter.instance = instance;
}
/**
* Clear the instance. Primarily provided for tests and not usually used in
* application code.
*/
public static void clearInstance() {
instance = null;
}
/**
* Thread that is created early so not to retain the {@link RestartClassLoader}.
*/
private class LeakSafeThread extends Thread {
private Callable<?> callable;
private Object result;
public LeakSafeThread() {
setDaemon(false);
}
public void call(Callable<?> callable) {
this.callable = callable;
start();
}
@SuppressWarnings("unchecked")
public <V> V callAndWait(Callable<V> callable) {
this.callable = callable;
start();
try {
join();
return (V) this.result;
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException(ex);
}
}
@Override
public void run() {
// We are safe to refresh the ActionThread (and indirectly call
// AccessController.getContext()) since our stack doesn't include the
// RestartClassLoader
try {
Restarter.this.leakSafeThreads.put(new LeakSafeThread());
this.result = this.callable.call();
}
catch (Exception ex) {
ex.printStackTrace();
System.exit(1);
}
}
}
/**
* {@link ThreadFactory} that creates a leak safe thead.
*/
private class LeakSafeThreadFactory implements ThreadFactory {
@Override
public Thread newThread(final Runnable runnable) {
return getLeakSafeThread().callAndWait(new Callable<Thread>() {
@Override
public Thread call() throws Exception {
Thread thread = new Thread(runnable);
thread.setContextClassLoader(Restarter.this.applicationClassLoader);
return thread;
}
});
}
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright 2012-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.boot.developertools.restart;
import java.lang.Thread.UncaughtExceptionHandler;
/**
* {@link UncaughtExceptionHandler} decorator that allows a thread to exit silently.
*
* @author Phillip Webb
*/
class SilentExitExceptionHandler implements UncaughtExceptionHandler {
private final UncaughtExceptionHandler delegate;
public SilentExitExceptionHandler(UncaughtExceptionHandler delegate) {
this.delegate = delegate;
}
@Override
public void uncaughtException(Thread thread, Throwable exception) {
if (exception instanceof SilentExitException) {
return;
}
if (this.delegate != null) {
this.delegate.uncaughtException(thread, exception);
}
}
public static void setup(Thread thread) {
UncaughtExceptionHandler handler = thread.getUncaughtExceptionHandler();
if (!(handler instanceof SilentExitExceptionHandler)) {
handler = new SilentExitExceptionHandler(handler);
thread.setUncaughtExceptionHandler(handler);
}
}
public static void exitCurrentThread() {
throw new SilentExitException();
}
private static class SilentExitException extends RuntimeException {
}
}

View File

@ -0,0 +1,21 @@
/*
* 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.
*/
/**
* Application restart support
*/
package org.springframework.boot.developertools.restart;

View File

@ -1,3 +1,7 @@
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.developertools.restart.RestartApplicationListener
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.developertools.autoconfigure.LocalDeveloperToolsAutoConfiguration

View File

@ -26,6 +26,9 @@ import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
import org.springframework.boot.developertools.restart.MockRestartInitializer;
import org.springframework.boot.developertools.restart.MockRestarter;
import org.springframework.boot.developertools.restart.Restarter;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@ -45,6 +48,9 @@ public class LocalDeveloperToolsAutoConfigurationTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public MockRestarter mockRestarter = new MockRestarter();
private int liveReloadPort = SocketUtils.findAvailableTcpPort();
private ConfigurableApplicationContext context;
@ -70,6 +76,7 @@ public class LocalDeveloperToolsAutoConfigurationTests {
private ConfigurableApplicationContext initializeAndRun(Class<?> config,
Map<String, Object> properties) {
Restarter.initialize(new String[0], false, new MockRestartInitializer(), false);
SpringApplication application = new SpringApplication(config);
application.setDefaultProperties(getDefaultProperties(properties));
application.setWebEnvironment(false);

View File

@ -0,0 +1,76 @@
/*
* 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.io.File;
import java.io.IOException;
import java.net.URL;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link ChangeableUrls}.
*
* @author Phillip Webb
*/
public class ChangeableUrlsTests {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void folderUrl() throws Exception {
URL url = makeUrl("myproject");
assertThat(ChangeableUrls.fromUrls(url).size(), equalTo(1));
}
@Test
public void fileUrl() throws Exception {
URL url = this.temporaryFolder.newFile().toURI().toURL();
assertThat(ChangeableUrls.fromUrls(url).size(), equalTo(0));
}
@Test
public void httpUrl() throws Exception {
URL url = new URL("http://spring.io");
assertThat(ChangeableUrls.fromUrls(url).size(), equalTo(0));
}
@Test
public void skipsUrls() throws Exception {
ChangeableUrls urls = ChangeableUrls
.fromUrls(makeUrl("spring-boot"), makeUrl("spring-boot-autoconfigure"),
makeUrl("spring-boot-actuator"), makeUrl("spring-boot-starter"),
makeUrl("spring-boot-starter-some-thing"));
assertThat(urls.size(), equalTo(0));
}
private URL makeUrl(String name) throws IOException {
File file = this.temporaryFolder.newFolder();
file = new File(file, name);
file = new File(file, "target");
file = new File(file, "classes");
file.mkdirs();
return file.toURI().toURL();
}
}

View File

@ -0,0 +1,130 @@
/*
* 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.net.URL;
import org.junit.Test;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link DefaultRestartInitializer}.
*
* @author Phillip Webb
*/
public class DefaultRestartInitializerTests {
@Test
public void nullForTests() throws Exception {
MockRestartInitializer initializer = new MockRestartInitializer(true);
assertThat(initializer.getInitialUrls(Thread.currentThread()), nullValue());
}
@Test
public void validMainThread() throws Exception {
MockRestartInitializer initializer = new MockRestartInitializer(false);
ClassLoader classLoader = new MockAppClassLoader(getClass().getClassLoader());
Thread thread = new Thread();
thread.setName("main");
thread.setContextClassLoader(classLoader);
assertThat(initializer.isMain(thread), equalTo(true));
assertThat(initializer.getInitialUrls(thread), not(nullValue()));
}
@Test
public void threadNotNamedMain() throws Exception {
MockRestartInitializer initializer = new MockRestartInitializer(false);
ClassLoader classLoader = new MockAppClassLoader(getClass().getClassLoader());
Thread thread = new Thread();
thread.setName("buscuit");
thread.setContextClassLoader(classLoader);
assertThat(initializer.isMain(thread), equalTo(false));
assertThat(initializer.getInitialUrls(thread), nullValue());
}
@Test
public void threadNotUsingAppClassLoader() throws Exception {
MockRestartInitializer initializer = new MockRestartInitializer(false);
ClassLoader classLoader = new MockLauncherClassLoader(getClass().getClassLoader());
Thread thread = new Thread();
thread.setName("main");
thread.setContextClassLoader(classLoader);
assertThat(initializer.isMain(thread), equalTo(false));
assertThat(initializer.getInitialUrls(thread), nullValue());
}
@Test
public void skipsDueToJUnitStacks() throws Exception {
testSkipStack("org.junit.runners.Something", true);
}
@Test
public void skipsDueToSpringTest() throws Exception {
testSkipStack("org.springframework.boot.test.Something", true);
}
private void testSkipStack(String className, boolean expected) {
MockRestartInitializer initializer = new MockRestartInitializer(true);
StackTraceElement element = new StackTraceElement(className, "someMethod",
"someFile", 123);
assertThat(initializer.isSkippedStackElement(element), equalTo(expected));
}
private static class MockAppClassLoader extends ClassLoader {
public MockAppClassLoader(ClassLoader parent) {
super(parent);
}
}
private static class MockLauncherClassLoader extends ClassLoader {
public MockLauncherClassLoader(ClassLoader parent) {
super(parent);
}
}
private static class MockRestartInitializer extends DefaultRestartInitializer {
private final boolean considerStackElements;
public MockRestartInitializer(boolean considerStackElements) {
this.considerStackElements = considerStackElements;
}
@Override
protected boolean isSkippedStackElement(StackTraceElement element) {
if (!this.considerStackElements) {
return false;
}
return true;
}
@Override
protected URL[] getUrls(Thread thread) {
return new URL[0];
}
}
}

View File

@ -0,0 +1,155 @@
/*
* 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.reflect.Method;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.util.ReflectionUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link MainMethod}.
*
* @author Phillip Webb
*/
public class MainMethodTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private static ThreadLocal<MainMethod> mainMethod = new ThreadLocal<MainMethod>();
private Method actualMain;
@Before
public void setup() throws Exception {
this.actualMain = Valid.class.getMethod("main", String[].class);
}
@Test
public void threadMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Thread must not be null");
new MainMethod(null);
}
@Test
public void validMainMethod() throws Exception {
MainMethod method = new TestThread(new Runnable() {
@Override
public void run() {
Valid.main();
}
}).test();
assertThat(method.getMethod(), equalTo(this.actualMain));
assertThat(method.getDeclaringClassName(), equalTo(this.actualMain
.getDeclaringClass().getName()));
}
@Test
public void missingArgsMainMethod() throws Exception {
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("Unable to find main method");
new TestThread(new Runnable() {
@Override
public void run() {
MissingArgs.main();
}
}).test();
}
@Test
public void nonStatic() throws Exception {
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("Unable to find main method");
new TestThread(new Runnable() {
@Override
public void run() {
new NonStaticMain().main();
}
}).test();
}
private static class TestThread extends Thread {
private final Runnable runnable;
private Exception exception;
private MainMethod mainMethod;
public TestThread(Runnable runnable) {
this.runnable = runnable;
}
public MainMethod test() throws InterruptedException {
start();
join();
if (this.exception != null) {
ReflectionUtils.rethrowRuntimeException(this.exception);
}
return this.mainMethod;
}
@Override
public void run() {
try {
this.runnable.run();
this.mainMethod = MainMethodTests.mainMethod.get();
}
catch (Exception ex) {
this.exception = ex;
}
}
}
public static class Valid {
public static void main(String... args) {
someOtherMethod();
}
private static void someOtherMethod() {
mainMethod.set(new MainMethod());
}
}
public static class MissingArgs {
public static void main() {
mainMethod.set(new MainMethod());
}
}
private static class NonStaticMain {
public void main(String... args) {
mainMethod.set(new MainMethod());
}
}
}

View File

@ -0,0 +1,35 @@
/*
* 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.net.URL;
import org.springframework.boot.developertools.restart.RestartInitializer;
/**
* Simple mock {@link RestartInitializer} that returns an empty array of URLs.
*
* @author Phillip Webb
*/
public class MockRestartInitializer implements RestartInitializer {
@Override
public URL[] getInitialUrls(Thread thread) {
return new URL[] {};
}
}

View File

@ -0,0 +1,78 @@
/*
* 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.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ThreadFactory;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Mocked version of {@link Restarter}.
*
* @author Phillip Webb
*/
public class MockRestarter implements TestRule {
private Map<String, Object> attributes = new HashMap<String, Object>();
private Restarter mock = mock(Restarter.class);
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
setup();
base.evaluate();
cleanup();
}
};
}
private void setup() {
Restarter.setInstance(this.mock);
given(this.mock.getInitialUrls()).willReturn(new URL[] {});
given(this.mock.getThreadFactory()).willReturn(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r);
}
});
}
private void cleanup() {
this.attributes.clear();
Restarter.clearInstance();
}
public Restarter getMock() {
return this.mock;
}
}

View File

@ -0,0 +1,112 @@
/*
* 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.net.URL;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link OnInitializedRestarterCondition}.
*
* @author Phillip Webb
*/
public class OnInitializedRestarterConditionTests {
private static Object wait = new Object();
@Before
@After
public void cleanup() {
Restarter.clearInstance();
}
@Test
public void noInstance() throws Exception {
Restarter.clearInstance();
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
Config.class);
assertThat(context.containsBean("bean"), equalTo(false));
context.close();
}
@Test
public void noInitialization() throws Exception {
Restarter.initialize(new String[0], false, RestartInitializer.NONE);
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
Config.class);
assertThat(context.containsBean("bean"), equalTo(false));
context.close();
}
@Test
public void initialized() throws Exception {
Thread thread = new Thread() {
@Override
public void run() {
TestInitialized.main();
};
};
thread.start();
synchronized (wait) {
wait.wait();
}
}
public static class TestInitialized {
public static void main(String... args) {
RestartInitializer initializer = mock(RestartInitializer.class);
given(initializer.getInitialUrls((Thread) any())).willReturn(new URL[0]);
Restarter.initialize(new String[0], false, initializer);
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
Config.class);
assertThat(context.containsBean("bean"), equalTo(true));
context.close();
synchronized (wait) {
wait.notify();
}
}
}
@Configuration
public static class Config {
@Bean
@ConditionalOnInitializedRestarter
public String bean() {
return "bean";
}
}
}

View File

@ -0,0 +1,87 @@
/*
* 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.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.test.util.ReflectionTestUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RestartApplicationListener}.
*
* @author Phillip Webb
*/
public class RestartApplicationListenerTests {
@Before
@After
public void cleanup() {
Restarter.clearInstance();
}
@Test
public void isHighestPriority() throws Exception {
assertThat(new RestartApplicationListener().getOrder(),
equalTo(Ordered.HIGHEST_PRECEDENCE));
}
@Test
public void initializeWithReady() throws Exception {
testInitialize(false);
}
@Test
public void initializeWithFail() throws Exception {
testInitialize(true);
}
private void testInitialize(boolean failed) {
Restarter.clearInstance();
RestartApplicationListener listener = new RestartApplicationListener();
SpringApplication application = new SpringApplication();
ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class);
String[] args = new String[] { "a", "b", "c" };
listener.onApplicationEvent(new ApplicationStartedEvent(application, args));
assertThat(Restarter.getInstance(), not(nullValue()));
assertThat(Restarter.getInstance().isFinished(), equalTo(false));
assertThat(ReflectionTestUtils.getField(Restarter.getInstance(), "args"),
equalTo((Object) args));
if (failed) {
listener.onApplicationEvent(new ApplicationFailedEvent(application, args,
context, new RuntimeException()));
}
else {
listener.onApplicationEvent(new ApplicationReadyEvent(application, args,
context));
}
assertThat(Restarter.getInstance().isFinished(), equalTo(true));
}
}

View File

@ -0,0 +1,194 @@
/*
* 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.net.URL;
import java.net.URLClassLoader;
import java.util.concurrent.ThreadFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.test.OutputCapture;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link Restarter}.
*
* @author Phillip Webb
*/
public class RestarterTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public OutputCapture out = new OutputCapture();
@Before
public void setup() {
Restarter.setInstance(new TestableRestarter());
}
@After
public void cleanup() {
Restarter.clearInstance();
}
@Test
public void cantGetInstanceBeforeInitialize() throws Exception {
Restarter.clearInstance();
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("Restarter has not been initialized");
Restarter.getInstance();
}
@Test
public void testRestart() throws Exception {
Restarter.clearInstance();
Thread thread = new Thread() {
@Override
public void run() {
SampleApplication.main();
};
};
thread.start();
Thread.sleep(1600);
String output = this.out.toString();
assertThat(StringUtils.countOccurrencesOf(output, "Tick 0"), greaterThan(2));
assertThat(StringUtils.countOccurrencesOf(output, "Tick 1"), greaterThan(2));
}
@Test
public void getThreadFactory() throws Exception {
final ClassLoader parentLoader = Thread.currentThread().getContextClassLoader();
final ClassLoader contextClassLoader = new URLClassLoader(new URL[0]);
Thread thread = new Thread() {
@Override
public void run() {
Runnable runnable = mock(Runnable.class);
Thread regular = new Thread();
ThreadFactory factory = Restarter.getInstance().getThreadFactory();
Thread viaFactory = factory.newThread(runnable);
// Regular threads will inherit the current thread
assertThat(regular.getContextClassLoader(), equalTo(contextClassLoader));
// Factory threads should should inherit from the initial thread
assertThat(viaFactory.getContextClassLoader(), equalTo(parentLoader));
};
};
thread.setContextClassLoader(contextClassLoader);
thread.start();
thread.join();
}
@Test
public void getInitialUrls() throws Exception {
Restarter.clearInstance();
RestartInitializer initializer = mock(RestartInitializer.class);
URL[] urls = new URL[] { new URL("file:/proj/module-a.jar!/") };
given(initializer.getInitialUrls(any(Thread.class))).willReturn(urls);
Restarter.initialize(new String[0], false, initializer, false);
assertThat(Restarter.getInstance().getInitialUrls(), equalTo(urls));
}
@Component
@EnableScheduling
public static class SampleApplication {
private int count = 0;
private static volatile boolean quit = false;
@Scheduled(fixedDelay = 100)
public void tickBean() {
System.out.println("Tick " + this.count++ + " " + Thread.currentThread());
}
@Scheduled(initialDelay = 350, fixedDelay = 350)
public void restart() {
System.out.println("Restart " + Thread.currentThread());
if (!SampleApplication.quit) {
Restarter.getInstance().restart();
}
}
public static void main(String... args) {
Restarter.initialize(args, false, new MockRestartInitializer());
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
SampleApplication.class);
context.registerShutdownHook();
System.out.println("Sleep " + Thread.currentThread());
sleep();
quit = true;
context.close();
}
private static void sleep() {
try {
Thread.sleep(1200);
}
catch (InterruptedException ex) {
}
}
}
private static class TestableRestarter extends Restarter {
public TestableRestarter() {
this(Thread.currentThread(), new String[] {}, false,
new MockRestartInitializer());
}
protected TestableRestarter(Thread thread, String[] args,
boolean forceReferenceCleanup, RestartInitializer initializer) {
super(thread, args, forceReferenceCleanup, initializer);
}
@Override
public void restart() {
try {
stop();
start();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
@Override
protected void stop() throws Exception {
}
}
}

View File

@ -0,0 +1,84 @@
/*
* 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.junit.Test;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
/**
* Tests for {@link SilentExitExceptionHandler}.
*
* @author Phillip Webb
*/
public class SilentExitExceptionHandlerTests {
@Test
public void setupAndExit() throws Exception {
TestThread testThread = new TestThread() {
@Override
public void run() {
SilentExitExceptionHandler.exitCurrentThread();
fail("Didn't exit");
}
};
SilentExitExceptionHandler.setup(testThread);
testThread.startAndJoin();
assertThat(testThread.getThrown(), nullValue());
}
@Test
public void doesntInterferWithOtherExceptions() throws Exception {
TestThread testThread = new TestThread() {
@Override
public void run() {
throw new IllegalStateException("Expected");
}
};
SilentExitExceptionHandler.setup(testThread);
testThread.startAndJoin();
assertThat(testThread.getThrown().getMessage(), equalTo("Expected"));
}
private static abstract class TestThread extends Thread {
private Throwable thrown;
public TestThread() {
setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
TestThread.this.thrown = e;
}
});
}
public Throwable getThrown() {
return this.thrown;
}
public void startAndJoin() throws InterruptedException {
start();
join();
}
}
}