Call context.close() rather than shutdown hook in DevTools restart
Previously, when DevTools was restarting the application it would
use reflection to run all of the JVM's shutdown hooks. This was done
to close any SpringApplications' application contexts. Unfortunately,
it had the unwanted side-effect of running other shutdown hooks as
well.
The other shutdown hooks were often written with the, entirely
reasonable, expectation that they would only be called when the JVM
was shutting down. Calling them at another time could leave the
hook's library in an unexpected state. One such example is Log4J2
which was worked around in aaae4aa3 (see gh-4279). Another is the
problem with Eureka (see gh-4097). There's no work around for this
problem, even with reflective hackery, hence the change being made
here.
This commit updates the Restarter so that shutdown hooks are no longer
called during a restart. This removes the chance of a restart having
the unwanted side-effect of leaving a third-party library in a broken
state. RestartApplicationListener now prepares the Restarter with the
root application context, and the Restarter then closes it as part of
the restart. The changes have been tested with an application that
uses a single context and an application with a context hierarchy.
Closes gh-4097
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
* Copyright 2012-2016 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.
|
||||
@@ -22,6 +22,7 @@ import org.junit.Test;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.context.event.ApplicationFailedEvent;
|
||||
import org.springframework.boot.context.event.ApplicationPreparedEvent;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
@@ -36,6 +37,7 @@ import static org.mockito.Mockito.mock;
|
||||
* Tests for {@link RestartApplicationListener}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class RestartApplicationListenerTests {
|
||||
|
||||
@@ -62,6 +64,8 @@ public class RestartApplicationListenerTests {
|
||||
assertThat(ReflectionTestUtils.getField(Restarter.getInstance(), "args"))
|
||||
.isEqualTo(ARGS);
|
||||
assertThat(Restarter.getInstance().isFinished()).isTrue();
|
||||
assertThat(ReflectionTestUtils.getField(Restarter.getInstance(), "rootContext"))
|
||||
.isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -70,6 +74,8 @@ public class RestartApplicationListenerTests {
|
||||
assertThat(ReflectionTestUtils.getField(Restarter.getInstance(), "args"))
|
||||
.isEqualTo(ARGS);
|
||||
assertThat(Restarter.getInstance().isFinished()).isTrue();
|
||||
assertThat(ReflectionTestUtils.getField(Restarter.getInstance(), "rootContext"))
|
||||
.isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -89,6 +95,8 @@ public class RestartApplicationListenerTests {
|
||||
listener.onApplicationEvent(new ApplicationStartedEvent(application, ARGS));
|
||||
assertThat(Restarter.getInstance()).isNotEqualTo(nullValue());
|
||||
assertThat(Restarter.getInstance().isFinished()).isFalse();
|
||||
listener.onApplicationEvent(
|
||||
new ApplicationPreparedEvent(application, ARGS, context));
|
||||
if (failed) {
|
||||
listener.onApplicationEvent(new ApplicationFailedEvent(application, ARGS,
|
||||
context, new RuntimeException()));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
* Copyright 2012-2016 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.
|
||||
@@ -34,7 +34,9 @@ import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile;
|
||||
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.ApplicationListener;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.event.ContextClosedEvent;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -51,6 +53,7 @@ import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
* Tests for {@link Restarter}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class RestarterTests {
|
||||
|
||||
@@ -94,7 +97,7 @@ public class RestarterTests {
|
||||
String output = this.out.toString();
|
||||
assertThat(StringUtils.countOccurrencesOf(output, "Tick 0")).isGreaterThan(1);
|
||||
assertThat(StringUtils.countOccurrencesOf(output, "Tick 1")).isGreaterThan(1);
|
||||
assertThat(TestRestartListener.restarts).isGreaterThan(0);
|
||||
assertThat(CloseCountingApplicationListener.closed).isGreaterThan(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -213,15 +216,14 @@ public class RestarterTests {
|
||||
}
|
||||
|
||||
public static void main(String... args) {
|
||||
Restarter.initialize(args, false, new MockRestartInitializer(), true,
|
||||
new TestRestartListener());
|
||||
Restarter.initialize(args, false, new MockRestartInitializer(), true);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
||||
SampleApplication.class);
|
||||
context.registerShutdownHook();
|
||||
context.addApplicationListener(new CloseCountingApplicationListener());
|
||||
Restarter.getInstance().prepare(context);
|
||||
System.out.println("Sleep " + Thread.currentThread());
|
||||
sleep();
|
||||
quit = true;
|
||||
context.close();
|
||||
}
|
||||
|
||||
private static void sleep() {
|
||||
@@ -235,6 +237,18 @@ public class RestarterTests {
|
||||
|
||||
}
|
||||
|
||||
private static class CloseCountingApplicationListener
|
||||
implements ApplicationListener<ContextClosedEvent> {
|
||||
|
||||
static int closed = 0;
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ContextClosedEvent event) {
|
||||
closed++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class TestableRestarter extends Restarter {
|
||||
|
||||
private ClassLoader relaunchClassLoader;
|
||||
@@ -276,15 +290,4 @@ public class RestarterTests {
|
||||
|
||||
}
|
||||
|
||||
private static class TestRestartListener implements RestartListener {
|
||||
|
||||
private static int restarts;
|
||||
|
||||
@Override
|
||||
public void beforeRestart() {
|
||||
restarts++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user