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
This commit is contained in:
Phillip Webb 2015-06-11 21:19:20 -07:00
parent 24fc94461b
commit 099db11754
15 changed files with 419 additions and 88 deletions

View File

@ -0,0 +1,76 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.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<ChangedFiles> changeSet) {
System.out.println("Changes");
this.latch.countDown();
}
}
}

View File

@ -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(),

View File

@ -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<ChangedFiles> 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<ChangedFiles> changeSet) {

View File

@ -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();
}

View File

@ -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<File> {
private static final Log logger = LogFactory.getLog(ClassPathFolders.class);
private final List<File> folders = new ArrayList<File>();
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<File> iterator() {
return Collections.unmodifiableList(this.folders).iterator();
}
}

View File

@ -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<File> 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}.

View File

@ -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();
}

View File

@ -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(),

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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();
}
/**

View File

@ -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.<ChangedFiles> 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<String, Object> properties = new HashMap<String, Object>();
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));
}

View File

@ -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<ApplicationEvent> 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<ChangedFiles> 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));

View File

@ -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;
}
}
}

View File

@ -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