From 099db11754afe0261fadcb2968e0228b6813009a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 11 Jun 2015 21:19:20 -0700 Subject: [PATCH] Don't quit application on restart failures Update `Restarter` to support a `FailureHandler` strategy that can be used to determine how to deal with errors. The `LocalDevToolsAutoConfiguration` now uses a strategy that doesn't quit the application, but instead continues to wait for further file changes. This helps make restart much more usable in situations where you accidentally break code. Fixes gh-3210 --- .../FileWatchingFailureHandler.java | 76 +++++++++++++++++++ .../LocalDevToolsAutoConfiguration.java | 24 ++++-- .../ClassPathFileChangeListener.java | 21 +++-- .../classpath/ClassPathFileSystemWatcher.java | 64 +++++----------- .../devtools/classpath/ClassPathFolders.java | 71 +++++++++++++++++ .../devtools/filewatch/FileSystemWatcher.java | 12 +++ .../filewatch/FileSystemWatcherFactory.java | 33 ++++++++ .../client/RemoteClientConfiguration.java | 16 +++- .../boot/devtools/restart/FailureHandler.java | 64 ++++++++++++++++ .../devtools/restart/RestartLauncher.java | 10 ++- .../boot/devtools/restart/Restarter.java | 38 ++++++++-- .../LocalDevToolsAutoConfigurationTests.java | 10 ++- .../ClassPathFileChangeListenerTests.java | 31 +++++--- .../ClassPathFileSystemWatcherTests.java | 26 +++++-- .../boot/devtools/restart/RestarterTests.java | 11 ++- 15 files changed, 419 insertions(+), 88 deletions(-) create mode 100644 spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/FileWatchingFailureHandler.java create mode 100644 spring-boot-devtools/src/main/java/org/springframework/boot/devtools/classpath/ClassPathFolders.java create mode 100644 spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcherFactory.java create mode 100644 spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/FailureHandler.java diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/FileWatchingFailureHandler.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/FileWatchingFailureHandler.java new file mode 100644 index 0000000000..5506274616 --- /dev/null +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/FileWatchingFailureHandler.java @@ -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.devtools.autoconfigure; + +import java.util.Set; +import java.util.concurrent.CountDownLatch; + +import org.springframework.boot.devtools.classpath.ClassPathFolders; +import org.springframework.boot.devtools.filewatch.ChangedFiles; +import org.springframework.boot.devtools.filewatch.FileChangeListener; +import org.springframework.boot.devtools.filewatch.FileSystemWatcher; +import org.springframework.boot.devtools.filewatch.FileSystemWatcherFactory; +import org.springframework.boot.devtools.restart.FailureHandler; +import org.springframework.boot.devtools.restart.Restarter; + +/** + * {@link FailureHandler} that waits for filesystem changes before retrying. + * + * @author Phillip Webb + */ +class FileWatchingFailureHandler implements FailureHandler { + + private final FileSystemWatcherFactory fileSystemWatcherFactory; + + public FileWatchingFailureHandler(FileSystemWatcherFactory fileSystemWatcherFactory) { + this.fileSystemWatcherFactory = fileSystemWatcherFactory; + } + + @Override + public Outcome handle(Throwable failure) { + failure.printStackTrace(); + CountDownLatch latch = new CountDownLatch(1); + FileSystemWatcher watcher = this.fileSystemWatcherFactory.getFileSystemWatcher(); + watcher.addSourceFolders(new ClassPathFolders(Restarter.getInstance() + .getInitialUrls())); + watcher.addListener(new Listener(latch)); + watcher.start(); + try { + latch.await(); + } + catch (InterruptedException ex) { + } + return Outcome.RETRY; + } + + private static class Listener implements FileChangeListener { + + private final CountDownLatch latch; + + public Listener(CountDownLatch latch) { + this.latch = latch; + } + + @Override + public void onChange(Set changeSet) { + System.out.println("Changes"); + this.latch.countDown(); + } + + } + +} diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfiguration.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfiguration.java index 2b17b542aa..f1002b92a7 100644 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfiguration.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfiguration.java @@ -29,6 +29,7 @@ import org.springframework.boot.devtools.classpath.ClassPathFileSystemWatcher; import org.springframework.boot.devtools.classpath.ClassPathRestartStrategy; import org.springframework.boot.devtools.classpath.PatternClassPathRestartStrategy; import org.springframework.boot.devtools.filewatch.FileSystemWatcher; +import org.springframework.boot.devtools.filewatch.FileSystemWatcherFactory; import org.springframework.boot.devtools.livereload.LiveReloadServer; import org.springframework.boot.devtools.restart.ConditionalOnInitializedRestarter; import org.springframework.boot.devtools.restart.RestartScope; @@ -114,8 +115,8 @@ public class LocalDevToolsAutoConfiguration { @EventListener public void onClassPathChanged(ClassPathChangedEvent event) { if (event.isRestartRequired()) { - getFileSystemWatcher().stop(); - Restarter.getInstance().restart(); + Restarter.getInstance().restart( + new FileWatchingFailureHandler(getFileSystemWatcherFactory())); } } @@ -123,8 +124,10 @@ public class LocalDevToolsAutoConfiguration { @ConditionalOnMissingBean public ClassPathFileSystemWatcher classPathFileSystemWatcher() { URL[] urls = Restarter.getInstance().getInitialUrls(); - return new ClassPathFileSystemWatcher(getFileSystemWatcher(), - classPathRestartStrategy(), urls); + ClassPathFileSystemWatcher watcher = new ClassPathFileSystemWatcher( + getFileSystemWatcherFactory(), classPathRestartStrategy(), urls); + watcher.setStopWatcherOnRestart(true); + return watcher; } @Bean @@ -135,7 +138,18 @@ public class LocalDevToolsAutoConfiguration { } @Bean - public FileSystemWatcher getFileSystemWatcher() { + public FileSystemWatcherFactory getFileSystemWatcherFactory() { + return new FileSystemWatcherFactory() { + + @Override + public FileSystemWatcher getFileSystemWatcher() { + return newFileSystemWatcher(); + } + + }; + } + + private FileSystemWatcher newFileSystemWatcher() { Restart restartProperties = this.properties.getRestart(); FileSystemWatcher watcher = new FileSystemWatcher(true, restartProperties.getPollInterval(), diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/classpath/ClassPathFileChangeListener.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/classpath/ClassPathFileChangeListener.java index f02ff2e80f..bc7e162cd4 100644 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/classpath/ClassPathFileChangeListener.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/classpath/ClassPathFileChangeListener.java @@ -21,7 +21,7 @@ import java.util.Set; import org.springframework.boot.devtools.filewatch.ChangedFile; import org.springframework.boot.devtools.filewatch.ChangedFiles; import org.springframework.boot.devtools.filewatch.FileChangeListener; -import org.springframework.context.ApplicationEvent; +import org.springframework.boot.devtools.filewatch.FileSystemWatcher; import org.springframework.context.ApplicationEventPublisher; import org.springframework.util.Assert; @@ -30,33 +30,44 @@ import org.springframework.util.Assert; * ClassPathChangedEvents}. * * @author Phillip Webb - * @since 1.3.0 * @see ClassPathFileSystemWatcher */ -public class ClassPathFileChangeListener implements FileChangeListener { +class ClassPathFileChangeListener implements FileChangeListener { private final ApplicationEventPublisher eventPublisher; private final ClassPathRestartStrategy restartStrategy; + private final FileSystemWatcher fileSystemWatcherToStop; + /** * Create a new {@link ClassPathFileChangeListener} instance. * @param eventPublisher the event publisher used send events * @param restartStrategy the restart strategy to use + * @param fileSystemWatcherToStop the file system watcher to stop on a restart (or + * {@code null}) */ public ClassPathFileChangeListener(ApplicationEventPublisher eventPublisher, - ClassPathRestartStrategy restartStrategy) { + ClassPathRestartStrategy restartStrategy, + FileSystemWatcher fileSystemWatcherToStop) { Assert.notNull(eventPublisher, "EventPublisher must not be null"); Assert.notNull(restartStrategy, "RestartStrategy must not be null"); this.eventPublisher = eventPublisher; this.restartStrategy = restartStrategy; + this.fileSystemWatcherToStop = fileSystemWatcherToStop; } @Override public void onChange(Set changeSet) { boolean restart = isRestartRequired(changeSet); - ApplicationEvent event = new ClassPathChangedEvent(this, changeSet, restart); + publishEvent(new ClassPathChangedEvent(this, changeSet, restart)); + } + + private void publishEvent(ClassPathChangedEvent event) { this.eventPublisher.publishEvent(event); + if (event.isRestartRequired() && this.fileSystemWatcherToStop != null) { + this.fileSystemWatcherToStop.stop(); + } } private boolean isRestartRequired(Set changeSet) { diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/classpath/ClassPathFileSystemWatcher.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/classpath/ClassPathFileSystemWatcher.java index a32d806636..4b7185f2ef 100644 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/classpath/ClassPathFileSystemWatcher.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/classpath/ClassPathFileSystemWatcher.java @@ -18,16 +18,14 @@ package org.springframework.boot.devtools.classpath; import java.net.URL; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.devtools.filewatch.FileSystemWatcher; +import org.springframework.boot.devtools.filewatch.FileSystemWatcherFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.util.Assert; -import org.springframework.util.ResourceUtils; /** * Encapsulates a {@link FileSystemWatcher} to watch the local classpath folders for @@ -40,63 +38,37 @@ import org.springframework.util.ResourceUtils; public class ClassPathFileSystemWatcher implements InitializingBean, DisposableBean, ApplicationContextAware { - private static final Log logger = LogFactory.getLog(ClassPathFileSystemWatcher.class); - private final FileSystemWatcher fileSystemWatcher; private ClassPathRestartStrategy restartStrategy; private ApplicationContext applicationContext; - /** - * Create a new {@link ClassPathFileSystemWatcher} instance. - * @param urls the classpath URLs to watch - */ - public ClassPathFileSystemWatcher(URL[] urls) { - this(new FileSystemWatcher(), null, urls); - } + private boolean stopWatcherOnRestart; /** * Create a new {@link ClassPathFileSystemWatcher} instance. + * @param fileSystemWatcherFactory the underlying {@link FileSystemWatcher} used to + * monitor the local file system * @param restartStrategy the classpath restart strategy * @param urls the URLs to watch */ - public ClassPathFileSystemWatcher(ClassPathRestartStrategy restartStrategy, URL[] urls) { - this(new FileSystemWatcher(), restartStrategy, urls); - } - - /** - * Create a new {@link ClassPathFileSystemWatcher} instance. - * @param fileSystemWatcher the underlying {@link FileSystemWatcher} used to monitor - * the local file system - * @param restartStrategy the classpath restart strategy - * @param urls the URLs to watch - */ - public ClassPathFileSystemWatcher(FileSystemWatcher fileSystemWatcher, + public ClassPathFileSystemWatcher(FileSystemWatcherFactory fileSystemWatcherFactory, ClassPathRestartStrategy restartStrategy, URL[] urls) { - Assert.notNull(fileSystemWatcher, "FileSystemWatcher must not be null"); + Assert.notNull(fileSystemWatcherFactory, + "FileSystemWatcherFactory must not be null"); Assert.notNull(urls, "Urls must not be null"); - this.fileSystemWatcher = fileSystemWatcher; + this.fileSystemWatcher = fileSystemWatcherFactory.getFileSystemWatcher(); this.restartStrategy = restartStrategy; - addUrls(urls); + this.fileSystemWatcher.addSourceFolders(new ClassPathFolders(urls)); } - private void addUrls(URL[] urls) { - for (URL url : urls) { - addUrl(url); - } - } - - private void addUrl(URL url) { - if (url.getProtocol().equals("file") && url.getPath().endsWith("/")) { - try { - this.fileSystemWatcher.addSourceFolder(ResourceUtils.getFile(url)); - } - catch (Exception ex) { - logger.warn("Unable to watch classpath URL " + url); - logger.trace("Unable to watch classpath URL " + url, ex); - } - } + /** + * Set if the {@link FileSystemWatcher} should be stopped when a full restart occurs. + * @param stopWatcherOnRestart if the watcher should be stopped when a restart occurs + */ + public void setStopWatcherOnRestart(boolean stopWatcherOnRestart) { + this.stopWatcherOnRestart = stopWatcherOnRestart; } @Override @@ -108,8 +80,12 @@ public class ClassPathFileSystemWatcher implements InitializingBean, DisposableB @Override public void afterPropertiesSet() throws Exception { if (this.restartStrategy != null) { + FileSystemWatcher watcherToStop = null; + if (this.stopWatcherOnRestart) { + watcherToStop = this.fileSystemWatcher; + } this.fileSystemWatcher.addListener(new ClassPathFileChangeListener( - this.applicationContext, this.restartStrategy)); + this.applicationContext, this.restartStrategy, watcherToStop)); } this.fileSystemWatcher.start(); } diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/classpath/ClassPathFolders.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/classpath/ClassPathFolders.java new file mode 100644 index 0000000000..c91ad2ed9f --- /dev/null +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/classpath/ClassPathFolders.java @@ -0,0 +1,71 @@ +/* + * 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.devtools.classpath; + +import java.io.File; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.ResourceUtils; + +/** + * Provides access to entries on the classpath that refer to folders. + * + * @author Phillip Webb + * @since 1.3.0 + */ +public class ClassPathFolders implements Iterable { + + private static final Log logger = LogFactory.getLog(ClassPathFolders.class); + + private final List folders = new ArrayList(); + + public ClassPathFolders(URL[] urls) { + if (urls != null) { + addUrls(urls); + } + } + + private void addUrls(URL[] urls) { + for (URL url : urls) { + addUrl(url); + } + } + + private void addUrl(URL url) { + if (url.getProtocol().equals("file") && url.getPath().endsWith("/")) { + try { + this.folders.add(ResourceUtils.getFile(url)); + } + catch (Exception ex) { + logger.warn("Unable to get classpath URL " + url); + logger.trace("Unable to get classpath URL " + url, ex); + } + } + } + + @Override + public Iterator iterator() { + return Collections.unmodifiableList(this.folders).iterator(); + } + +} diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java index 6a4547fe4b..fb41016844 100644 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java @@ -95,6 +95,18 @@ public class FileSystemWatcher { this.listeners.add(fileChangeListener); } + /** + * Add a source folders to monitor. Cannot be called after the watcher has been + * {@link #start() started}. + * @param folders the folders to monitor + */ + public void addSourceFolders(Iterable folders) { + Assert.notNull(folders, "Folders must not be null"); + for (File folder : folders) { + addSourceFolder(folder); + } + } + /** * Add a source folder to monitor. Cannot be called after the watcher has been * {@link #start() started}. diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcherFactory.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcherFactory.java new file mode 100644 index 0000000000..15971a425d --- /dev/null +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcherFactory.java @@ -0,0 +1,33 @@ +/* + * 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.devtools.filewatch; + +/** + * Factory used to create new {@link FileSystemWatcher} instances. + * + * @author Phillip Webb + * @since 1.3.0 + */ +public interface FileSystemWatcherFactory { + + /** + * Create a new {@link FileSystemWatcher}. + * @return a new {@link FileSystemWatcher} + */ + FileSystemWatcher getFileSystemWatcher(); + +} diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/RemoteClientConfiguration.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/RemoteClientConfiguration.java index fe2a7523e0..e103270251 100644 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/RemoteClientConfiguration.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/RemoteClientConfiguration.java @@ -44,6 +44,7 @@ import org.springframework.boot.devtools.classpath.ClassPathFileSystemWatcher; import org.springframework.boot.devtools.classpath.ClassPathRestartStrategy; import org.springframework.boot.devtools.classpath.PatternClassPathRestartStrategy; import org.springframework.boot.devtools.filewatch.FileSystemWatcher; +import org.springframework.boot.devtools.filewatch.FileSystemWatcherFactory; import org.springframework.boot.devtools.livereload.LiveReloadServer; import org.springframework.boot.devtools.restart.DefaultRestartInitializer; import org.springframework.boot.devtools.restart.RestartScope; @@ -188,12 +189,23 @@ public class RemoteClientConfiguration { if (urls == null) { urls = new URL[0]; } - return new ClassPathFileSystemWatcher(getFileSystemWather(), + return new ClassPathFileSystemWatcher(getFileSystemWatcherFactory(), classPathRestartStrategy(), urls); } @Bean - public FileSystemWatcher getFileSystemWather() { + public FileSystemWatcherFactory getFileSystemWatcherFactory() { + return new FileSystemWatcherFactory() { + + @Override + public FileSystemWatcher getFileSystemWatcher() { + return newFileSystemWatcher(); + } + + }; + } + + private FileSystemWatcher newFileSystemWatcher() { Restart restartProperties = this.properties.getRestart(); FileSystemWatcher watcher = new FileSystemWatcher(true, restartProperties.getPollInterval(), diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/FailureHandler.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/FailureHandler.java new file mode 100644 index 0000000000..53333fedad --- /dev/null +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/FailureHandler.java @@ -0,0 +1,64 @@ +/* + * 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.devtools.restart; + +/** + * Strategy used to handle launch failures. + * + * @author Phillip Webb + * @since 1.3.0 + */ +public interface FailureHandler { + + /** + * {@link FailureHandler} that always aborts. + */ + public static final FailureHandler NONE = new FailureHandler() { + + @Override + public Outcome handle(Throwable failure) { + return Outcome.ABORT; + } + + }; + + /** + * Handle a run failure. Implementations may block, for example to wait until specific + * files are updated. + * @param failure the exception + * @return the outcome + */ + Outcome handle(Throwable failure); + + /** + * Various outcomes for the handler. + */ + public static enum Outcome { + + /** + * Abort the relaunch. + */ + ABORT, + + /** + * Try again to relaunch the application. + */ + RETRY + + } + +} diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/RestartLauncher.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/RestartLauncher.java index 52f84ec8d0..ee9712be7d 100644 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/RestartLauncher.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/RestartLauncher.java @@ -29,6 +29,8 @@ class RestartLauncher extends Thread { private final String[] args; + private Throwable error; + public RestartLauncher(ClassLoader classLoader, String mainClassName, String[] args, UncaughtExceptionHandler exceptionHandler) { this.mainClassName = mainClassName; @@ -46,9 +48,13 @@ class RestartLauncher extends Thread { Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); mainMethod.invoke(null, new Object[] { this.args }); } - catch (Exception ex) { - throw new IllegalStateException(ex); + catch (Throwable ex) { + this.error = ex; } } + public Throwable getError() { + return this.error; + } + } diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java index f9d73a3901..becf57a800 100644 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java @@ -43,6 +43,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.CachedIntrospectionResults; import org.springframework.beans.factory.ObjectFactory; import org.springframework.boot.SpringApplication; +import org.springframework.boot.devtools.restart.FailureHandler.Outcome; import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles; import org.springframework.boot.devtools.restart.classloader.RestartClassLoader; import org.springframework.boot.logging.DeferredLog; @@ -160,7 +161,7 @@ public class Restarter { @Override public Void call() throws Exception { - start(); + start(FailureHandler.NONE); cleanupCaches(); return null; } @@ -227,6 +228,14 @@ public class Restarter { * Restart the running application. */ public void restart() { + restart(FailureHandler.NONE); + } + + /** + * Restart the running application. + * @param failureHandler a failure handler to deal with application that don't start + */ + public void restart(final FailureHandler failureHandler) { if (!this.enabled) { this.logger.debug("Application restart is disabled"); return; @@ -237,7 +246,7 @@ public class Restarter { @Override public Void call() throws Exception { Restarter.this.stop(); - Restarter.this.start(); + Restarter.this.start(failureHandler); return null; } @@ -246,9 +255,26 @@ public class Restarter { /** * Start the application. + * @param failureHandler a failure handler for application that wont start * @throws Exception */ - protected void start() throws Exception { + protected void start(FailureHandler failureHandler) throws Exception { + do { + Throwable error = doStart(); + if (error == null) { + return; + } + if (failureHandler.handle(error) == Outcome.ABORT) { + if (error instanceof Exception) { + throw (Exception) error; + } + throw new Exception(error); + } + } + while (true); + } + + private Throwable doStart() throws Exception { Assert.notNull(this.mainClassName, "Unable to find the main class to restart"); ClassLoader parent = this.applicationClassLoader; URL[] urls = this.urls.toArray(new URL[this.urls.size()]); @@ -259,19 +285,21 @@ public class Restarter { this.logger.debug("Starting application " + this.mainClassName + " with URLs " + Arrays.asList(urls)); } - relaunch(classLoader); + return relaunch(classLoader); } /** * Relaunch the application using the specified classloader. * @param classLoader the classloader to use + * @return any exception that caused the launch to fail or {@code null} * @throws Exception */ - protected void relaunch(ClassLoader classLoader) throws Exception { + protected Throwable relaunch(ClassLoader classLoader) throws Exception { RestartLauncher launcher = new RestartLauncher(classLoader, this.mainClassName, this.args, this.exceptionHandler); launcher.start(); launcher.join(); + return launcher.getError(); } /** diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfigurationTests.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfigurationTests.java index 1b2b5bf1be..5a5df39eaa 100644 --- a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfigurationTests.java +++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfigurationTests.java @@ -30,8 +30,8 @@ import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfigurati import org.springframework.boot.devtools.classpath.ClassPathChangedEvent; import org.springframework.boot.devtools.classpath.ClassPathFileSystemWatcher; import org.springframework.boot.devtools.filewatch.ChangedFiles; -import org.springframework.boot.devtools.filewatch.FileSystemWatcher; import org.springframework.boot.devtools.livereload.LiveReloadServer; +import org.springframework.boot.devtools.restart.FailureHandler; import org.springframework.boot.devtools.restart.MockRestartInitializer; import org.springframework.boot.devtools.restart.MockRestarter; import org.springframework.boot.devtools.restart.Restarter; @@ -48,6 +48,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -138,7 +139,7 @@ public class LocalDevToolsAutoConfigurationTests { ClassPathChangedEvent event = new ClassPathChangedEvent(this.context, Collections. emptySet(), true); this.context.publishEvent(event); - verify(this.mockRestarter.getMock()).restart(); + verify(this.mockRestarter.getMock()).restart(any(FailureHandler.class)); } @Test @@ -172,7 +173,10 @@ public class LocalDevToolsAutoConfigurationTests { Map properties = new HashMap(); properties.put("spring.devtools.restart.trigger-file", "somefile.txt"); this.context = initializeAndRun(Config.class, properties); - FileSystemWatcher watcher = this.context.getBean(FileSystemWatcher.class); + ClassPathFileSystemWatcher classPathWatcher = this.context + .getBean(ClassPathFileSystemWatcher.class); + Object watcher = ReflectionTestUtils.getField(classPathWatcher, + "fileSystemWatcher"); Object filter = ReflectionTestUtils.getField(watcher, "triggerFilter"); assertThat(filter, instanceOf(TriggerFileFilter.class)); } diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/classpath/ClassPathFileChangeListenerTests.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/classpath/ClassPathFileChangeListenerTests.java index d74467106b..656f5f1226 100644 --- a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/classpath/ClassPathFileChangeListenerTests.java +++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/classpath/ClassPathFileChangeListenerTests.java @@ -27,19 +27,18 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; import org.mockito.Captor; +import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.springframework.boot.devtools.classpath.ClassPathChangedEvent; -import org.springframework.boot.devtools.classpath.ClassPathFileChangeListener; -import org.springframework.boot.devtools.classpath.ClassPathRestartStrategy; import org.springframework.boot.devtools.filewatch.ChangedFile; import org.springframework.boot.devtools.filewatch.ChangedFiles; +import org.springframework.boot.devtools.filewatch.FileSystemWatcher; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; /** @@ -52,6 +51,15 @@ public class ClassPathFileChangeListenerTests { @Rule public ExpectedException thrown = ExpectedException.none(); + @Mock + private ApplicationEventPublisher eventPublisher; + + @Mock + private ClassPathRestartStrategy restartStrategy; + + @Mock + private FileSystemWatcher fileSystemWatcher; + @Captor private ArgumentCaptor eventCaptor; @@ -64,31 +72,32 @@ public class ClassPathFileChangeListenerTests { public void eventPublisherMustNotBeNull() throws Exception { this.thrown.expect(IllegalArgumentException.class); this.thrown.expectMessage("EventPublisher must not be null"); - new ClassPathFileChangeListener(null, mock(ClassPathRestartStrategy.class)); + new ClassPathFileChangeListener(null, this.restartStrategy, + this.fileSystemWatcher); } @Test public void restartStrategyMustNotBeNull() throws Exception { this.thrown.expect(IllegalArgumentException.class); this.thrown.expectMessage("RestartStrategy must not be null"); - new ClassPathFileChangeListener(mock(ApplicationEventPublisher.class), null); + new ClassPathFileChangeListener(this.eventPublisher, null, this.fileSystemWatcher); } @Test public void sendsEventWithoutRestart() throws Exception { testSendsEvent(false); + verify(this.fileSystemWatcher, never()).stop(); } @Test public void sendsEventWithRestart() throws Exception { testSendsEvent(true); + verify(this.fileSystemWatcher).stop(); } private void testSendsEvent(boolean restart) { - ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class); - ClassPathRestartStrategy restartStrategy = mock(ClassPathRestartStrategy.class); ClassPathFileChangeListener listener = new ClassPathFileChangeListener( - eventPublisher, restartStrategy); + this.eventPublisher, this.restartStrategy, this.fileSystemWatcher); File folder = new File("s1"); File file = new File("f1"); ChangedFile file1 = new ChangedFile(folder, file, ChangedFile.Type.ADD); @@ -99,10 +108,10 @@ public class ClassPathFileChangeListenerTests { ChangedFiles changedFiles = new ChangedFiles(new File("source"), files); Set changeSet = Collections.singleton(changedFiles); if (restart) { - given(restartStrategy.isRestartRequired(file2)).willReturn(true); + given(this.restartStrategy.isRestartRequired(file2)).willReturn(true); } listener.onChange(changeSet); - verify(eventPublisher).publishEvent(this.eventCaptor.capture()); + verify(this.eventPublisher).publishEvent(this.eventCaptor.capture()); ClassPathChangedEvent actualEvent = (ClassPathChangedEvent) this.eventCaptor .getValue(); assertThat(actualEvent.getChangeSet(), equalTo(changeSet)); diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/classpath/ClassPathFileSystemWatcherTests.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/classpath/ClassPathFileSystemWatcherTests.java index 8f1360f388..cac54d2700 100644 --- a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/classpath/ClassPathFileSystemWatcherTests.java +++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/classpath/ClassPathFileSystemWatcherTests.java @@ -28,11 +28,9 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.devtools.classpath.ClassPathChangedEvent; -import org.springframework.boot.devtools.classpath.ClassPathFileSystemWatcher; -import org.springframework.boot.devtools.classpath.ClassPathRestartStrategy; import org.springframework.boot.devtools.filewatch.ChangedFile; import org.springframework.boot.devtools.filewatch.FileSystemWatcher; +import org.springframework.boot.devtools.filewatch.FileSystemWatcherFactory; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -43,6 +41,7 @@ import org.springframework.util.FileCopyUtils; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link ClassPathFileSystemWatcher}. @@ -62,7 +61,8 @@ public class ClassPathFileSystemWatcherTests { this.thrown.expect(IllegalArgumentException.class); this.thrown.expectMessage("Urls must not be null"); URL[] urls = null; - new ClassPathFileSystemWatcher(urls); + new ClassPathFileSystemWatcher(mock(FileSystemWatcherFactory.class), + mock(ClassPathRestartStrategy.class), urls); } @Test @@ -99,7 +99,8 @@ public class ClassPathFileSystemWatcherTests { public ClassPathFileSystemWatcher watcher() { FileSystemWatcher watcher = new FileSystemWatcher(false, 100, 10); URL[] urls = this.environemnt.getProperty("urls", URL[].class); - return new ClassPathFileSystemWatcher(watcher, restartStrategy(), urls); + return new ClassPathFileSystemWatcher(new MockFileSystemWatcherFactory( + watcher), restartStrategy(), urls); } @Bean @@ -136,4 +137,19 @@ public class ClassPathFileSystemWatcherTests { } + private static class MockFileSystemWatcherFactory implements FileSystemWatcherFactory { + + private final FileSystemWatcher watcher; + + public MockFileSystemWatcherFactory(FileSystemWatcher watcher) { + this.watcher = watcher; + } + + @Override + public FileSystemWatcher getFileSystemWatcher() { + return this.watcher; + } + + } + } diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/RestarterTests.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/RestarterTests.java index ad84e6d687..a51bfc1911 100644 --- a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/RestarterTests.java +++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/RestarterTests.java @@ -29,11 +29,9 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectFactory; -import org.springframework.boot.devtools.restart.RestartInitializer; -import org.springframework.boot.devtools.restart.Restarter; import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile; -import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles; import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind; +import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles; import org.springframework.boot.test.OutputCapture; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.scheduling.annotation.EnableScheduling; @@ -250,10 +248,10 @@ public class RestarterTests { } @Override - public void restart() { + public void restart(FailureHandler failureHandler) { try { stop(); - start(); + start(failureHandler); } catch (Exception ex) { throw new IllegalStateException(ex); @@ -261,8 +259,9 @@ public class RestarterTests { } @Override - protected void relaunch(ClassLoader classLoader) throws Exception { + protected Throwable relaunch(ClassLoader classLoader) throws Exception { this.relaunchClassLoader = classLoader; + return null; } @Override