Skip to content

Commit 9744d28

Browse files
committed
Uninstall SLF4J’s Java logging bridge handler during shutdown
Previously, when LogbackLoggingSystem or Log4JLoggingSystem were initialized during application start up, they would install SLF4J’s Java logging bridge handler, however no corresponding uninstall was performed during application shutdown. When deployed to a servlet container, where the application’s lifecycle doesn’t match the JVM’s lifecycle, this lead to a memory leak. This commit updates LoggingSystem to introduce a new cleanUp method. An empty implementation is provided to preserve backwards compatibility with existing LoggingSystem subclasses. Both LogbackLoggingSystem and Log4JLoggingSystem have been updated to implement cleanUp and uninstall the SLF4J bridge handler. LoggingApplicationListener has been updated to call LoggingSystem.cleanUp in response to a ContextClosedEvent. Closes gh-2324
1 parent 92c8b75 commit 9744d28

File tree

7 files changed

+184
-29
lines changed

7 files changed

+184
-29
lines changed

spring-boot/src/main/java/org/springframework/boot/logging/LoggingApplicationListener.java

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2014 the original author or authors.
2+
* Copyright 2012-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,8 +28,10 @@
2828
import org.springframework.boot.bind.RelaxedPropertyResolver;
2929
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
3030
import org.springframework.boot.context.event.ApplicationStartedEvent;
31+
import org.springframework.context.ApplicationContext;
3132
import org.springframework.context.ApplicationEvent;
3233
import org.springframework.context.ApplicationListener;
34+
import org.springframework.context.event.ContextClosedEvent;
3335
import org.springframework.context.event.SmartApplicationListener;
3436
import org.springframework.core.Ordered;
3537
import org.springframework.core.env.ConfigurableEnvironment;
@@ -68,6 +70,7 @@
6870
*
6971
* @author Dave Syer
7072
* @author Phillip Webb
73+
* @author Andy Wilkinson
7174
*/
7275
public class LoggingApplicationListener implements SmartApplicationListener {
7376

@@ -115,7 +118,10 @@ public class LoggingApplicationListener implements SmartApplicationListener {
115118
}
116119

117120
private static Class<?>[] EVENT_TYPES = { ApplicationStartedEvent.class,
118-
ApplicationEnvironmentPreparedEvent.class };
121+
ApplicationEnvironmentPreparedEvent.class, ContextClosedEvent.class };
122+
123+
private static Class<?>[] SOURCE_TYPES = { SpringApplication.class,
124+
ApplicationContext.class };
119125

120126
private final Log logger = LogFactory.getLog(getClass());
121127

@@ -127,17 +133,21 @@ public class LoggingApplicationListener implements SmartApplicationListener {
127133

128134
@Override
129135
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
130-
for (Class<?> type : EVENT_TYPES) {
131-
if (type.isAssignableFrom(eventType)) {
132-
return true;
133-
}
134-
}
135-
return false;
136+
return isAssignableFrom(eventType, EVENT_TYPES);
136137
}
137138

138139
@Override
139140
public boolean supportsSourceType(Class<?> sourceType) {
140-
return SpringApplication.class.isAssignableFrom(sourceType);
141+
return isAssignableFrom(sourceType, SOURCE_TYPES);
142+
}
143+
144+
private boolean isAssignableFrom(Class<?> type, Class<?>[] supportedTypes) {
145+
for (Class<?> supportedType : supportedTypes) {
146+
if (supportedType.isAssignableFrom(type)) {
147+
return true;
148+
}
149+
}
150+
return false;
141151
}
142152

143153
@Override
@@ -147,14 +157,19 @@ public void onApplicationEvent(ApplicationEvent event) {
147157
initialize(available.getEnvironment(), available.getSpringApplication()
148158
.getClassLoader());
149159
}
150-
else {
160+
else if (event instanceof ApplicationStartedEvent) {
151161
if (System.getProperty(PID_KEY) == null) {
152162
System.setProperty(PID_KEY, new ApplicationPid().toString());
153163
}
154164
LoggingSystem loggingSystem = LoggingSystem.get(ClassUtils
155165
.getDefaultClassLoader());
156166
loggingSystem.beforeInitialize();
157167
}
168+
else {
169+
LoggingSystem loggingSystem = LoggingSystem.get(ClassUtils
170+
.getDefaultClassLoader());
171+
loggingSystem.cleanUp();
172+
}
158173
}
159174

160175
/**

spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystem.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2013 the original author or authors.
2+
* Copyright 2012-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@
2727
*
2828
* @author Phillip Webb
2929
* @author Dave Syer
30+
* @author Andy Wilkinson
3031
*/
3132
public abstract class LoggingSystem {
3233

@@ -61,6 +62,14 @@ public abstract class LoggingSystem {
6162
*/
6263
public abstract void initialize(String configLocation);
6364

65+
/**
66+
* Clean up the logging system. The default implementation does nothing. Subclasses
67+
* should override this method to perform any logging system-specific cleanup.
68+
*/
69+
public void cleanUp() {
70+
71+
}
72+
6473
/**
6574
* Sets the logging level for a given logger.
6675
* @param loggerName the name of the logger to set

spring-boot/src/main/java/org/springframework/boot/logging/log4j/Log4JLoggingSystem.java

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2013 the original author or authors.
2+
* Copyright 2012-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,6 +37,7 @@
3737
*
3838
* @author Phillip Webb
3939
* @author Dave Syer
40+
* @author Andy Wilkinson
4041
*/
4142
public class Log4JLoggingSystem extends AbstractLoggingSystem {
4243

@@ -60,10 +61,7 @@ public Log4JLoggingSystem(ClassLoader classLoader) {
6061
@Override
6162
public void beforeInitialize() {
6263
super.beforeInitialize();
63-
if (ClassUtils.isPresent("org.slf4j.bridge.SLF4JBridgeHandler", getClassLoader())) {
64-
SLF4JBridgeHandler.removeHandlersForRootLogger();
65-
SLF4JBridgeHandler.install();
66-
}
64+
configureJdkLoggingBridgeHandler();
6765
}
6866

6967
@Override
@@ -78,6 +76,45 @@ public void initialize(String configLocation) {
7876
}
7977
}
8078

79+
@Override
80+
public void cleanUp() {
81+
removeJdkLoggingBridgeHandler();
82+
}
83+
84+
private void configureJdkLoggingBridgeHandler() {
85+
try {
86+
if (bridgeHandlerIsAvailable()) {
87+
removeJdkLoggingBridgeHandler();
88+
SLF4JBridgeHandler.install();
89+
}
90+
}
91+
catch (Throwable ex) {
92+
// Ignore. No java.util.logging bridge is installed.
93+
}
94+
}
95+
96+
private boolean bridgeHandlerIsAvailable() {
97+
return ClassUtils.isPresent("org.slf4j.bridge.SLF4JBridgeHandler",
98+
getClassLoader());
99+
}
100+
101+
private void removeJdkLoggingBridgeHandler() {
102+
try {
103+
if (bridgeHandlerIsAvailable()) {
104+
try {
105+
SLF4JBridgeHandler.removeHandlersForRootLogger();
106+
}
107+
catch (NoSuchMethodError ex) {
108+
// Method missing in older versions of SLF4J like in JBoss AS 7.1
109+
SLF4JBridgeHandler.uninstall();
110+
}
111+
}
112+
}
113+
catch (Throwable ex) {
114+
// Ignore and continue
115+
}
116+
}
117+
81118
@Override
82119
public void setLogLevel(String loggerName, LogLevel level) {
83120
Logger logger = (StringUtils.hasLength(loggerName) ? LogManager

spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2014 the original author or authors.
2+
* Copyright 2012-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -41,7 +41,7 @@
4141
import ch.qos.logback.classic.util.ContextInitializer;
4242

4343
/**
44-
* {@link LoggingSystem} for for <a href="http://logback.qos.ch">logback</a>.
44+
* {@link LoggingSystem} for <a href="http://logback.qos.ch">logback</a>.
4545
*
4646
* @author Phillip Webb
4747
* @author Dave Syer
@@ -74,29 +74,49 @@ public void beforeInitialize() {
7474
configureJBossLoggingToUseSlf4j();
7575
}
7676

77+
@Override
78+
public void cleanUp() {
79+
removeJdkLoggingBridgeHandler();
80+
}
81+
7782
private void configureJdkLoggingBridgeHandler() {
7883
try {
79-
if (ClassUtils.isPresent("org.slf4j.bridge.SLF4JBridgeHandler",
80-
getClassLoader())) {
84+
if (bridgeHandlerIsAvailable()) {
85+
removeJdkLoggingBridgeHandler();
86+
SLF4JBridgeHandler.install();
87+
}
88+
}
89+
catch (Throwable ex) {
90+
// Ignore. No java.util.logging bridge is installed.
91+
}
92+
}
93+
94+
private boolean bridgeHandlerIsAvailable() {
95+
return ClassUtils.isPresent("org.slf4j.bridge.SLF4JBridgeHandler",
96+
getClassLoader());
97+
}
98+
99+
private void configureJBossLoggingToUseSlf4j() {
100+
System.setProperty("org.jboss.logging.provider", "slf4j");
101+
}
102+
103+
private void removeJdkLoggingBridgeHandler() {
104+
try {
105+
if (bridgeHandlerIsAvailable()) {
81106
try {
82107
SLF4JBridgeHandler.removeHandlersForRootLogger();
83108
}
84109
catch (NoSuchMethodError ex) {
85110
// Method missing in older versions of SLF4J like in JBoss AS 7.1
86111
SLF4JBridgeHandler.uninstall();
87112
}
88-
SLF4JBridgeHandler.install();
89113
}
90114
}
91115
catch (Throwable ex) {
92-
// Ignore. No java.util.logging bridge is installed.
116+
// Ignore and continue
93117
}
94118
}
95119

96-
private void configureJBossLoggingToUseSlf4j() {
97-
System.setProperty("org.jboss.logging.provider", "slf4j");
98-
}
99-
100120
@Override
101121
public void initialize(String configLocation) {
102122
Assert.notNull(configLocation, "ConfigLocation must not be null");

spring-boot/src/test/java/org/springframework/boot/logging/LoggingApplicationListenerTests.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2014 the original author or authors.
2+
* Copyright 2012-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,7 +18,9 @@
1818

1919
import java.io.File;
2020
import java.io.IOException;
21+
import java.util.logging.Handler;
2122
import java.util.logging.LogManager;
23+
import java.util.logging.Logger;
2224

2325
import org.apache.commons.logging.Log;
2426
import org.apache.commons.logging.LogFactory;
@@ -28,11 +30,13 @@
2830
import org.junit.Rule;
2931
import org.junit.Test;
3032
import org.junit.rules.TemporaryFolder;
33+
import org.slf4j.bridge.SLF4JBridgeHandler;
3134
import org.springframework.boot.SpringApplication;
3235
import org.springframework.boot.context.event.ApplicationStartedEvent;
3336
import org.springframework.boot.logging.java.JavaLoggingSystem;
3437
import org.springframework.boot.test.EnvironmentTestUtils;
3538
import org.springframework.boot.test.OutputCapture;
39+
import org.springframework.context.event.ContextClosedEvent;
3640
import org.springframework.context.support.GenericApplicationContext;
3741

3842
import static org.hamcrest.Matchers.containsString;
@@ -46,6 +50,7 @@
4650
*
4751
* @author Dave Syer
4852
* @author Phillip Webb
53+
* @author Andy Wilkinson
4954
*/
5055
public class LoggingApplicationListenerTests {
5156

@@ -261,4 +266,22 @@ public void parseArgsDoesntReplace() throws Exception {
261266
this.logger.debug("testatdebug");
262267
assertThat(this.outputCapture.toString(), not(containsString("testatdebug")));
263268
}
269+
270+
@Test
271+
public void bridgeHandlerLifecycle() throws Exception {
272+
assertTrue(bridgeHandlerInstalled());
273+
this.initializer.onApplicationEvent(new ContextClosedEvent(this.context));
274+
assertFalse(bridgeHandlerInstalled());
275+
}
276+
277+
private boolean bridgeHandlerInstalled() {
278+
Logger rootLogger = LogManager.getLogManager().getLogger("");
279+
Handler[] handlers = rootLogger.getHandlers();
280+
for (Handler handler : handlers) {
281+
if (handler instanceof SLF4JBridgeHandler) {
282+
return true;
283+
}
284+
}
285+
return false;
286+
}
264287
}

spring-boot/src/test/java/org/springframework/boot/logging/log4j/Log4JLoggingSystemTests.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2014 the original author or authors.
2+
* Copyright 2012-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,23 +16,29 @@
1616

1717
package org.springframework.boot.logging.log4j;
1818

19+
import java.util.logging.Handler;
20+
import java.util.logging.LogManager;
21+
1922
import org.apache.commons.logging.impl.Log4JLogger;
2023
import org.junit.After;
2124
import org.junit.Before;
2225
import org.junit.Rule;
2326
import org.junit.Test;
27+
import org.slf4j.bridge.SLF4JBridgeHandler;
2428
import org.springframework.boot.logging.LogLevel;
2529
import org.springframework.boot.test.OutputCapture;
2630
import org.springframework.util.StringUtils;
2731

2832
import static org.hamcrest.Matchers.equalTo;
33+
import static org.junit.Assert.assertFalse;
2934
import static org.junit.Assert.assertThat;
3035
import static org.junit.Assert.assertTrue;
3136

3237
/**
3338
* Tests for {@link Log4JLoggingSystem}.
3439
*
3540
* @author Phillip Webb
41+
* @author Andy Wilkinson
3642
*/
3743
public class Log4JLoggingSystemTests {
3844

@@ -51,6 +57,7 @@ public void setup() {
5157

5258
@After
5359
public void clear() {
60+
this.loggingSystem.cleanUp();
5461
System.clearProperty("LOG_FILE");
5562
System.clearProperty("LOG_PATH");
5663
System.clearProperty("PID");
@@ -89,4 +96,24 @@ public void setLevel() throws Exception {
8996
equalTo(1));
9097
}
9198

99+
@Test
100+
public void bridgeHandlerLifecycle() {
101+
assertFalse(bridgeHandlerInstalled());
102+
this.loggingSystem.beforeInitialize();
103+
assertTrue(bridgeHandlerInstalled());
104+
this.loggingSystem.cleanUp();
105+
assertFalse(bridgeHandlerInstalled());
106+
}
107+
108+
private boolean bridgeHandlerInstalled() {
109+
java.util.logging.Logger rootLogger = LogManager.getLogManager().getLogger("");
110+
Handler[] handlers = rootLogger.getHandlers();
111+
for (Handler handler : handlers) {
112+
if (handler instanceof SLF4JBridgeHandler) {
113+
return true;
114+
}
115+
}
116+
return false;
117+
}
118+
92119
}

0 commit comments

Comments
 (0)