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