This is an automated email from the ASF dual-hosted git repository.

ramanathan1504 pushed a commit to branch ramanathan1504-issue-2351
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 0814ed564c8a0fca2730a96a0a9e28e2ec183b07
Author: Ramanathan <[email protected]>
AuthorDate: Wed Jun 17 23:53:27 2026 +0530

    Deprecate `externalContext` and migrate to Map-backed `getObject/putObject` 
for improved servlet context resolution in composite configurations
---
 .../apache/logging/log4j/spi/LoggerContext.java    |  3 +
 .../logging/log4j/core/LoggerContextTest.java      | 20 ++++++
 .../apache/logging/log4j/core/LoggerContext.java   | 16 ++++-
 .../log4j/core/impl/Log4jContextFactory.java       | 39 ++++++++---
 .../apache/logging/log4j/web/WebLookupTest.java    | 81 ----------------------
 .../logging/log4j/web/Log4jWebInitializerImpl.java | 60 +++++++++++++---
 .../logging/log4j/web/WebLoggerContextUtils.java   | 30 ++++++--
 .../apache/logging/log4j/web/WebLookupTest.java    | 52 ++++++++++++++
 ...l_context_and_migrate_to_map_get_put_object.xml | 13 ++++
 9 files changed, 208 insertions(+), 106 deletions(-)

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 d5a9119a1e..7a75ce27f7 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 e6d58f66c7..074fdbb994 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 @@ class LoggerContextTest {
             
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 da62cc2485..457d2154b7 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 class LoggerContext extends AbstractLifeCycle
      *
      * @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 class LoggerContext extends AbstractLifeCycle
      * @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 class LoggerContext extends AbstractLifeCycle
      * 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 class LoggerContext extends AbstractLifeCycle
      * 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 class LoggerContext extends AbstractLifeCycle
      * 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 c279fa2dd9..d69548c2bb 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 @@ public class Log4jContextFactory implements 
LoggerContextFactory, ShutdownCallba
      * 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 class Log4jContextFactory implements 
LoggerContextFactory, ShutdownCallba
      * 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 class Log4jContextFactory implements 
LoggerContextFactory, ShutdownCallba
      * 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 class Log4jContextFactory implements 
LoggerContextFactory, ShutdownCallba
      * 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 class Log4jContextFactory implements 
LoggerContextFactory, ShutdownCallba
         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,17 +325,11 @@ public class Log4jContextFactory implements 
LoggerContextFactory, ShutdownCallba
             final boolean currentContext,
             final List<URI> configLocations,
             final String name) {
-        final LoggerContext ctx;
-        if (externalContext instanceof Map.Entry) {
-            @SuppressWarnings("unchecked")
-            final Map.Entry<String, Object> entry = (Map.Entry<String, 
Object>) externalContext;
-            ctx = selector.getContext(fqcn, loader, entry, currentContext, 
null);
-        } else {
-            ctx = selector.getContext(fqcn, loader, currentContext, null);
-            if (externalContext != null && ctx.getExternalContext() == null) {
-                ctx.setExternalContext(externalContext);
-            }
+        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-jakarta-web/src/test/java/org/apache/logging/log4j/web/WebLookupTest.java
 
b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/WebLookupTest.java
index 1b9b60a739..92874baa5e 100644
--- 
a/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/WebLookupTest.java
+++ 
b/log4j-jakarta-web/src/test/java/org/apache/logging/log4j/web/WebLookupTest.java
@@ -16,28 +16,8 @@
  */
 package org.apache.logging.log4j.web;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertInstanceOf;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import jakarta.servlet.ServletContext;
-import org.apache.logging.log4j.core.LoggerContext;
-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.AfterEach;
-import org.junit.jupiter.api.Test;
-
 public class WebLookupTest {
 
-    @AfterEach
-    void tearDown() {
-        ContextAnchor.THREAD_CONTEXT.remove();
-    }
-
     // TODO: re-enable when 
https://github.com/spring-projects/spring-framework/issues/25354 is fixed
 
     //    @Test
@@ -114,66 +94,5 @@ public class WebLookupTest {
     //        initializer.stop();
     //        ContextAnchor.THREAD_CONTEXT.remove();
     //    }
-    /**
-     * Regression test for GitHub issue #2351:
-     * "Missing servlet context in web lookup when using composite 
configuration".
-     *
-     * When log4jConfiguration contains a comma-separated list of config files,
-     * the resulting composite LoggerContext must still expose the 
ServletContext
-     * via WebLoggerContextUtils.getServletContext() so that ${web:*} lookups 
resolve.
-     */
-    @Test
-    void testCompositeConfigurationServletContextName() throws Exception {
-        ContextAnchor.THREAD_CONTEXT.remove();
-
-        final String expectedServletContextName = "CompositeTest";
-
-        // Use Mockito to create a minimal ServletContext (no Spring 
dependency needed)
-        final ServletContext servletContext = mock(ServletContext.class);
-        
when(servletContext.getServletContextName()).thenReturn(expectedServletContextName);
-        when(servletContext.getContextPath()).thenReturn("/composite-test");
-        // Composite configuration: two comma-separated config files
-        
when(servletContext.getInitParameter(Log4jWebSupport.LOG4J_CONFIG_LOCATION))
-                .thenReturn("log4j2-combined.xml,log4j2-override.xml");
-        // Let the initializer resolve each file via the servlet context 
resource lookup
-        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");
-
-            // The servlet context MUST be reachable via the web lookup for 
composite config.
-            // Before the fix this returns null, breaking all ${web:*} lookups.
-            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");
-            assertInstanceOf(
-                    CompositeConfiguration.class,
-                    config,
-                    "Expected CompositeConfiguration for comma-separated 
log4jConfiguration");
-            final StrSubstitutor substitutor = config.getStrSubstitutor();
-            assertNotNull(substitutor, "No StrSubstitutor");
 
-            // Core assertion: ${web:servletContextName} must resolve to the 
actual name
-            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/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 0884f6a69c..b3b2dc9367 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 java.util.concurrent.TimeUnit;
 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 @@ final class Log4jWebInitializerImpl extends 
AbstractLifeCycle implements Log4jWe
             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,17 +175,54 @@ final class Log4jWebInitializerImpl extends 
AbstractLifeCycle implements Log4jWe
             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<URI> 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<AbstractConfiguration> 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;
         }
 
         final URI uri = getConfigURI(location);
+        // Use Map.Entry version for single URI to ensure ServletContext is 
available before configuration loads
         this.loggerContext = Configurator.initialize(
                 this.name, this.getClassLoader(), uri, 
WebLoggerContextUtils.createExternalEntry(this.servletContext));
     }
@@ -275,7 +319,7 @@ final class Log4jWebInitializerImpl extends 
AbstractLifeCycle implements Log4jWe
                     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 1e6d506be8..60feab64b6 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 final class WebLoggerContextUtils {
         };
     }
 
+    /**
+     * @deprecated Use {@link #setServletContext(LoggerContext, 
ServletContext)} instead.
+     * @since 2.27.0
+     */
+    @Deprecated
     public static Map.Entry<String, Object> 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 final class WebLoggerContextUtils {
             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 4973d179d4..bced45848a 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 @@ package org.apache.logging.log4j.web;
 
 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.Appender;
 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 @@ class WebLookupTest {
         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 0000000000..4326fd1157
--- /dev/null
+++ 
b/src/changelog/.2.x.x/deprecate_external_context_and_migrate_to_map_get_put_object.xml
@@ -0,0 +1,13 @@
+<?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="changed">
+    <issue id="2351" 
link="https://github.com/apache/logging-log4j2/pull/2351"/>
+    <issue id="4070" 
link="https://github.com/apache/logging-log4j2/pull/4070"/>
+    <description format="asciidoc">
+        Deprecate `externalContext` and migrate to Map-backed 
`getObject/putObject`. This work resolves the servlet context resolution issue 
in composite configurations.
+    </description>
+</entry>

Reply via email to