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 e14bc11a2eb..e6d58f66c79 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.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 @@ void getLoggers_can_be_updated_concurrently(final TestInfo testInfo) { 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 2e576d226b8..4bfa9f5d6ad 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 bf2f77383c0..6db471cb87a 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 static LoggerContext getContext( return (LoggerContext) LogManager.getContext(loader, currentContext, configLocation); } + /** + * Starts the context using the configuration specified by {@link #getConfigLocation()}. + *
+ * 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 + * + * Log4j 2 Configuration File Location documentation. + *
+ */ @Override public void start() { LOGGER.debug("Starting LoggerContext[name={}, {}]...", getName(), this); @@ -312,21 +322,31 @@ public void start() { } /** - * Starts with a specific configuration. - * - * @param config The new Configuration. + * Starts the context using a specific configuration. + *+ * Warning: 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. + *
+ * @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 00000000000..84416d9c546 --- /dev/null +++ b/src/changelog/.2.x.x/3770_LoggerContext_start.xml @@ -0,0 +1,12 @@ + +