Skip to content

Commit 9b66389

Browse files
authored
fix: Restore Backward Compatibility with Spring Boot Reconfiguration (#3773)
### feat: add tests for `LoggerContext.start` behavior Add test verifying expected behavior of `LoggerContext.start(Configuration)` to ensure backward compatibility: - The configuration must always be replaced, even if the context has already started. - Only the first configuration should register the shutdown hook. ### fix: Restore Backward Compatibility with Spring Boot Reconfiguration Although Spring Boot never directly starts a `LoggerContext`, its logging system — including our `Log4j2SpringBootLoggingSystem` and equivalents in Spring Boot 2.x and 3.x — has consistently used `LoggerContext.start(Configuration)` for reconfiguration. This use case was not taken into consideration in #2614, causing a regression for Spring Boot users. To maintain backward compatibility with these usages, `start(Configuration)` now falls back to `reconfigure(Configuration)` if the context is already started. Closes #3770
1 parent f93b6b1 commit 9b66389

File tree

4 files changed

+131
-14
lines changed

4 files changed

+131
-14
lines changed

log4j-core-test/src/test/java/org/apache/logging/log4j/core/LoggerContextTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,26 @@
1717
package org.apache.logging.log4j.core;
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.mockito.Mockito.CALLS_REAL_METHODS;
2021
import static org.mockito.Mockito.mock;
22+
import static org.mockito.Mockito.withSettings;
2123

2224
import java.util.Collection;
2325
import java.util.concurrent.ExecutorService;
2426
import java.util.concurrent.Executors;
2527
import java.util.concurrent.Future;
2628
import java.util.stream.Collectors;
2729
import java.util.stream.IntStream;
30+
import org.apache.logging.log4j.core.config.AbstractConfiguration;
31+
import org.apache.logging.log4j.core.config.Configuration;
32+
import org.apache.logging.log4j.core.config.ConfigurationSource;
33+
import org.apache.logging.log4j.core.config.DefaultConfiguration;
2834
import org.apache.logging.log4j.message.MessageFactory;
2935
import org.apache.logging.log4j.message.MessageFactory2;
3036
import org.junit.jupiter.api.Assertions;
3137
import org.junit.jupiter.api.Test;
3238
import org.junit.jupiter.api.TestInfo;
39+
import org.junitpioneer.jupiter.Issue;
3340

3441
class LoggerContextTest {
3542

@@ -75,4 +82,23 @@ void getLoggers_can_be_updated_concurrently(final TestInfo testInfo) {
7582
executorService.shutdown();
7683
}
7784
}
85+
86+
@Test
87+
@Issue("https://github.com/apache/logging-log4j2/issues/3770")
88+
void start_should_fallback_on_reconfigure_if_context_already_started(final TestInfo testInfo) {
89+
final String testName = testInfo.getDisplayName();
90+
try (final LoggerContext loggerContext = new LoggerContext(testName)) {
91+
loggerContext.start();
92+
assertThat(loggerContext.isStarted()).isTrue();
93+
assertThat(loggerContext.getConfiguration()).isInstanceOf(DefaultConfiguration.class);
94+
// Start
95+
Configuration configuration = mock(
96+
AbstractConfiguration.class,
97+
withSettings()
98+
.useConstructor(null, ConfigurationSource.NULL_SOURCE)
99+
.defaultAnswer(CALLS_REAL_METHODS));
100+
loggerContext.start(configuration);
101+
assertThat(loggerContext.getConfiguration()).isSameAs(configuration);
102+
}
103+
}
78104
}

log4j-core-test/src/test/java/org/apache/logging/log4j/core/ShutdownDisabledTest.java

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,84 @@
1616
*/
1717
package org.apache.logging.log4j.core;
1818

19-
import static org.junit.jupiter.api.Assertions.assertFalse;
20-
import static org.junit.jupiter.api.Assertions.assertNull;
19+
import static org.apache.logging.log4j.core.util.ReflectionUtil.getFieldValue;
20+
import static org.assertj.core.api.Assertions.assertThat;
21+
import static org.mockito.Mockito.CALLS_REAL_METHODS;
22+
import static org.mockito.Mockito.mock;
23+
import static org.mockito.Mockito.when;
24+
import static org.mockito.Mockito.withSettings;
2125

2226
import java.lang.reflect.Field;
27+
import org.apache.logging.log4j.core.config.AbstractConfiguration;
2328
import org.apache.logging.log4j.core.config.Configuration;
29+
import org.apache.logging.log4j.core.config.ConfigurationSource;
2430
import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
25-
import org.apache.logging.log4j.core.util.ReflectionUtil;
2631
import org.apache.logging.log4j.test.junit.SetTestProperty;
2732
import org.junit.jupiter.api.Test;
33+
import org.junit.jupiter.api.TestInfo;
2834

2935
@SetTestProperty(key = "log4j2.isWebapp", value = "false")
30-
@LoggerContextSource("log4j-test3.xml")
3136
class ShutdownDisabledTest {
3237

38+
private static final Field shutdownCallbackField;
39+
40+
static {
41+
try {
42+
shutdownCallbackField = LoggerContext.class.getDeclaredField("shutdownCallback");
43+
} catch (NoSuchFieldException e) {
44+
throw new RuntimeException(e);
45+
}
46+
}
47+
48+
@Test
49+
@LoggerContextSource("log4j-test3.xml")
50+
void testShutdownFlag(final Configuration config, final LoggerContext ctx) {
51+
assertThat(config.isShutdownHookEnabled())
52+
.as("Shutdown hook is enabled")
53+
.isFalse();
54+
assertThat(getFieldValue(shutdownCallbackField, ctx))
55+
.as("Shutdown callback")
56+
.isNull();
57+
}
58+
3359
@Test
34-
void testShutdownFlag(final Configuration config, final LoggerContext ctx) throws NoSuchFieldException {
35-
Field shutdownCallback = LoggerContext.class.getDeclaredField("shutdownCallback");
36-
Object fieldValue = ReflectionUtil.getFieldValue(shutdownCallback, ctx);
37-
assertFalse(config.isShutdownHookEnabled(), "Shutdown hook is enabled");
38-
assertNull(fieldValue, "Shutdown callback");
60+
void whenLoggerContextInitialized_respectsShutdownDisabled(TestInfo testInfo) {
61+
Configuration configuration = mockConfiguration();
62+
when(configuration.isShutdownHookEnabled()).thenReturn(false);
63+
try (final LoggerContext ctx = new LoggerContext(testInfo.getDisplayName())) {
64+
ctx.start(configuration);
65+
assertThat(ctx.isStarted()).isTrue();
66+
assertThat(ctx.getConfiguration()).isSameAs(configuration);
67+
assertThat(getFieldValue(shutdownCallbackField, ctx))
68+
.as("Shutdown callback")
69+
.isNull();
70+
}
71+
}
72+
73+
@Test
74+
void whenLoggerContextStarted_ignoresShutdownDisabled(TestInfo testInfo) {
75+
// Traditional behavior: during reconfiguration, the shutdown hook is not removed.
76+
Configuration initialConfiguration = mockConfiguration();
77+
when(initialConfiguration.isShutdownHookEnabled()).thenReturn(true);
78+
Configuration configuration = mockConfiguration();
79+
when(configuration.isShutdownHookEnabled()).thenReturn(false);
80+
try (final LoggerContext ctx = new LoggerContext(testInfo.getDisplayName())) {
81+
ctx.start(initialConfiguration);
82+
assertThat(ctx.isStarted()).isTrue();
83+
Object shutdownCallback = getFieldValue(shutdownCallbackField, ctx);
84+
assertThat(shutdownCallback).as("Shutdown callback").isNotNull();
85+
ctx.start(configuration);
86+
assertThat(getFieldValue(shutdownCallbackField, ctx))
87+
.as("Shutdown callback")
88+
.isSameAs(shutdownCallback);
89+
}
90+
}
91+
92+
private static Configuration mockConfiguration() {
93+
return mock(
94+
AbstractConfiguration.class,
95+
withSettings()
96+
.useConstructor(null, ConfigurationSource.NULL_SOURCE)
97+
.defaultAnswer(CALLS_REAL_METHODS));
3998
}
4099
}

log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,16 @@ public static LoggerContext getContext(
286286
return (LoggerContext) LogManager.getContext(loader, currentContext, configLocation);
287287
}
288288

289+
/**
290+
* Starts the context using the configuration specified by {@link #getConfigLocation()}.
291+
* <p>
292+
* If the configuration location is {@code null}, Log4j will search for a configuration file
293+
* using the default classpath resources. For details on the search order and supported formats,
294+
* see the
295+
* <a href="https://logging.apache.org/log4j/2.x/manual/configuration.html#automatic-configuration">
296+
* Log4j 2 Configuration File Location documentation</a>.
297+
* </p>
298+
*/
289299
@Override
290300
public void start() {
291301
LOGGER.debug("Starting LoggerContext[name={}, {}]...", getName(), this);
@@ -312,21 +322,31 @@ public void start() {
312322
}
313323

314324
/**
315-
* Starts with a specific configuration.
316-
*
317-
* @param config The new Configuration.
325+
* Starts the context using a specific configuration.
326+
* <p>
327+
* <strong>Warning:</strong> For backward compatibility, especially with Spring Boot,
328+
* if the context is already started, this method will fall back to {@link #reconfigure(Configuration)}.
329+
* This behavior is maintained for legacy integrations and may change in future major versions.
330+
* New code should not rely on this fallback.
331+
* </p>
332+
* @param config The new {@link Configuration} to use for this context
318333
*/
319334
public void start(final Configuration config) {
320335
LOGGER.info("Starting {}[name={}] with configuration {}...", getClass().getSimpleName(), getName(), config);
321336
if (configLock.tryLock()) {
322337
try {
323-
if (this.isInitialized() || this.isStopped()) {
338+
if (isInitialized() || isStopped()) {
324339
setStarting();
325340
reconfigure(config);
326341
if (this.configuration.isShutdownHookEnabled()) {
327342
setUpShutdownHook();
328343
}
329-
this.setStarted();
344+
setStarted();
345+
} else {
346+
// Required for Spring Boot integration:
347+
// Both `Log4jSpringBootLoggingSystem` and its Spring Boot 3.x equivalent
348+
// invoke `start()` even during context reconfiguration.
349+
reconfigure(config);
330350
}
331351
} finally {
332352
configLock.unlock();
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<entry xmlns="https://logging.apache.org/xml/ns"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="
5+
https://logging.apache.org/xml/ns
6+
https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
7+
type="fixed">
8+
<issue id="3770" link="https://github.com/apache/logging-log4j2/issues/3770"/>
9+
<description format="asciidoc">
10+
Restore backward compatibility with the Spring Boot reconfiguration process.
11+
</description>
12+
</entry>

0 commit comments

Comments
 (0)