Clean up the logging system later in context close processing

Previously, the logging system was cleaned up in response to the
root context's ContextClosedEvent being received. This event is
published early in a context's close processing. As a result, the
logging system is in cleaned up state while, for example, disposable
beans are being destroyed.

This commit reworks the logic that triggers logging system clean up
to use a disposable bean instead. Disposable beans are called in
reverse-registration order. The logging clean up bean is registered as
early as possible so that it should be the last disposable bean to
be called.

Closes gh-11676
pull/11775/head
Andy Wilkinson 7 years ago
parent 3c462d3b93
commit 8619256d2a

@ -30,7 +30,7 @@ import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.event.ApplicationStartingEvent; import org.springframework.boot.context.event.ApplicationStartingEvent;
import org.springframework.boot.context.event.SpringApplicationEvent; import org.springframework.boot.context.event.SpringApplicationEvent;
import org.springframework.boot.context.logging.LoggingApplicationListener; import org.springframework.boot.context.logging.LoggingSystemLifecycle;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.DefaultFormattingConversionService;
@ -45,7 +45,7 @@ import org.springframework.http.converter.support.AllEncompassingFormHttpMessage
* @author Andy Wilkinson * @author Andy Wilkinson
* @since 1.3.0 * @since 1.3.0
*/ */
@Order(LoggingApplicationListener.DEFAULT_ORDER + 1) @Order(LoggingSystemLifecycle.DEFAULT_ORDER + 1)
public class BackgroundPreinitializer public class BackgroundPreinitializer
implements ApplicationListener<SpringApplicationEvent> { implements ApplicationListener<SpringApplicationEvent> {

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -27,7 +27,7 @@ import org.springframework.boot.WebApplicationType;
import org.springframework.boot.context.config.AnsiOutputApplicationListener; import org.springframework.boot.context.config.AnsiOutputApplicationListener;
import org.springframework.boot.context.config.ConfigFileApplicationListener; import org.springframework.boot.context.config.ConfigFileApplicationListener;
import org.springframework.boot.context.logging.ClasspathLoggingApplicationListener; import org.springframework.boot.context.logging.ClasspathLoggingApplicationListener;
import org.springframework.boot.context.logging.LoggingApplicationListener; import org.springframework.boot.context.logging.LoggingSystemLifecycle;
import org.springframework.boot.devtools.remote.client.RemoteClientConfiguration; import org.springframework.boot.devtools.remote.client.RemoteClientConfiguration;
import org.springframework.boot.devtools.restart.RestartInitializer; import org.springframework.boot.devtools.restart.RestartInitializer;
import org.springframework.boot.devtools.restart.RestartScopeInitializer; import org.springframework.boot.devtools.restart.RestartScopeInitializer;
@ -74,7 +74,7 @@ public final class RemoteSpringApplication {
listeners.add(new AnsiOutputApplicationListener()); listeners.add(new AnsiOutputApplicationListener());
listeners.add(new ConfigFileApplicationListener()); listeners.add(new ConfigFileApplicationListener());
listeners.add(new ClasspathLoggingApplicationListener()); listeners.add(new ClasspathLoggingApplicationListener());
listeners.add(new LoggingApplicationListener()); listeners.add(new LoggingSystemLifecycle());
listeners.add(new RemoteUrlPropertyExtractor()); listeners.add(new RemoteUrlPropertyExtractor());
return listeners; return listeners;
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -41,7 +41,7 @@ import org.springframework.core.ResolvableType;
public final class ClasspathLoggingApplicationListener public final class ClasspathLoggingApplicationListener
implements GenericApplicationListener { implements GenericApplicationListener {
private static final int ORDER = LoggingApplicationListener.DEFAULT_ORDER + 1; private static final int ORDER = LoggingSystemLifecycle.DEFAULT_ORDER + 1;
private static final Log logger = LogFactory private static final Log logger = LogFactory
.getLog(ClasspathLoggingApplicationListener.class); .getLog(ClasspathLoggingApplicationListener.class);

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -24,7 +24,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.context.event.ApplicationFailedEvent; import org.springframework.boot.context.event.ApplicationFailedEvent;
@ -38,8 +43,9 @@ import org.springframework.boot.logging.LoggingInitializationContext;
import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.logging.LoggingSystem;
import org.springframework.boot.logging.LoggingSystemProperties; import org.springframework.boot.logging.LoggingSystemProperties;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.GenericApplicationListener; import org.springframework.context.event.GenericApplicationListener;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
@ -52,10 +58,11 @@ import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
* An {@link ApplicationListener} that configures the {@link LoggingSystem}. If the * {@code LoggingSystemLifecyle} configures the {@link LoggingSystem} and manages its
* environment contains a {@code logging.config} property it will be used to bootstrap the * lifecycle. If the environment contains a {@code logging.config} property it will be
* logging system, otherwise a default configuration is used. Regardless, logging levels * used to bootstrap the logging system, otherwise a default configuration is used.
* will be customized if the environment contains {@code logging.level.*} entries. * Regardless, logging levels will be customized if the environment contains
* {@code logging.level.*} entries.
* <p> * <p>
* Debug and trace logging for Spring, Tomcat, Jetty and Hibernate will be enabled when * Debug and trace logging for Spring, Tomcat, Jetty and Hibernate will be enabled when
* the environment contains {@code debug} or {@code trace} properties that aren't set to * the environment contains {@code debug} or {@code trace} properties that aren't set to
@ -82,7 +89,8 @@ import org.springframework.util.StringUtils;
* @since 2.0.0 * @since 2.0.0
* @see LoggingSystem#get(ClassLoader) * @see LoggingSystem#get(ClassLoader)
*/ */
public class LoggingApplicationListener implements GenericApplicationListener { public class LoggingSystemLifecycle implements GenericApplicationListener,
ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
private static final Bindable<Map<String, String>> STRING_STRING_MAP = Bindable private static final Bindable<Map<String, String>> STRING_STRING_MAP = Bindable
.mapOf(String.class, String.class); .mapOf(String.class, String.class);
@ -175,10 +183,6 @@ public class LoggingApplicationListener implements GenericApplicationListener {
else if (event instanceof ApplicationPreparedEvent) { else if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent((ApplicationPreparedEvent) event); onApplicationPreparedEvent((ApplicationPreparedEvent) event);
} }
else if (event instanceof ContextClosedEvent && ((ContextClosedEvent) event)
.getApplicationContext().getParent() == null) {
onContextClosedEvent();
}
else if (event instanceof ApplicationFailedEvent) { else if (event instanceof ApplicationFailedEvent) {
onApplicationFailedEvent(); onApplicationFailedEvent();
} }
@ -207,12 +211,6 @@ public class LoggingApplicationListener implements GenericApplicationListener {
} }
} }
private void onContextClosedEvent() {
if (this.loggingSystem != null) {
this.loggingSystem.cleanUp();
}
}
private void onApplicationFailedEvent() { private void onApplicationFailedEvent() {
if (this.loggingSystem != null) { if (this.loggingSystem != null) {
this.loggingSystem.cleanUp(); this.loggingSystem.cleanUp();
@ -367,4 +365,41 @@ public class LoggingApplicationListener implements GenericApplicationListener {
this.parseArgs = parseArgs; this.parseArgs = parseArgs;
} }
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
registerCleanupBean(applicationContext);
}
private void registerCleanupBean(ConfigurableApplicationContext applicationContext) {
BeanFactory beanFactory = applicationContext.getBeanFactory();
BeanDefinition beanDefinition = BeanDefinitionBuilder
.genericBeanDefinition(LoggingSystemCleanup.class)
.addConstructorArgValue(this.loggingSystem)
.addConstructorArgValue(applicationContext).getBeanDefinition();
((BeanDefinitionRegistry) beanFactory).registerBeanDefinition(
LoggingSystemCleanup.class.getName(), beanDefinition);
}
private static final class LoggingSystemCleanup implements DisposableBean {
private final LoggingSystem loggingSystem;
private final ApplicationContext applicationContext;
private LoggingSystemCleanup(LoggingSystem loggingSystem,
ApplicationContext applicationContext) {
this.loggingSystem = loggingSystem;
this.applicationContext = applicationContext;
}
@Override
public void destroy() throws Exception {
if (this.applicationContext.getParent() == null
&& this.loggingSystem != null) {
this.loggingSystem.cleanUp();
}
}
}
} }

@ -16,6 +16,7 @@ org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\ org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\ org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\ org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.context.logging.LoggingSystemLifecycle,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer
# Application Listeners # Application Listeners
@ -27,7 +28,7 @@ org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\ org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\ org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\ org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\ org.springframework.boot.context.logging.LoggingSystemLifecycle,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener
# Environment Post Processors # Environment Post Processors

@ -277,7 +277,7 @@ public class SpringApplicationBuilderTests {
SpringApplicationBuilder application = new SpringApplicationBuilder( SpringApplicationBuilder application = new SpringApplicationBuilder(
ExampleConfig.class).web(WebApplicationType.NONE); ExampleConfig.class).web(WebApplicationType.NONE);
this.context = application.run(); this.context = application.run();
assertThat(application.application().getInitializers()).hasSize(4); assertThat(application.application().getInitializers()).hasSize(5);
} }
@Test @Test
@ -286,7 +286,7 @@ public class SpringApplicationBuilderTests {
ExampleConfig.class).child(ChildConfig.class) ExampleConfig.class).child(ChildConfig.class)
.web(WebApplicationType.NONE); .web(WebApplicationType.NONE);
this.context = application.run(); this.context = application.run();
assertThat(application.application().getInitializers()).hasSize(5); assertThat(application.application().getInitializers()).hasSize(6);
} }
@Test @Test
@ -296,7 +296,7 @@ public class SpringApplicationBuilderTests {
(ConfigurableApplicationContext applicationContext) -> { (ConfigurableApplicationContext applicationContext) -> {
}); });
this.context = application.run(); this.context = application.run();
assertThat(application.application().getInitializers()).hasSize(5); assertThat(application.application().getInitializers()).hasSize(6);
} }
@Test @Test

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -33,11 +33,11 @@ import org.springframework.stereotype.Component;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
/** /**
* Integration tests for {@link LoggingApplicationListener}. * Integration tests for {@link LoggingSystemLifecycle}.
* *
* @author Stephane Nicoll * @author Stephane Nicoll
*/ */
public class LoggingApplicationListenerIntegrationTests { public class LoggingSystemLifecycleIntegrationTests {
@Rule @Rule
public OutputCapture outputCapture = new OutputCapture(); public OutputCapture outputCapture = new OutputCapture();

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -56,7 +56,6 @@ import org.springframework.boot.testsupport.runner.classpath.ClassPathExclusions
import org.springframework.boot.testsupport.runner.classpath.ModifiedClassPathRunner; import org.springframework.boot.testsupport.runner.classpath.ModifiedClassPathRunner;
import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.SimpleApplicationEventMulticaster; import org.springframework.context.event.SimpleApplicationEventMulticaster;
import org.springframework.context.support.GenericApplicationContext; import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.ConfigurableEnvironment;
@ -70,7 +69,7 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
/** /**
* Tests for {@link LoggingApplicationListener} with Logback. * Tests for {@link LoggingSystemLifecycle} with Logback.
* *
* @author Dave Syer * @author Dave Syer
* @author Phillip Webb * @author Phillip Webb
@ -80,7 +79,7 @@ import static org.hamcrest.Matchers.not;
*/ */
@RunWith(ModifiedClassPathRunner.class) @RunWith(ModifiedClassPathRunner.class)
@ClassPathExclusions("log4j*.jar") @ClassPathExclusions("log4j*.jar")
public class LoggingApplicationListenerTests { public class LoggingSystemLifecycleTests {
private static final String[] NO_ARGS = {}; private static final String[] NO_ARGS = {};
@ -93,7 +92,7 @@ public class LoggingApplicationListenerTests {
@Rule @Rule
public OutputCapture outputCapture = new OutputCapture(); public OutputCapture outputCapture = new OutputCapture();
private final LoggingApplicationListener initializer = new LoggingApplicationListener(); private final LoggingSystemLifecycle initializer = new LoggingSystemLifecycle();
private final Log logger = new SLF4JLogFactory().getInstance(getClass()); private final Log logger = new SLF4JLogFactory().getInstance(getClass());
@ -218,7 +217,7 @@ public class LoggingApplicationListenerTests {
"logging.file=target/foo.log"); "logging.file=target/foo.log");
this.initializer.initialize(this.context.getEnvironment(), this.initializer.initialize(this.context.getEnvironment(),
this.context.getClassLoader()); this.context.getClassLoader());
Log logger = LogFactory.getLog(LoggingApplicationListenerTests.class); Log logger = LogFactory.getLog(LoggingSystemLifecycleTests.class);
logger.info("Hello world"); logger.info("Hello world");
String output = this.outputCapture.toString().trim(); String output = this.outputCapture.toString().trim();
assertThat(output).startsWith("target/foo.log"); assertThat(output).startsWith("target/foo.log");
@ -231,7 +230,7 @@ public class LoggingApplicationListenerTests {
"logging.file=target/foo.log"); "logging.file=target/foo.log");
this.initializer.initialize(this.context.getEnvironment(), this.initializer.initialize(this.context.getEnvironment(),
this.context.getClassLoader()); this.context.getClassLoader());
Log logger = LogFactory.getLog(LoggingApplicationListenerTests.class); Log logger = LogFactory.getLog(LoggingSystemLifecycleTests.class);
logger.info("Hello world"); logger.info("Hello world");
assertThat(new File("target/foo.log").exists()).isTrue(); assertThat(new File("target/foo.log").exists()).isTrue();
} }
@ -243,7 +242,7 @@ public class LoggingApplicationListenerTests {
"logging.path=target/foo/"); "logging.path=target/foo/");
this.initializer.initialize(this.context.getEnvironment(), this.initializer.initialize(this.context.getEnvironment(),
this.context.getClassLoader()); this.context.getClassLoader());
Log logger = LogFactory.getLog(LoggingApplicationListenerTests.class); Log logger = LogFactory.getLog(LoggingSystemLifecycleTests.class);
logger.info("Hello world"); logger.info("Hello world");
String output = this.outputCapture.toString().trim(); String output = this.outputCapture.toString().trim();
assertThat(output).startsWith("target/foo/spring.log"); assertThat(output).startsWith("target/foo/spring.log");
@ -387,8 +386,10 @@ public class LoggingApplicationListenerTests {
@Test @Test
public void bridgeHandlerLifecycle() { public void bridgeHandlerLifecycle() {
this.initializer.initialize(this.context);
this.context.refresh();
assertThat(bridgeHandlerInstalled()).isTrue(); assertThat(bridgeHandlerInstalled()).isTrue();
multicastEvent(new ContextClosedEvent(this.context)); this.context.close();
assertThat(bridgeHandlerInstalled()).isFalse(); assertThat(bridgeHandlerInstalled()).isFalse();
} }
@ -449,10 +450,12 @@ public class LoggingApplicationListenerTests {
TestCleanupLoggingSystem.class.getName()); TestCleanupLoggingSystem.class.getName());
multicastEvent( multicastEvent(
new ApplicationStartingEvent(this.springApplication, new String[0])); new ApplicationStartingEvent(this.springApplication, new String[0]));
this.initializer.initialize(this.context);
this.context.refresh();
TestCleanupLoggingSystem loggingSystem = (TestCleanupLoggingSystem) ReflectionTestUtils TestCleanupLoggingSystem loggingSystem = (TestCleanupLoggingSystem) ReflectionTestUtils
.getField(this.initializer, "loggingSystem"); .getField(this.initializer, "loggingSystem");
assertThat(loggingSystem.cleanedUp).isFalse(); assertThat(loggingSystem.cleanedUp).isFalse();
multicastEvent(new ContextClosedEvent(this.context)); this.context.close();
assertThat(loggingSystem.cleanedUp).isTrue(); assertThat(loggingSystem.cleanedUp).isTrue();
} }
@ -462,16 +465,19 @@ public class LoggingApplicationListenerTests {
TestCleanupLoggingSystem.class.getName()); TestCleanupLoggingSystem.class.getName());
multicastEvent( multicastEvent(
new ApplicationStartingEvent(this.springApplication, new String[0])); new ApplicationStartingEvent(this.springApplication, new String[0]));
this.initializer.initialize(this.context);
this.context.refresh();
TestCleanupLoggingSystem loggingSystem = (TestCleanupLoggingSystem) ReflectionTestUtils TestCleanupLoggingSystem loggingSystem = (TestCleanupLoggingSystem) ReflectionTestUtils
.getField(this.initializer, "loggingSystem"); .getField(this.initializer, "loggingSystem");
assertThat(loggingSystem.cleanedUp).isFalse(); assertThat(loggingSystem.cleanedUp).isFalse();
GenericApplicationContext childContext = new GenericApplicationContext(); GenericApplicationContext childContext = new GenericApplicationContext();
childContext.setParent(this.context); childContext.setParent(this.context);
multicastEvent(new ContextClosedEvent(childContext)); this.initializer.initialize(childContext);
childContext.refresh();
childContext.close();
assertThat(loggingSystem.cleanedUp).isFalse(); assertThat(loggingSystem.cleanedUp).isFalse();
multicastEvent(new ContextClosedEvent(this.context)); this.context.close();
assertThat(loggingSystem.cleanedUp).isTrue(); assertThat(loggingSystem.cleanedUp).isTrue();
childContext.close();
} }
@Test @Test
@ -613,8 +619,7 @@ public class LoggingApplicationListenerTests {
} }
public static class TestLoggingApplicationListener public static class TestLoggingApplicationListener extends LoggingSystemLifecycle {
extends LoggingApplicationListener {
private Thread shutdownHook; private Thread shutdownHook;
Loading…
Cancel
Save