Skip to content

Commit d620881

Browse files
committed
Allow multiple Log4jServletContextListener registrations
This closes #1782.
1 parent 05afc58 commit d620881

File tree

5 files changed

+154
-14
lines changed

5 files changed

+154
-14
lines changed

log4j-jakarta-web/src/main/java/org/apache/logging/log4j/web/Log4jServletContextListener.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
*/
3737
public class Log4jServletContextListener implements ServletContextListener {
3838

39+
static final String START_COUNT_ATTR = Log4jServletContextListener.class.getName() + ".START_COUNT";
40+
3941
private static final int DEFAULT_STOP_TIMEOUT = 30;
4042
private static final TimeUnit DEFAULT_STOP_TIMEOUT_TIMEUNIT = TimeUnit.SECONDS;
4143

@@ -47,10 +49,36 @@ public class Log4jServletContextListener implements ServletContextListener {
4749
private ServletContext servletContext;
4850
private Log4jWebLifeCycle initializer;
4951

52+
private int getAndIncrementCount() {
53+
Integer count = (Integer) servletContext.getAttribute(START_COUNT_ATTR);
54+
if (count == null) {
55+
count = 0;
56+
}
57+
servletContext.setAttribute(START_COUNT_ATTR, count + 1);
58+
return count;
59+
}
60+
61+
private int decrementAndGetCount() {
62+
Integer count = (Integer) servletContext.getAttribute(START_COUNT_ATTR);
63+
if (count == null) {
64+
LOGGER.warn(
65+
"{} received a 'contextDestroyed' message without a corresponding 'contextInitialized' message.",
66+
getClass().getName());
67+
count = 1;
68+
}
69+
servletContext.setAttribute(START_COUNT_ATTR, --count);
70+
return count;
71+
}
72+
5073
@Override
5174
public void contextInitialized(final ServletContextEvent event) {
5275
this.servletContext = event.getServletContext();
53-
LOGGER.debug("Log4jServletContextListener ensuring that Log4j starts up properly.");
76+
if (getAndIncrementCount() != 0) {
77+
LOGGER.debug("Skipping Log4j context initialization, since {} is registered multiple times.",
78+
getClass().getSimpleName());
79+
return;
80+
}
81+
LOGGER.info("Log4j context initialization by {}.", getClass().getSimpleName());
5482

5583
if ("true".equalsIgnoreCase(servletContext.getInitParameter(
5684
Log4jWebSupport.IS_LOG4J_AUTO_SHUTDOWN_DISABLED))) {
@@ -72,10 +100,15 @@ public void contextInitialized(final ServletContextEvent event) {
72100
@Override
73101
public void contextDestroyed(final ServletContextEvent event) {
74102
if (this.servletContext == null || this.initializer == null) {
75-
LOGGER.warn("Context destroyed before it was initialized.");
103+
LOGGER.warn("Servlet context destroyed before it was initialized.");
104+
return;
105+
}
106+
if (decrementAndGetCount() != 0) {
107+
LOGGER.debug("Skipping Log4j context shutdown, since {} is registered multiple times.",
108+
getClass().getSimpleName());
76109
return;
77110
}
78-
LOGGER.debug("Log4jServletContextListener ensuring that Log4j shuts down properly.");
111+
LOGGER.info("Log4j context shutdown by {}.", getClass().getSimpleName());
79112

80113
this.initializer.clearLoggerContext(); // the application is finished
81114
// shutting down now

log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/Log4jServletContextListenerTest.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
*/
1717
package org.apache.logging.log4j.web;
1818

19+
import java.util.concurrent.atomic.AtomicReference;
20+
1921
import jakarta.servlet.ServletContext;
2022
import jakarta.servlet.ServletContextEvent;
2123

@@ -26,11 +28,15 @@
2628
import org.mockito.Mock;
2729
import org.mockito.junit.jupiter.MockitoExtension;
2830

29-
import static org.junit.jupiter.api.Assertions.*;
30-
import static org.mockito.BDDMockito.eq;
31+
import static org.junit.jupiter.api.Assertions.assertEquals;
32+
import static org.junit.jupiter.api.Assertions.fail;
33+
import static org.mockito.AdditionalAnswers.answerVoid;
34+
import static org.mockito.ArgumentMatchers.any;
35+
import static org.mockito.ArgumentMatchers.eq;
3136
import static org.mockito.BDDMockito.given;
3237
import static org.mockito.BDDMockito.then;
3338
import static org.mockito.BDDMockito.willThrow;
39+
import static org.mockito.Mockito.doAnswer;
3440

3541
@ExtendWith(MockitoExtension.class)
3642
public class Log4jServletContextListenerTest {
@@ -46,21 +52,38 @@ public class Log4jServletContextListenerTest {
4652

4753
private Log4jServletContextListener listener;
4854

55+
private final AtomicReference<Object> count = new AtomicReference<>();
56+
4957
@BeforeEach
5058
public void setUp() {
5159
this.listener = new Log4jServletContextListener();
5260
given(event.getServletContext()).willReturn(servletContext);
5361
given(servletContext.getAttribute(Log4jWebSupport.SUPPORT_ATTRIBUTE)).willReturn(initializer);
62+
63+
doAnswer(answerVoid((k, v) -> count.set(v)))
64+
.when(servletContext)
65+
.setAttribute(eq(Log4jServletContextListener.START_COUNT_ATTR), any());
66+
doAnswer(__ -> count.get())
67+
.when(servletContext)
68+
.getAttribute(Log4jServletContextListener.START_COUNT_ATTR);
5469
}
5570

5671
@Test
5772
public void testInitAndDestroy() throws Exception {
58-
this.listener.contextInitialized(this.event);
73+
listener.contextInitialized(event);
5974

6075
then(initializer).should().start();
6176
then(initializer).should().setLoggerContext();
6277

63-
this.listener.contextDestroyed(this.event);
78+
listener.contextInitialized(event);
79+
80+
then(initializer).shouldHaveNoMoreInteractions();
81+
82+
listener.contextDestroyed(event);
83+
84+
then(initializer).shouldHaveNoMoreInteractions();
85+
86+
listener.contextDestroyed(event);
6487

6588
then(initializer).should().clearLoggerContext();
6689
then(initializer).should().stop();

log4j-web/src/main/java/org/apache/logging/log4j/web/Log4jServletContextListener.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
*/
3737
public class Log4jServletContextListener implements ServletContextListener {
3838

39+
static final String START_COUNT_ATTR = Log4jServletContextListener.class.getName() + ".START_COUNT";
40+
3941
private static final int DEFAULT_STOP_TIMEOUT = 30;
4042
private static final TimeUnit DEFAULT_STOP_TIMEOUT_TIMEUNIT = TimeUnit.SECONDS;
4143

@@ -47,10 +49,36 @@ public class Log4jServletContextListener implements ServletContextListener {
4749
private ServletContext servletContext;
4850
private Log4jWebLifeCycle initializer;
4951

52+
private int getAndIncrementCount() {
53+
Integer count = (Integer) servletContext.getAttribute(START_COUNT_ATTR);
54+
if (count == null) {
55+
count = 0;
56+
}
57+
servletContext.setAttribute(START_COUNT_ATTR, count + 1);
58+
return count;
59+
}
60+
61+
private int decrementAndGetCount() {
62+
Integer count = (Integer) servletContext.getAttribute(START_COUNT_ATTR);
63+
if (count == null) {
64+
LOGGER.warn(
65+
"{} received a 'contextDestroyed' message without a corresponding 'contextInitialized' message.",
66+
getClass().getName());
67+
count = 1;
68+
}
69+
servletContext.setAttribute(START_COUNT_ATTR, --count);
70+
return count;
71+
}
72+
5073
@Override
5174
public void contextInitialized(final ServletContextEvent event) {
5275
this.servletContext = event.getServletContext();
53-
LOGGER.debug("Log4jServletContextListener ensuring that Log4j starts up properly.");
76+
if (getAndIncrementCount() != 0) {
77+
LOGGER.debug("Skipping Log4j context initialization, since {} is registered multiple times.",
78+
getClass().getSimpleName());
79+
return;
80+
}
81+
LOGGER.info("Log4j context initialization by {}.", getClass().getSimpleName());
5482

5583
if ("true".equalsIgnoreCase(servletContext.getInitParameter(
5684
Log4jWebSupport.IS_LOG4J_AUTO_SHUTDOWN_DISABLED))) {
@@ -72,10 +100,15 @@ public void contextInitialized(final ServletContextEvent event) {
72100
@Override
73101
public void contextDestroyed(final ServletContextEvent event) {
74102
if (this.servletContext == null || this.initializer == null) {
75-
LOGGER.warn("Context destroyed before it was initialized.");
103+
LOGGER.warn("Servlet context destroyed before it was initialized.");
104+
return;
105+
}
106+
if (decrementAndGetCount() != 0) {
107+
LOGGER.debug("Skipping Log4j context shutdown, since {} is registered multiple times.",
108+
getClass().getSimpleName());
76109
return;
77110
}
78-
LOGGER.debug("Log4jServletContextListener ensuring that Log4j shuts down properly.");
111+
LOGGER.info("Log4j context shutdown by {}.", getClass().getSimpleName());
79112

80113
this.initializer.clearLoggerContext(); // the application is finished
81114
// shutting down now

log4j-web/src/test/java/org/apache/logging/log4j/web/Log4jServletContextListenerTest.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
*/
1717
package org.apache.logging.log4j.web;
1818

19+
import java.util.concurrent.atomic.AtomicReference;
20+
1921
import javax.servlet.ServletContext;
2022
import javax.servlet.ServletContextEvent;
2123

@@ -26,11 +28,15 @@
2628
import org.mockito.Mock;
2729
import org.mockito.junit.jupiter.MockitoExtension;
2830

29-
import static org.junit.jupiter.api.Assertions.*;
30-
import static org.mockito.BDDMockito.eq;
31+
import static org.junit.jupiter.api.Assertions.assertEquals;
32+
import static org.junit.jupiter.api.Assertions.fail;
33+
import static org.mockito.AdditionalAnswers.answerVoid;
34+
import static org.mockito.ArgumentMatchers.any;
35+
import static org.mockito.ArgumentMatchers.eq;
3136
import static org.mockito.BDDMockito.given;
3237
import static org.mockito.BDDMockito.then;
3338
import static org.mockito.BDDMockito.willThrow;
39+
import static org.mockito.Mockito.doAnswer;
3440

3541
@ExtendWith(MockitoExtension.class)
3642
public class Log4jServletContextListenerTest {
@@ -46,21 +52,38 @@ public class Log4jServletContextListenerTest {
4652

4753
private Log4jServletContextListener listener;
4854

55+
private final AtomicReference<Object> count = new AtomicReference<>();
56+
4957
@BeforeEach
5058
public void setUp() {
5159
this.listener = new Log4jServletContextListener();
5260
given(event.getServletContext()).willReturn(servletContext);
5361
given(servletContext.getAttribute(Log4jWebSupport.SUPPORT_ATTRIBUTE)).willReturn(initializer);
62+
63+
doAnswer(answerVoid((k, v) -> count.set(v)))
64+
.when(servletContext)
65+
.setAttribute(eq(Log4jServletContextListener.START_COUNT_ATTR), any());
66+
doAnswer(__ -> count.get())
67+
.when(servletContext)
68+
.getAttribute(Log4jServletContextListener.START_COUNT_ATTR);
5469
}
5570

5671
@Test
5772
public void testInitAndDestroy() throws Exception {
58-
this.listener.contextInitialized(this.event);
73+
listener.contextInitialized(event);
5974

6075
then(initializer).should().start();
6176
then(initializer).should().setLoggerContext();
6277

63-
this.listener.contextDestroyed(this.event);
78+
listener.contextInitialized(event);
79+
80+
then(initializer).shouldHaveNoMoreInteractions();
81+
82+
listener.contextDestroyed(event);
83+
84+
then(initializer).shouldHaveNoMoreInteractions();
85+
86+
listener.contextDestroyed(event);
6487

6588
then(initializer).should().clearLoggerContext();
6689
then(initializer).should().stop();
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Licensed to the Apache Software Foundation (ASF) under one or more
4+
~ contributor license agreements. See the NOTICE file distributed with
5+
~ this work for additional information regarding copyright ownership.
6+
~ The ASF licenses this file to you under the Apache License, Version 2.0
7+
~ (the "License"); you may not use this file except in compliance with
8+
~ the License. You may obtain a copy of the License at
9+
~
10+
~ http://www.apache.org/licenses/LICENSE-2.0
11+
~
12+
~ Unless required by applicable law or agreed to in writing, software
13+
~ distributed under the License is distributed on an "AS IS" BASIS,
14+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
~ See the License for the specific language governing permissions and
16+
~ limitations under the License.
17+
-->
18+
<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
19+
xmlns="http://logging.apache.org/log4j/changelog"
20+
xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.0.xsd"
21+
type="fixed">
22+
<issue id="1782" link="https://github.com/apache/logging-log4j2/issues/1782"/>
23+
<author name="Christian Seewald" id="github:cseewald"/>
24+
<author id="github:ppkarwasz"/>
25+
<description format="asciidoc">
26+
Only shutdown Log4j after last `Log4jServletContextListener` is executed.
27+
</description>
28+
</entry>

0 commit comments

Comments
 (0)