This is an automated email from the ASF dual-hosted git repository. pkarwasz pushed a commit to branch release/2.21.0 in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit 3c5191df6a4c3444fb0d785c66cc0fd6747a8b6b Author: Piotr P. Karwasz <[email protected]> AuthorDate: Mon Sep 11 13:11:37 2023 +0200 Allow multiple Log4jServletContextListener registrations This closes #1782. --- .../log4j/web/Log4jServletContextListener.java | 70 ++++++++++++----- .../log4j/web/Log4jServletContextListenerTest.java | 89 ++++++++++++++-------- .../log4j/web/Log4jServletContextListener.java | 70 ++++++++++++----- .../log4j/web/Log4jServletContextListenerTest.java | 89 ++++++++++++++-------- ...w_multiple_servletcontextlistener_instances.xml | 28 +++++++ 5 files changed, 246 insertions(+), 100 deletions(-) diff --git a/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jServletContextListener.java b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jServletContextListener.java index 599eddb8b8..3a518c62e7 100644 --- a/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jServletContextListener.java +++ b/log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jServletContextListener.java @@ -36,6 +36,8 @@ import static org.apache.logging.log4j.util.Strings.toRootUpperCase; */ public class Log4jServletContextListener implements ServletContextListener { + static final String START_COUNT_ATTR = Log4jServletContextListener.class.getName() + ".START_COUNT"; + private static final int DEFAULT_STOP_TIMEOUT = 30; private static final TimeUnit DEFAULT_STOP_TIMEOUT_TIMEUNIT = TimeUnit.SECONDS; @@ -47,11 +49,30 @@ public class Log4jServletContextListener implements ServletContextListener { private ServletContext servletContext; private Log4jWebLifeCycle initializer; + private int getAndIncrementCount() { + Integer count = (Integer) servletContext.getAttribute(START_COUNT_ATTR); + if (count == null) { + count = 0; + } + servletContext.setAttribute(START_COUNT_ATTR, count + 1); + return count; + } + + private int decrementAndGetCount() { + Integer count = (Integer) servletContext.getAttribute(START_COUNT_ATTR); + if (count == null) { + LOGGER.warn( + "{} received a 'contextDestroyed' message without a corresponding 'contextInitialized' message.", + getClass().getName()); + count = 1; + } + servletContext.setAttribute(START_COUNT_ATTR, --count); + return count; + } + @Override public void contextInitialized(final ServletContextEvent event) { this.servletContext = event.getServletContext(); - LOGGER.debug("Log4jServletContextListener ensuring that Log4j starts up properly."); - if ("true".equalsIgnoreCase(servletContext.getInitParameter( Log4jWebSupport.IS_LOG4J_AUTO_SHUTDOWN_DISABLED))) { throw new IllegalStateException("Do not use " + getClass().getSimpleName() + " when " @@ -61,6 +82,12 @@ public class Log4jServletContextListener implements ServletContextListener { } this.initializer = WebLoggerContextUtils.getWebLifeCycle(this.servletContext); + if (getAndIncrementCount() != 0) { + LOGGER.debug("Skipping Log4j context initialization, since {} is registered multiple times.", + getClass().getSimpleName()); + return; + } + LOGGER.info("{} triggered a Log4j context initialization.", getClass().getSimpleName()); try { this.initializer.start(); this.initializer.setLoggerContext(); // the application is just now starting to start up @@ -72,23 +99,32 @@ public class Log4jServletContextListener implements ServletContextListener { @Override public void contextDestroyed(final ServletContextEvent event) { if (this.servletContext == null || this.initializer == null) { - LOGGER.warn("Context destroyed before it was initialized."); + LOGGER.warn("Servlet context destroyed before it was initialized."); return; } - LOGGER.debug("Log4jServletContextListener ensuring that Log4j shuts down properly."); - - this.initializer.clearLoggerContext(); // the application is finished - // shutting down now - if (initializer instanceof LifeCycle2) { - final String stopTimeoutStr = servletContext.getInitParameter(KEY_STOP_TIMEOUT); - final long stopTimeout = Strings.isEmpty(stopTimeoutStr) ? DEFAULT_STOP_TIMEOUT - : Long.parseLong(stopTimeoutStr); - final String timeoutTimeUnitStr = servletContext.getInitParameter(KEY_STOP_TIMEOUT_TIMEUNIT); - final TimeUnit timeoutTimeUnit = Strings.isEmpty(timeoutTimeUnitStr) ? DEFAULT_STOP_TIMEOUT_TIMEUNIT - : TimeUnit.valueOf(toRootUpperCase(timeoutTimeUnitStr)); - ((LifeCycle2) this.initializer).stop(stopTimeout, timeoutTimeUnit); - } else { - this.initializer.stop(); + + if (decrementAndGetCount() != 0) { + LOGGER.debug("Skipping Log4j context shutdown, since {} is registered multiple times.", + getClass().getSimpleName()); + return; + } + LOGGER.info("{} triggered a Log4j context shutdown.", getClass().getSimpleName()); + try { + this.initializer.clearLoggerContext(); // the application is finished + // shutting down now + if (initializer instanceof LifeCycle2) { + final String stopTimeoutStr = servletContext.getInitParameter(KEY_STOP_TIMEOUT); + final long stopTimeout = Strings.isEmpty(stopTimeoutStr) ? DEFAULT_STOP_TIMEOUT + : Long.parseLong(stopTimeoutStr); + final String timeoutTimeUnitStr = servletContext.getInitParameter(KEY_STOP_TIMEOUT_TIMEUNIT); + final TimeUnit timeoutTimeUnit = Strings.isEmpty(timeoutTimeUnitStr) ? DEFAULT_STOP_TIMEOUT_TIMEUNIT + : TimeUnit.valueOf(toRootUpperCase(timeoutTimeUnitStr)); + ((LifeCycle2) this.initializer).stop(stopTimeout, timeoutTimeUnit); + } else { + this.initializer.stop(); + } + } catch (final IllegalStateException e) { + throw new IllegalStateException("Failed to shutdown Log4j properly.", e); } } } diff --git a/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jServletContextListenerTest.java b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jServletContextListenerTest.java index 2022d13768..909e67296c 100644 --- a/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jServletContextListenerTest.java +++ b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jServletContextListenerTest.java @@ -16,6 +16,8 @@ */ package org.apache.logging.log4j.web; +import java.util.concurrent.atomic.AtomicReference; + import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContextEvent; @@ -23,59 +25,85 @@ import org.apache.logging.log4j.util.Strings; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; +import org.mockito.Mock.Strictness; import org.mockito.junit.jupiter.MockitoExtension; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.BDDMockito.eq; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.AdditionalAnswers.answerVoid; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.doAnswer; @ExtendWith(MockitoExtension.class) public class Log4jServletContextListenerTest { /* event and servletContext are marked lenient because they aren't used in the * testDestroyWithNoInit but are only accessed during initialization */ - @Mock(lenient = true) + @Mock(strictness = Strictness.LENIENT) private ServletContextEvent event; - @Mock(lenient = true) + @Mock(strictness = Strictness.LENIENT) private ServletContext servletContext; @Mock private Log4jWebLifeCycle initializer; - private Log4jServletContextListener listener; + private final AtomicReference<Object> count = new AtomicReference<>(); @BeforeEach public void setUp() { - this.listener = new Log4jServletContextListener(); given(event.getServletContext()).willReturn(servletContext); given(servletContext.getAttribute(Log4jWebSupport.SUPPORT_ATTRIBUTE)).willReturn(initializer); - } - @Test - public void testInitAndDestroy() throws Exception { - this.listener.contextInitialized(this.event); + doAnswer(answerVoid((k, v) -> count.set(v))) + .when(servletContext) + .setAttribute(eq(Log4jServletContextListener.START_COUNT_ATTR), any()); + doAnswer(__ -> count.get()) + .when(servletContext) + .getAttribute(Log4jServletContextListener.START_COUNT_ATTR); + } - then(initializer).should().start(); - then(initializer).should().setLoggerContext(); + @ParameterizedTest + @ValueSource(ints = { 1, 2, 3 }) + public void testInitAndDestroy(final int listenerCount) throws Exception { + final Log4jServletContextListener[] listeners = new Log4jServletContextListener[listenerCount]; + for (int idx = 0; idx < listenerCount; idx++) { + final Log4jServletContextListener listener = new Log4jServletContextListener(); + listeners[idx] = listener; + + listener.contextInitialized(event); + if (idx == 0) { + then(initializer).should().start(); + then(initializer).should().setLoggerContext(); + } else { + then(initializer).shouldHaveNoMoreInteractions(); + } + } - this.listener.contextDestroyed(this.event); + for (int idx = listenerCount - 1; idx >= 0; idx--) { + final Log4jServletContextListener listener = listeners[idx]; - then(initializer).should().clearLoggerContext(); - then(initializer).should().stop(); + listener.contextDestroyed(event); + if (idx == 0) { + then(initializer).should().clearLoggerContext(); + then(initializer).should().stop(); + } else { + then(initializer).shouldHaveNoMoreInteractions(); + } + } } @Test public void testInitFailure() throws Exception { willThrow(new IllegalStateException(Strings.EMPTY)).given(initializer).start(); + final Log4jServletContextListener listener = new Log4jServletContextListener(); - try { - this.listener.contextInitialized(this.event); - fail("Expected a RuntimeException."); - } catch (final RuntimeException e) { - assertEquals("Failed to initialize Log4j properly.", e.getMessage(), "The message is not correct."); - } + assertThrows(RuntimeException.class, () -> listener.contextInitialized(this.event), + "Failed to initialize Log4j properly."); } @Test @@ -93,17 +121,12 @@ public class Log4jServletContextListenerTest { } private void ensureInitializingFailsWhenAuthShutdownIsEnabled() { - try { - this.listener.contextInitialized(this.event); - fail("Expected a RuntimeException."); - } catch (final RuntimeException e) { - final String expectedMessage = - "Do not use " + Log4jServletContextListener.class.getSimpleName() + " when " - + Log4jWebSupport.IS_LOG4J_AUTO_SHUTDOWN_DISABLED + " is true. Please use " - + Log4jShutdownOnContextDestroyedListener.class.getSimpleName() + " instead of " - + Log4jServletContextListener.class.getSimpleName() + "."; - - assertEquals(expectedMessage, e.getMessage(), "The message is not correct"); - } + final Log4jServletContextListener listener = new Log4jServletContextListener(); + final String message = "Do not use " + Log4jServletContextListener.class.getSimpleName() + " when " + + Log4jWebSupport.IS_LOG4J_AUTO_SHUTDOWN_DISABLED + " is true. Please use " + + Log4jShutdownOnContextDestroyedListener.class.getSimpleName() + " instead of " + + Log4jServletContextListener.class.getSimpleName() + "."; + + assertThrows(RuntimeException.class, () -> listener.contextInitialized(event), message); } } diff --git a/log4j-web/src/main/java/org/apache/logging/log4j/web/Log4jServletContextListener.java b/log4j-web/src/main/java/org/apache/logging/log4j/web/Log4jServletContextListener.java index 9ca8320208..93312d14e1 100644 --- a/log4j-web/src/main/java/org/apache/logging/log4j/web/Log4jServletContextListener.java +++ b/log4j-web/src/main/java/org/apache/logging/log4j/web/Log4jServletContextListener.java @@ -36,6 +36,8 @@ import static org.apache.logging.log4j.util.Strings.toRootUpperCase; */ public class Log4jServletContextListener implements ServletContextListener { + static final String START_COUNT_ATTR = Log4jServletContextListener.class.getName() + ".START_COUNT"; + private static final int DEFAULT_STOP_TIMEOUT = 30; private static final TimeUnit DEFAULT_STOP_TIMEOUT_TIMEUNIT = TimeUnit.SECONDS; @@ -47,11 +49,30 @@ public class Log4jServletContextListener implements ServletContextListener { private ServletContext servletContext; private Log4jWebLifeCycle initializer; + private int getAndIncrementCount() { + Integer count = (Integer) servletContext.getAttribute(START_COUNT_ATTR); + if (count == null) { + count = 0; + } + servletContext.setAttribute(START_COUNT_ATTR, count + 1); + return count; + } + + private int decrementAndGetCount() { + Integer count = (Integer) servletContext.getAttribute(START_COUNT_ATTR); + if (count == null) { + LOGGER.warn( + "{} received a 'contextDestroyed' message without a corresponding 'contextInitialized' message.", + getClass().getName()); + count = 1; + } + servletContext.setAttribute(START_COUNT_ATTR, --count); + return count; + } + @Override public void contextInitialized(final ServletContextEvent event) { this.servletContext = event.getServletContext(); - LOGGER.debug("Log4jServletContextListener ensuring that Log4j starts up properly."); - if ("true".equalsIgnoreCase(servletContext.getInitParameter( Log4jWebSupport.IS_LOG4J_AUTO_SHUTDOWN_DISABLED))) { throw new IllegalStateException("Do not use " + getClass().getSimpleName() + " when " @@ -61,6 +82,12 @@ public class Log4jServletContextListener implements ServletContextListener { } this.initializer = WebLoggerContextUtils.getWebLifeCycle(this.servletContext); + if (getAndIncrementCount() != 0) { + LOGGER.debug("Skipping Log4j context initialization, since {} is registered multiple times.", + getClass().getSimpleName()); + return; + } + LOGGER.info("{} triggered a Log4j context initialization.", getClass().getSimpleName()); try { this.initializer.start(); this.initializer.setLoggerContext(); // the application is just now starting to start up @@ -72,23 +99,32 @@ public class Log4jServletContextListener implements ServletContextListener { @Override public void contextDestroyed(final ServletContextEvent event) { if (this.servletContext == null || this.initializer == null) { - LOGGER.warn("Context destroyed before it was initialized."); + LOGGER.warn("Servlet context destroyed before it was initialized."); return; } - LOGGER.debug("Log4jServletContextListener ensuring that Log4j shuts down properly."); - - this.initializer.clearLoggerContext(); // the application is finished - // shutting down now - if (initializer instanceof LifeCycle2) { - final String stopTimeoutStr = servletContext.getInitParameter(KEY_STOP_TIMEOUT); - final long stopTimeout = Strings.isEmpty(stopTimeoutStr) ? DEFAULT_STOP_TIMEOUT - : Long.parseLong(stopTimeoutStr); - final String timeoutTimeUnitStr = servletContext.getInitParameter(KEY_STOP_TIMEOUT_TIMEUNIT); - final TimeUnit timeoutTimeUnit = Strings.isEmpty(timeoutTimeUnitStr) ? DEFAULT_STOP_TIMEOUT_TIMEUNIT - : TimeUnit.valueOf(toRootUpperCase(timeoutTimeUnitStr)); - ((LifeCycle2) this.initializer).stop(stopTimeout, timeoutTimeUnit); - } else { - this.initializer.stop(); + + if (decrementAndGetCount() != 0) { + LOGGER.debug("Skipping Log4j context shutdown, since {} is registered multiple times.", + getClass().getSimpleName()); + return; + } + LOGGER.info("{} triggered a Log4j context shutdown.", getClass().getSimpleName()); + try { + this.initializer.clearLoggerContext(); // the application is finished + // shutting down now + if (initializer instanceof LifeCycle2) { + final String stopTimeoutStr = servletContext.getInitParameter(KEY_STOP_TIMEOUT); + final long stopTimeout = Strings.isEmpty(stopTimeoutStr) ? DEFAULT_STOP_TIMEOUT + : Long.parseLong(stopTimeoutStr); + final String timeoutTimeUnitStr = servletContext.getInitParameter(KEY_STOP_TIMEOUT_TIMEUNIT); + final TimeUnit timeoutTimeUnit = Strings.isEmpty(timeoutTimeUnitStr) ? DEFAULT_STOP_TIMEOUT_TIMEUNIT + : TimeUnit.valueOf(toRootUpperCase(timeoutTimeUnitStr)); + ((LifeCycle2) this.initializer).stop(stopTimeout, timeoutTimeUnit); + } else { + this.initializer.stop(); + } + } catch (final IllegalStateException e) { + throw new IllegalStateException("Failed to shutdown Log4j properly.", e); } } } diff --git a/log4j-web/src/test/java/org/apache/logging/log4j/web/Log4jServletContextListenerTest.java b/log4j-web/src/test/java/org/apache/logging/log4j/web/Log4jServletContextListenerTest.java index 698e25871a..187b44c6ed 100644 --- a/log4j-web/src/test/java/org/apache/logging/log4j/web/Log4jServletContextListenerTest.java +++ b/log4j-web/src/test/java/org/apache/logging/log4j/web/Log4jServletContextListenerTest.java @@ -16,6 +16,8 @@ */ package org.apache.logging.log4j.web; +import java.util.concurrent.atomic.AtomicReference; + import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; @@ -23,59 +25,85 @@ import org.apache.logging.log4j.util.Strings; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; +import org.mockito.Mock.Strictness; import org.mockito.junit.jupiter.MockitoExtension; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.BDDMockito.eq; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.AdditionalAnswers.answerVoid; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.doAnswer; @ExtendWith(MockitoExtension.class) public class Log4jServletContextListenerTest { /* event and servletContext are marked lenient because they aren't used in the * testDestroyWithNoInit but are only accessed during initialization */ - @Mock(lenient = true) + @Mock(strictness = Strictness.LENIENT) private ServletContextEvent event; - @Mock(lenient = true) + @Mock(strictness = Strictness.LENIENT) private ServletContext servletContext; @Mock private Log4jWebLifeCycle initializer; - private Log4jServletContextListener listener; + private final AtomicReference<Object> count = new AtomicReference<>(); @BeforeEach public void setUp() { - this.listener = new Log4jServletContextListener(); given(event.getServletContext()).willReturn(servletContext); given(servletContext.getAttribute(Log4jWebSupport.SUPPORT_ATTRIBUTE)).willReturn(initializer); - } - @Test - public void testInitAndDestroy() throws Exception { - this.listener.contextInitialized(this.event); + doAnswer(answerVoid((k, v) -> count.set(v))) + .when(servletContext) + .setAttribute(eq(Log4jServletContextListener.START_COUNT_ATTR), any()); + doAnswer(__ -> count.get()) + .when(servletContext) + .getAttribute(Log4jServletContextListener.START_COUNT_ATTR); + } - then(initializer).should().start(); - then(initializer).should().setLoggerContext(); + @ParameterizedTest + @ValueSource(ints = { 1, 2, 3 }) + public void testInitAndDestroy(final int listenerCount) throws Exception { + final Log4jServletContextListener[] listeners = new Log4jServletContextListener[listenerCount]; + for (int idx = 0; idx < listenerCount; idx++) { + final Log4jServletContextListener listener = new Log4jServletContextListener(); + listeners[idx] = listener; + + listener.contextInitialized(event); + if (idx == 0) { + then(initializer).should().start(); + then(initializer).should().setLoggerContext(); + } else { + then(initializer).shouldHaveNoMoreInteractions(); + } + } - this.listener.contextDestroyed(this.event); + for (int idx = listenerCount - 1; idx >= 0; idx--) { + final Log4jServletContextListener listener = listeners[idx]; - then(initializer).should().clearLoggerContext(); - then(initializer).should().stop(); + listener.contextDestroyed(event); + if (idx == 0) { + then(initializer).should().clearLoggerContext(); + then(initializer).should().stop(); + } else { + then(initializer).shouldHaveNoMoreInteractions(); + } + } } @Test public void testInitFailure() throws Exception { willThrow(new IllegalStateException(Strings.EMPTY)).given(initializer).start(); + final Log4jServletContextListener listener = new Log4jServletContextListener(); - try { - this.listener.contextInitialized(this.event); - fail("Expected a RuntimeException."); - } catch (final RuntimeException e) { - assertEquals("Failed to initialize Log4j properly.", e.getMessage(), "The message is not correct."); - } + assertThrows(RuntimeException.class, () -> listener.contextInitialized(this.event), + "Failed to initialize Log4j properly."); } @Test @@ -93,17 +121,12 @@ public class Log4jServletContextListenerTest { } private void ensureInitializingFailsWhenAuthShutdownIsEnabled() { - try { - this.listener.contextInitialized(this.event); - fail("Expected a RuntimeException."); - } catch (final RuntimeException e) { - final String expectedMessage = - "Do not use " + Log4jServletContextListener.class.getSimpleName() + " when " - + Log4jWebSupport.IS_LOG4J_AUTO_SHUTDOWN_DISABLED + " is true. Please use " - + Log4jShutdownOnContextDestroyedListener.class.getSimpleName() + " instead of " - + Log4jServletContextListener.class.getSimpleName() + "."; - - assertEquals(expectedMessage, e.getMessage(), "The message is not correct"); - } + final Log4jServletContextListener listener = new Log4jServletContextListener(); + final String message = "Do not use " + Log4jServletContextListener.class.getSimpleName() + " when " + + Log4jWebSupport.IS_LOG4J_AUTO_SHUTDOWN_DISABLED + " is true. Please use " + + Log4jShutdownOnContextDestroyedListener.class.getSimpleName() + " instead of " + + Log4jServletContextListener.class.getSimpleName() + "."; + + assertThrows(RuntimeException.class, () -> listener.contextInitialized(event), message); } } diff --git a/src/changelog/.2.x.x/1782_allow_multiple_servletcontextlistener_instances.xml b/src/changelog/.2.x.x/1782_allow_multiple_servletcontextlistener_instances.xml new file mode 100644 index 0000000000..50f3f61988 --- /dev/null +++ b/src/changelog/.2.x.x/1782_allow_multiple_servletcontextlistener_instances.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to you under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="http://logging.apache.org/log4j/changelog" + xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.0.xsd" + type="fixed"> + <issue id="1782" link="https://github.com/apache/logging-log4j2/issues/1782"/> + <author name="Christian Seewald" id="github:cseewald"/> + <author id="github:ppkarwasz"/> + <description format="asciidoc"> + Only shutdown Log4j after last `Log4jServletContextListener` is executed. + </description> +</entry>
