diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggerContext.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggerContext.java index d5a9119a1e3..7a75ce27f75 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggerContext.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggerContext.java @@ -34,7 +34,10 @@ public interface LoggerContext { /** * Gets the anchor for some other context, such as a ClassLoader or ServletContext. * @return The external context. + * @deprecated Use {@link #getObject(String)} instead. + * @since 2.27.0 */ + @Deprecated Object getExternalContext(); /** 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 e6d58f66c79..074fdbb994b 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,6 +17,7 @@ package org.apache.logging.log4j.core; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.withSettings; @@ -101,4 +102,23 @@ void start_should_fallback_on_reconfigure_if_context_already_started(final TestI assertThat(loggerContext.getConfiguration()).isSameAs(configuration); } } + + @Test + public void testLegacyExternalContextCompatibility() { + LoggerContext ctx = new LoggerContext("TestContext"); + String legacyValue = "Spring-Boot-Flag"; + ctx.setExternalContext(legacyValue); + assertEquals(legacyValue, ctx.getExternalContext()); + assertEquals(legacyValue, ctx.getObject("__EXTERNAL_CONTEXT_KEY__")); + } + + @Test + public void testCollisionPrevention() { + LoggerContext ctx = new LoggerContext("CollisionTest"); + ctx.setExternalContext("Spring-Flag"); + String mockServletContext = "MockServletContext"; + ctx.putObject("org.apache.logging.log4j.web.servletContext", mockServletContext); + assertEquals("Spring-Flag", ctx.getExternalContext()); + assertEquals(mockServletContext, ctx.getObject("org.apache.logging.log4j.web.servletContext")); + } } 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 da62cc24855..457d2154b7e 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 @@ -152,7 +152,10 @@ public LoggerContext(final String name) { * * @param name The context name. * @param externalContext The external context. + * @deprecated Use {@link #LoggerContext(String)} and {@link #putObject(String, Object)} instead. + * @since 2.27.0 */ + @Deprecated public LoggerContext(final String name, final Object externalContext) { this(name, externalContext, (URI) null); } @@ -163,11 +166,14 @@ public LoggerContext(final String name, final Object externalContext) { * @param name The context name. * @param externalContext The external context. * @param configLocn The location of the configuration as a URI. + * @deprecated Use {@link #LoggerContext(String)} and {@link #putObject(String, Object)} instead. + * @since 2.27.0 */ + @Deprecated public LoggerContext(final String name, final Object externalContext, final URI configLocn) { this.contextName = name; if (externalContext != null) { - externalMap.put(EXTERNAL_CONTEXT_KEY, externalContext); + this.putObject(EXTERNAL_CONTEXT_KEY, externalContext); } this.configLocation = configLocn; } @@ -531,7 +537,10 @@ public boolean removeObject(final String key, final Object value) { * Sets the external context. * * @param context The external context. + * @deprecated Use {@link #putObject(String, Object)} instead. + * @since 2.27.0 */ + @Deprecated public void setExternalContext(final Object context) { if (context != null) { this.externalMap.put(EXTERNAL_CONTEXT_KEY, context); @@ -544,8 +553,11 @@ public void setExternalContext(final Object context) { * Returns the external context. * * @return The external context. + * @deprecated Use {@link #getObject(String)} instead. + * @since 2.27.0 */ @Override + @Deprecated public Object getExternalContext() { return this.externalMap.get(EXTERNAL_CONTEXT_KEY); } @@ -766,7 +778,7 @@ public void setConfigLocation(final URI configLocation) { * Reconfigures the context. */ private void reconfigure(final URI configURI) { - final Object externalContext = externalMap.get(EXTERNAL_CONTEXT_KEY); + final Object externalContext = getExternalContext(); final ClassLoader cl = externalContext instanceof ClassLoader ? (ClassLoader) externalContext : null; LOGGER.debug( "Reconfiguration started for context[name={}] at URI {} ({}) with optional ClassLoader: {}", diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jContextFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jContextFactory.java index bc892767746..d69548c2bb9 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jContextFactory.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jContextFactory.java @@ -148,8 +148,11 @@ private void initializeShutdownCallbackRegistry() { * for the caller if a more appropriate Context can be determined. * @param externalContext An external context (such as a ServletContext) to be associated with the LoggerContext. * @return The LoggerContext. + * @deprecated Use {@link org.apache.logging.log4j.spi.LoggerContext#putObject(String, Object)} instead. + * @since 2.27.0 */ @Override + @Deprecated public LoggerContext getContext( final String fqcn, final ClassLoader loader, final Object externalContext, final boolean currentContext) { final LoggerContext ctx = selector.getContext(fqcn, loader, currentContext); @@ -171,7 +174,10 @@ public LoggerContext getContext( * for the caller if a more appropriate Context can be determined. * @param source The configuration source. * @return The LoggerContext. + * @deprecated Use {@link org.apache.logging.log4j.spi.LoggerContext#putObject(String, Object)} instead. + * @since 2.27.0 */ + @Deprecated public LoggerContext getContext( final String fqcn, final ClassLoader loader, @@ -205,7 +211,10 @@ public LoggerContext getContext( * for the caller if a more appropriate Context can be determined. * @param configuration The Configuration. * @return The LoggerContext. + * @deprecated Use {@link org.apache.logging.log4j.spi.LoggerContext#putObject(String, Object)} instead. + * @since 2.27.0 */ + @Deprecated public LoggerContext getContext( final String fqcn, final ClassLoader loader, @@ -236,8 +245,11 @@ public LoggerContext getContext( * for the caller if a more appropriate Context can be determined. * @param configLocation The location of the configuration for the LoggerContext (or null). * @return The LoggerContext. + * @deprecated Use {@link org.apache.logging.log4j.spi.LoggerContext#putObject(String, Object)} instead. + * @since 2.27.0 */ @Override + @Deprecated public LoggerContext getContext( final String fqcn, final ClassLoader loader, @@ -293,6 +305,19 @@ public LoggerContext getContext( return ctx; } + /** + * Loads the LoggerContext using the ContextSelector. + * @param fqcn The fully qualified class name of the caller. + * @param loader The ClassLoader to use or null. + * @param externalContext An external context (such as a ServletContext) to be associated with the LoggerContext. + * @param currentContext If true returns the current Context, if false returns the Context appropriate + * for the caller if a more appropriate Context can be determined. + * @param configLocations The locations of the configuration for the LoggerContext (or null). + * @return The LoggerContext. + * @deprecated Use {@link org.apache.logging.log4j.spi.LoggerContext#putObject(String, Object)} instead. + * @since 2.27.0 + */ + @Deprecated public LoggerContext getContext( final String fqcn, final ClassLoader loader, @@ -300,11 +325,11 @@ public LoggerContext getContext( final boolean currentContext, final List configLocations, final String name) { - final LoggerContext ctx = - selector.getContext(fqcn, loader, currentContext, null /*this probably needs to change*/); + final LoggerContext ctx = selector.getContext(fqcn, loader, currentContext, null); if (externalContext != null && ctx.getExternalContext() == null) { ctx.setExternalContext(externalContext); } + if (name != null) { ctx.setName(name); } diff --git a/log4j-web/src/main/java/org/apache/logging/log4j/web/Log4jWebInitializerImpl.java b/log4j-web/src/main/java/org/apache/logging/log4j/web/Log4jWebInitializerImpl.java index 0884f6a69c8..68393b58e18 100644 --- a/log4j-web/src/main/java/org/apache/logging/log4j/web/Log4jWebInitializerImpl.java +++ b/log4j-web/src/main/java/org/apache/logging/log4j/web/Log4jWebInitializerImpl.java @@ -30,9 +30,14 @@ import javax.servlet.ServletContext; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.core.AbstractLifeCycle; +import org.apache.logging.log4j.core.LifeCycle; import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.async.AsyncLoggerContext; +import org.apache.logging.log4j.core.config.AbstractConfiguration; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.ConfigurationFactory; import org.apache.logging.log4j.core.config.Configurator; +import org.apache.logging.log4j.core.config.composite.CompositeConfiguration; import org.apache.logging.log4j.core.impl.ContextAnchor; import org.apache.logging.log4j.core.impl.Log4jContextFactory; import org.apache.logging.log4j.core.lookup.ConfigurationStrSubstitutor; @@ -53,6 +58,8 @@ final class Log4jWebInitializerImpl extends AbstractLifeCycle implements Log4jWe private static final String WEB_INF = "/WEB-INF/"; + private static final String SERVLET_CONTEXT_KEY = "org.apache.logging.log4j.web.servletContext"; + static { if (Loader.isClassAvailable("org.apache.logging.log4j.core.web.JNDIContextFilter")) { throw new IllegalStateException("You are using Log4j 2 in a web application with the old, extinct " @@ -133,8 +140,8 @@ private void initializeJndi(final String location) { final ContextSelector selector = ((Log4jContextFactory) factory).getSelector(); if (selector instanceof NamedContextSelector) { this.namedContextSelector = (NamedContextSelector) selector; - context = this.namedContextSelector.locateContext( - this.name, WebLoggerContextUtils.createExternalEntry(this.servletContext), configLocation); + context = this.namedContextSelector.locateContext(this.name, null, configLocation); + context.putObject(SERVLET_CONTEXT_KEY, this.servletContext); ContextAnchor.THREAD_CONTEXT.set(context); if (context.isInitialized()) { context.start(); @@ -168,13 +175,49 @@ private void initializeNonJndi(final String location) { LOGGER.error("No Log4j context configuration provided. This is very unusual."); this.name = new SimpleDateFormat("yyyyMMdd_HHmmss.SSS").format(new Date()); } + if (location != null && location.contains(",")) { final List uris = getConfigURIs(location); - this.loggerContext = Configurator.initialize( - this.name, - this.getClassLoader(), - uris, - WebLoggerContextUtils.createExternalEntry(this.servletContext)); + final LoggerContextFactory factory = LogManager.getFactory(); + if (factory instanceof Log4jContextFactory) { + final ContextSelector selector = ((Log4jContextFactory) factory).getSelector(); + this.loggerContext = selector.getContext( + Log4jWebInitializerImpl.class.getName(), this.getClassLoader(), false, null); + if (this.loggerContext != null) { + this.loggerContext.putObject(SERVLET_CONTEXT_KEY, this.servletContext); + if (this.name != null) { + this.loggerContext.setName(this.name); + } + if (this.loggerContext.getState() == LifeCycle.State.INITIALIZED) { + ContextAnchor.THREAD_CONTEXT.set(this.loggerContext); + try { + final List configurations = new ArrayList<>(uris.size()); + for (final URI configLocation : uris) { + final Configuration config = ConfigurationFactory.getInstance() + .getConfiguration(this.loggerContext, this.name, configLocation); + if (config instanceof AbstractConfiguration) { + configurations.add((AbstractConfiguration) config); + } + } + if (configurations.size() == 1) { + this.loggerContext.start(configurations.get(0)); + } else if (configurations.size() > 1) { + this.loggerContext.start(new CompositeConfiguration(configurations)); + } else { + this.loggerContext.start(); + } + } finally { + ContextAnchor.THREAD_CONTEXT.remove(); + } + } + } + } else { + this.loggerContext = + Configurator.initialize(this.name, this.getClassLoader(), uris, this.servletContext); + if (this.loggerContext != null) { + this.loggerContext.putObject(SERVLET_CONTEXT_KEY, this.servletContext); + } + } return; } @@ -275,7 +318,7 @@ public synchronized boolean stop(final long timeout, final TimeUnit timeUnit) { this.namedContextSelector.removeContext(this.name); } this.loggerContext.stop(timeout, timeUnit); - this.loggerContext.setExternalContext(null); + this.loggerContext.removeObject(SERVLET_CONTEXT_KEY); this.loggerContext = null; } this.setStopped(); diff --git a/log4j-web/src/main/java/org/apache/logging/log4j/web/WebLoggerContextUtils.java b/log4j-web/src/main/java/org/apache/logging/log4j/web/WebLoggerContextUtils.java index 1e6d506be81..60feab64b68 100644 --- a/log4j-web/src/main/java/org/apache/logging/log4j/web/WebLoggerContextUtils.java +++ b/log4j-web/src/main/java/org/apache/logging/log4j/web/WebLoggerContextUtils.java @@ -37,8 +37,7 @@ public final class WebLoggerContextUtils { private WebLoggerContextUtils() {} private static final Lock WEB_SUPPORT_LOOKUP = new ReentrantLock(); - private static final String SERVLET_CONTEXT = "__SERVLET_CONTEXT__"; - + private static final String SERVLET_CONTEXT = "org.apache.logging.log4j.web.servletContext"; /** * Finds the main {@link org.apache.logging.log4j.core.LoggerContext} configured for the given ServletContext. * @@ -110,18 +109,29 @@ public static Runnable wrapExecutionContext(final ServletContext servletContext, }; } + /** + * @deprecated Use {@link #setServletContext(LoggerContext, ServletContext)} instead. + * @since 2.27.0 + */ + @Deprecated public static Map.Entry createExternalEntry(final ServletContext servletContext) { return new AbstractMap.SimpleEntry<>(SERVLET_CONTEXT, servletContext); } - public static void setServletContext(LoggerContext lc, ServletContext servletContext) { + /** + * Sets the ServletContext for the given LoggerContext. + * + * @param lc the LoggerContext + * @param servletContext the ServletContext + */ + public static void setServletContext(final LoggerContext lc, final ServletContext servletContext) { if (lc != null) { lc.putObject(SERVLET_CONTEXT, servletContext); } } /** - * Gets the current {@link ServletContext} if it has already been assigned to a LoggerContext's external context. + * Gets the current {@link ServletContext} if it has already been assigned to a LoggerContext. * * @return the current ServletContext attached to a LoggerContext or {@code null} if none could be found * @since 2.1 @@ -132,10 +142,20 @@ public static ServletContext getServletContext() { lc = LogManager.getContext(false); } - final Object obj = lc != null ? lc.getObject(SERVLET_CONTEXT) : null; + if (lc == null) { + return null; + } + + final Object obj = lc.getObject(SERVLET_CONTEXT); if (obj instanceof ServletContext) { return (ServletContext) obj; } + + final Object legacy = lc.getExternalContext(); + if (legacy instanceof ServletContext) { + return (ServletContext) legacy; + } + return null; } } diff --git a/log4j-web/src/test/java/org/apache/logging/log4j/web/WebLookupTest.java b/log4j-web/src/test/java/org/apache/logging/log4j/web/WebLookupTest.java index 4973d179d40..bced45848aa 100644 --- a/log4j-web/src/test/java/org/apache/logging/log4j/web/WebLookupTest.java +++ b/log4j-web/src/test/java/org/apache/logging/log4j/web/WebLookupTest.java @@ -18,7 +18,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.util.Map; import javax.servlet.ServletContext; @@ -26,6 +29,7 @@ import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.appender.FileAppender; import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.composite.CompositeConfiguration; import org.apache.logging.log4j.core.impl.ContextAnchor; import org.apache.logging.log4j.core.lookup.StrSubstitutor; import org.junit.jupiter.api.Test; @@ -107,4 +111,52 @@ void testLookup2() { initializer.stop(); ContextAnchor.THREAD_CONTEXT.remove(); } + + @Test + void testCompositeConfigurationServletContextName() throws Exception { + ContextAnchor.THREAD_CONTEXT.remove(); + + final String expectedServletContextName = "CompositeTest"; + + final ServletContext servletContext = mock(ServletContext.class); + when(servletContext.getServletContextName()).thenReturn(expectedServletContextName); + when(servletContext.getContextPath()).thenReturn("/composite-test"); + when(servletContext.getInitParameter(Log4jWebSupport.LOG4J_CONFIG_LOCATION)) + .thenReturn("log4j2-combined.xml,log4j2-override.xml"); + when(servletContext.getResource("log4j2-combined.xml")) + .thenReturn(getClass().getResource("/log4j2-combined.xml")); + when(servletContext.getResource("log4j2-override.xml")) + .thenReturn(getClass().getResource("/log4j2-override.xml")); + + final Log4jWebLifeCycle initializer = WebLoggerContextUtils.getWebLifeCycle(servletContext); + try { + initializer.start(); + initializer.setLoggerContext(); + + final LoggerContext ctx = ContextAnchor.THREAD_CONTEXT.get(); + assertNotNull(ctx, "No LoggerContext"); + + assertNotNull( + WebLoggerContextUtils.getServletContext(), + "ServletContext is null in composite configuration - " + + "${web:*} lookups will not resolve (issue #2351)"); + + final Configuration config = ctx.getConfiguration(); + assertNotNull(config, "No Configuration"); + assertTrue( + config instanceof CompositeConfiguration, + "Expected CompositeConfiguration for comma-separated log4jConfiguration"); + final StrSubstitutor substitutor = config.getStrSubstitutor(); + assertNotNull(substitutor, "No StrSubstitutor"); + + final String value = substitutor.replace("${web:servletContextName}"); + assertEquals( + expectedServletContextName, + value, + "${web:servletContextName} did not resolve in composite configuration (issue #2351)"); + } finally { + initializer.stop(); + ContextAnchor.THREAD_CONTEXT.remove(); + } + } } diff --git a/src/changelog/.2.x.x/deprecate_external_context_and_migrate_to_map_get_put_object.xml b/src/changelog/.2.x.x/deprecate_external_context_and_migrate_to_map_get_put_object.xml new file mode 100644 index 00000000000..4326fd1157b --- /dev/null +++ b/src/changelog/.2.x.x/deprecate_external_context_and_migrate_to_map_get_put_object.xml @@ -0,0 +1,13 @@ + + + + + + Deprecate `externalContext` and migrate to Map-backed `getObject/putObject`. This work resolves the servlet context resolution issue in composite configurations. + +