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>

Reply via email to