Register application shutdown hook lazily

Previously, SpringApplicationShutdownHook would always register a
shutdown hook, even if SpringApplication was configured not to
use a shutdown hook, such as in a war deployment. This could
result in a memory leak when the war was undeployed. The shutdown
hook registered by SpringApplicationShutdownHook would remain
registered, pinning the web application's class loader in memory.

This commit updates SpringApplicationShutdownHook so that it
registers a shutdown hook with the JVM lazily, upon registeration
of the first application context.

Fixes gh-27987
This commit is contained in:
Andy Wilkinson 2021-09-16 09:58:07 +01:00
parent afb81f14ea
commit a4f1d32203
2 changed files with 23 additions and 14 deletions

View File

@ -24,6 +24,7 @@ import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@ -59,26 +60,16 @@ class SpringApplicationShutdownHook implements Runnable {
private final ApplicationContextClosedListener contextCloseListener = new ApplicationContextClosedListener();
private final AtomicBoolean shutdownHookAdded = new AtomicBoolean(false);
private boolean inProgress;
SpringApplicationShutdownHook() {
try {
addRuntimeShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments
}
}
protected void addRuntimeShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread(this, "SpringApplicationShutdownHook"));
}
SpringApplicationShutdownHandlers getHandlers() {
return this.handlers;
}
void registerApplicationContext(ConfigurableApplicationContext context) {
addRuntimeShutdownHookIfNecessary();
synchronized (SpringApplicationShutdownHook.class) {
assertNotInProgress();
context.addApplicationListener(this.contextCloseListener);
@ -86,6 +77,21 @@ class SpringApplicationShutdownHook implements Runnable {
}
}
private void addRuntimeShutdownHookIfNecessary() {
if (this.shutdownHookAdded.compareAndSet(false, true)) {
addRuntimeShutdownHook();
}
}
void addRuntimeShutdownHook() {
try {
Runtime.getRuntime().addShutdownHook(new Thread(this, "SpringApplicationShutdownHook"));
}
catch (AccessControlException ex) {
// Not allowed in some environments
}
}
@Override
public void run() {
Set<ConfigurableApplicationContext> contexts;

View File

@ -46,8 +46,11 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
class SpringApplicationShutdownHookTests {
@Test
void createCallsRegister() {
void shutdownHookIsNotAddedUntilContextIsRegistered() {
TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
assertThat(shutdownHook.isRuntimeShutdownHookAdded()).isFalse();
ConfigurableApplicationContext context = new GenericApplicationContext();
shutdownHook.registerApplicationContext(context);
assertThat(shutdownHook.isRuntimeShutdownHookAdded()).isTrue();
}