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