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:
parent
da51785706
commit
a5c56ca482
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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[] {};
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue