This is an automated email from the ASF dual-hosted git repository.
pkarwasz pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
The following commit(s) were added to refs/heads/2.x by this push:
new 9b663899eb fix: Restore Backward Compatibility with Spring Boot
Reconfiguration (#3773)
9b663899eb is described below
commit 9b663899ebe45f441d7b16e11b04b9a1ac23e98b
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Sat Jun 28 09:26:43 2025 +0200
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
---
.../logging/log4j/core/LoggerContextTest.java | 26 ++++++++
.../logging/log4j/core/ShutdownDisabledTest.java | 77 +++++++++++++++++++---
.../apache/logging/log4j/core/LoggerContext.java | 30 +++++++--
src/changelog/.2.x.x/3770_LoggerContext_start.xml | 12 ++++
4 files changed, 131 insertions(+), 14 deletions(-)
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/LoggerContextTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/LoggerContextTest.java
index e14bc11a2e..e6d58f66c7 100644
---
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/LoggerContextTest.java
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/LoggerContextTest.java
@@ -17,7 +17,9 @@
package org.apache.logging.log4j.core;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.withSettings;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
@@ -25,11 +27,16 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
+import org.apache.logging.log4j.core.config.AbstractConfiguration;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.ConfigurationSource;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
import org.apache.logging.log4j.message.MessageFactory;
import org.apache.logging.log4j.message.MessageFactory2;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
+import org.junitpioneer.jupiter.Issue;
class LoggerContextTest {
@@ -75,4 +82,23 @@ class LoggerContextTest {
executorService.shutdown();
}
}
+
+ @Test
+ @Issue("https://github.com/apache/logging-log4j2/issues/3770")
+ void start_should_fallback_on_reconfigure_if_context_already_started(final
TestInfo testInfo) {
+ final String testName = testInfo.getDisplayName();
+ try (final LoggerContext loggerContext = new LoggerContext(testName)) {
+ loggerContext.start();
+ assertThat(loggerContext.isStarted()).isTrue();
+
assertThat(loggerContext.getConfiguration()).isInstanceOf(DefaultConfiguration.class);
+ // Start
+ Configuration configuration = mock(
+ AbstractConfiguration.class,
+ withSettings()
+ .useConstructor(null,
ConfigurationSource.NULL_SOURCE)
+ .defaultAnswer(CALLS_REAL_METHODS));
+ loggerContext.start(configuration);
+
assertThat(loggerContext.getConfiguration()).isSameAs(configuration);
+ }
+ }
}
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/ShutdownDisabledTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/ShutdownDisabledTest.java
index 2e576d226b..4bfa9f5d6a 100644
---
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/ShutdownDisabledTest.java
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/ShutdownDisabledTest.java
@@ -16,25 +16,84 @@
*/
package org.apache.logging.log4j.core;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.apache.logging.log4j.core.util.ReflectionUtil.getFieldValue;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
import java.lang.reflect.Field;
+import org.apache.logging.log4j.core.config.AbstractConfiguration;
import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.ConfigurationSource;
import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
-import org.apache.logging.log4j.core.util.ReflectionUtil;
import org.apache.logging.log4j.test.junit.SetTestProperty;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
@SetTestProperty(key = "log4j2.isWebapp", value = "false")
-@LoggerContextSource("log4j-test3.xml")
class ShutdownDisabledTest {
+ private static final Field shutdownCallbackField;
+
+ static {
+ try {
+ shutdownCallbackField =
LoggerContext.class.getDeclaredField("shutdownCallback");
+ } catch (NoSuchFieldException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ @LoggerContextSource("log4j-test3.xml")
+ void testShutdownFlag(final Configuration config, final LoggerContext ctx)
{
+ assertThat(config.isShutdownHookEnabled())
+ .as("Shutdown hook is enabled")
+ .isFalse();
+ assertThat(getFieldValue(shutdownCallbackField, ctx))
+ .as("Shutdown callback")
+ .isNull();
+ }
+
@Test
- void testShutdownFlag(final Configuration config, final LoggerContext ctx)
throws NoSuchFieldException {
- Field shutdownCallback =
LoggerContext.class.getDeclaredField("shutdownCallback");
- Object fieldValue = ReflectionUtil.getFieldValue(shutdownCallback,
ctx);
- assertFalse(config.isShutdownHookEnabled(), "Shutdown hook is
enabled");
- assertNull(fieldValue, "Shutdown callback");
+ void whenLoggerContextInitialized_respectsShutdownDisabled(TestInfo
testInfo) {
+ Configuration configuration = mockConfiguration();
+ when(configuration.isShutdownHookEnabled()).thenReturn(false);
+ try (final LoggerContext ctx = new
LoggerContext(testInfo.getDisplayName())) {
+ ctx.start(configuration);
+ assertThat(ctx.isStarted()).isTrue();
+ assertThat(ctx.getConfiguration()).isSameAs(configuration);
+ assertThat(getFieldValue(shutdownCallbackField, ctx))
+ .as("Shutdown callback")
+ .isNull();
+ }
+ }
+
+ @Test
+ void whenLoggerContextStarted_ignoresShutdownDisabled(TestInfo testInfo) {
+ // Traditional behavior: during reconfiguration, the shutdown hook is
not removed.
+ Configuration initialConfiguration = mockConfiguration();
+ when(initialConfiguration.isShutdownHookEnabled()).thenReturn(true);
+ Configuration configuration = mockConfiguration();
+ when(configuration.isShutdownHookEnabled()).thenReturn(false);
+ try (final LoggerContext ctx = new
LoggerContext(testInfo.getDisplayName())) {
+ ctx.start(initialConfiguration);
+ assertThat(ctx.isStarted()).isTrue();
+ Object shutdownCallback = getFieldValue(shutdownCallbackField,
ctx);
+ assertThat(shutdownCallback).as("Shutdown callback").isNotNull();
+ ctx.start(configuration);
+ assertThat(getFieldValue(shutdownCallbackField, ctx))
+ .as("Shutdown callback")
+ .isSameAs(shutdownCallback);
+ }
+ }
+
+ private static Configuration mockConfiguration() {
+ return mock(
+ AbstractConfiguration.class,
+ withSettings()
+ .useConstructor(null, ConfigurationSource.NULL_SOURCE)
+ .defaultAnswer(CALLS_REAL_METHODS));
}
}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
index bf2f77383c..6db471cb87 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
@@ -286,6 +286,16 @@ public class LoggerContext extends AbstractLifeCycle
return (LoggerContext) LogManager.getContext(loader, currentContext,
configLocation);
}
+ /**
+ * Starts the context using the configuration specified by {@link
#getConfigLocation()}.
+ * <p>
+ * If the configuration location is {@code null}, Log4j will search for
a configuration file
+ * using the default classpath resources. For details on the search
order and supported formats,
+ * see the
+ * <a
href="https://logging.apache.org/log4j/2.x/manual/configuration.html#automatic-configuration">
+ * Log4j 2 Configuration File Location documentation</a>.
+ * </p>
+ */
@Override
public void start() {
LOGGER.debug("Starting LoggerContext[name={}, {}]...", getName(),
this);
@@ -312,21 +322,31 @@ public class LoggerContext extends AbstractLifeCycle
}
/**
- * Starts with a specific configuration.
- *
- * @param config The new Configuration.
+ * Starts the context using a specific configuration.
+ * <p>
+ * <strong>Warning:</strong> For backward compatibility, especially with
Spring Boot,
+ * if the context is already started, this method will fall back to
{@link #reconfigure(Configuration)}.
+ * This behavior is maintained for legacy integrations and may change in
future major versions.
+ * New code should not rely on this fallback.
+ * </p>
+ * @param config The new {@link Configuration} to use for this context
*/
public void start(final Configuration config) {
LOGGER.info("Starting {}[name={}] with configuration {}...",
getClass().getSimpleName(), getName(), config);
if (configLock.tryLock()) {
try {
- if (this.isInitialized() || this.isStopped()) {
+ if (isInitialized() || isStopped()) {
setStarting();
reconfigure(config);
if (this.configuration.isShutdownHookEnabled()) {
setUpShutdownHook();
}
- this.setStarted();
+ setStarted();
+ } else {
+ // Required for Spring Boot integration:
+ // Both `Log4jSpringBootLoggingSystem` and its Spring Boot
3.x equivalent
+ // invoke `start()` even during context reconfiguration.
+ reconfigure(config);
}
} finally {
configLock.unlock();
diff --git a/src/changelog/.2.x.x/3770_LoggerContext_start.xml
b/src/changelog/.2.x.x/3770_LoggerContext_start.xml
new file mode 100644
index 0000000000..84416d9c54
--- /dev/null
+++ b/src/changelog/.2.x.x/3770_LoggerContext_start.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ https://logging.apache.org/xml/ns
+ https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+ type="fixed">
+ <issue id="3770"
link="https://github.com/apache/logging-log4j2/issues/3770"/>
+ <description format="asciidoc">
+ Restore backward compatibility with the Spring Boot reconfiguration
process.
+ </description>
+</entry>