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:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user