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

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