You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by rg...@apache.org on 2024/04/03 15:54:38 UTC

(logging-log4j2) 04/05: Move ContextDataProviders to the API

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

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

commit 4490b4337e68d40c232bfe275f4eb3bbb01880b0
Author: Ralph Goers <rg...@apache.org>
AuthorDate: Wed Apr 3 08:19:52 2024 -0700

    Move ContextDataProviders to the API
---
 .../org/apache/logging/log4j/test/TestLogger.java  |  10 +-
 .../apache/logging/log4j/ResourceLoggerTest.java   |   7 +-
 .../java/org/apache/logging/log4j/ContextData.java | 106 ++++++++
 .../org/apache/logging/log4j/ResourceLogger.java   |   5 +-
 .../org/apache/logging/log4j/ScopedContext.java    |  62 ++++-
 .../apache/logging/log4j/simple/SimpleLogger.java  |   7 +-
 .../logging/log4j/spi}/ContextDataProvider.java    |  47 +++-
 .../ScopedContextDataProvider.java}                |  41 ++-
 .../log4j/spi/ThreadContextDataProvider.java       |  35 ++-
 .../apache/logging/log4j/ResourceLoggerTest.java   |   3 +-
 .../core/impl/ThreadContextDataInjectorTest.java   |   2 +-
 .../logging/log4j/core/ContextDataInjector.java    |  16 +-
 .../logging/log4j/core/async/AsyncLogger.java      |  10 +-
 .../log4j/core/async/RingBufferLogEvent.java       |   4 +-
 .../core/async/RingBufferLogEventTranslator.java   |  10 +-
 .../log4j/core/filter/DynamicThresholdFilter.java  |  19 +-
 .../log4j/core/filter/ThreadContextMapFilter.java  |  25 +-
 .../logging/log4j/core/filter/package-info.java    |   2 +-
 .../core/impl/ContextDataInjectorFactory.java      |  25 +-
 .../log4j/core/impl/JdkMapAdapterStringMap.java    |   2 +-
 .../logging/log4j/core/impl/Log4jLogEvent.java     |  21 ++
 .../log4j/core/impl/ReusableLogEventFactory.java   |  24 +-
 .../log4j/core/impl/ThreadContextDataInjector.java | 284 +++++++++++----------
 .../log4j/core/lookup/ContextMapLookup.java        |  18 +-
 .../logging/log4j/core/lookup/package-info.java    |   2 +-
 .../apache/logging/log4j/core/osgi/Activator.java  |   8 +-
 .../apache/logging/log4j/core/package-info.java    |   2 +-
 .../log4j/core/util/ContextDataProvider.java       |  14 +-
 .../logging/log4j/core/util/package-info.java      |   2 +-
 29 files changed, 548 insertions(+), 265 deletions(-)

diff --git a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/TestLogger.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/TestLogger.java
index d90258e5a2..2387fee7e6 100644
--- a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/TestLogger.java
+++ b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/TestLogger.java
@@ -23,10 +23,9 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import org.apache.logging.log4j.ContextData;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
-import org.apache.logging.log4j.ScopedContext;
-import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.MessageFactory;
 import org.apache.logging.log4j.message.ParameterizedMapMessage;
@@ -82,11 +81,8 @@ public class TestLogger extends AbstractLogger {
             sb.append(' ');
         }
         sb.append(message.getFormattedMessage());
-        Map<String, ScopedContext.Renderable> contextMap = ScopedContext.getContextMap();
-        final Map<String, String> mdc = new HashMap<>(ThreadContext.getImmutableContext());
-        if (contextMap != null && !contextMap.isEmpty()) {
-            contextMap.forEach((key, value) -> mdc.put(key, value.render()));
-        }
+        final Map<String, String> mdc = new HashMap<>(ContextData.size());
+        ContextData.addAll(mdc);
         if (!mdc.isEmpty()) {
             sb.append(' ');
             sb.append(mdc);
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/ResourceLoggerTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/ResourceLoggerTest.java
index c9a8ae691e..4822a52e12 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/ResourceLoggerTest.java
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/ResourceLoggerTest.java
@@ -40,12 +40,17 @@ public class ResourceLoggerTest {
         System.setProperty("log4j2.loggerContextFactory", TestLoggerContextFactory.class.getName());
     }
 
+    @BeforeAll
+    public static void afterAll() {
+        System.clearProperty("log4j2.loggerContextFactory");
+    }
+
     @Test
     public void testFactory() throws Exception {
         Connection connection = new Connection("Test", "dummy");
         connection.useConnection();
         MapSupplier mapSupplier = new MapSupplier(connection);
-        ResourceLogger logger = ResourceLogger.newBuilder()
+        Logger logger = ResourceLogger.newBuilder()
                 .withClass(this.getClass())
                 .withSupplier(mapSupplier)
                 .build();
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/ContextData.java b/log4j-api/src/main/java/org/apache/logging/log4j/ContextData.java
new file mode 100644
index 0000000000..1b9445dcd8
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/ContextData.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.logging.log4j.spi.ContextDataProvider;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.ServiceLoaderUtil;
+import org.apache.logging.log4j.util.StringMap;
+
+/**
+ * General purpose utility class for accessing data accessible through ContextDataProviders.
+ */
+public final class ContextData {
+
+    private static final Logger LOGGER = StatusLogger.getLogger();
+    /**
+     * ContextDataProviders loaded via OSGi.
+     */
+    public static Collection<ContextDataProvider> contextDataProviders = new ConcurrentLinkedDeque<>();
+
+    private static final List<ContextDataProvider> SERVICE_PROVIDERS = getServiceProviders();
+
+    private ContextData() {}
+
+    private static List<ContextDataProvider> getServiceProviders() {
+        final List<ContextDataProvider> providers = new ArrayList<>();
+        ServiceLoaderUtil.safeStream(
+                        ContextDataProvider.class,
+                        ServiceLoader.load(ContextDataProvider.class, ContextData.class.getClassLoader()),
+                        LOGGER)
+                .forEach(providers::add);
+        return Collections.unmodifiableList(providers);
+    }
+
+    public static void addProvider(ContextDataProvider provider) {
+        contextDataProviders.add(provider);
+    }
+
+    private static List<ContextDataProvider> getProviders() {
+        final List<ContextDataProvider> providers =
+                new ArrayList<>(contextDataProviders.size() + SERVICE_PROVIDERS.size());
+        providers.addAll(contextDataProviders);
+        providers.addAll(SERVICE_PROVIDERS);
+        return providers;
+    }
+
+    public static int size() {
+        final List<ContextDataProvider> providers = getProviders();
+        final AtomicInteger count = new AtomicInteger(0);
+        providers.forEach((provider) -> count.addAndGet(provider.size()));
+        return count.get();
+    }
+
+    /**
+     * Populates the provided StringMap with data from the Context.
+     * @param stringMap the StringMap to contain the results.
+     */
+    public static void addAll(StringMap stringMap) {
+        final List<ContextDataProvider> providers = getProviders();
+        providers.forEach((provider) -> provider.addAll(stringMap));
+    }
+
+    /**
+     * Populates the provided Map with data from the Context.
+     * @param map the Map to contain the results.
+     * @return the Map. Useful for chaining operations.
+     */
+    public static Map<String, String> addAll(Map<String, String> map) {
+        final List<ContextDataProvider> providers = getProviders();
+        providers.forEach((provider) -> provider.addAll(map));
+        return map;
+    }
+
+    public static String getValue(String key) {
+        List<ContextDataProvider> providers = getProviders();
+        for (ContextDataProvider provider : providers) {
+            String value = provider.get(key);
+            if (value != null) {
+                return value;
+            }
+        }
+        return null;
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/ResourceLogger.java b/log4j-api/src/main/java/org/apache/logging/log4j/ResourceLogger.java
index 17320a74af..7a30dab94d 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/ResourceLogger.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/ResourceLogger.java
@@ -28,8 +28,7 @@ import org.apache.logging.log4j.util.StackLocatorUtil;
 import org.apache.logging.log4j.util.Strings;
 
 /**
- * Logger for resources. Formats all events using the ParameterizedMapMessageFactory along with the provided
- * Supplier. The Supplier provides resource attributes that should be included in all log events generated
+ * Logger for resources. The Supplier provides resource attributes that should be included in all log events generated
  * from the current resource. Note that since the Supplier is called for every LogEvent being generated
  * the values returned may change as necessary. Care should be taken to make the Supplier as efficient as
  * possible to avoid performance issues.
@@ -137,7 +136,7 @@ public final class ResourceLogger extends ExtendedLoggerWrapper {
          * Construct the ResourceLogger.
          * @return the ResourceLogger.
          */
-        public ResourceLogger build() {
+        public Logger build() {
             if (this.logger == null) {
                 if (Strings.isEmpty(name)) {
                     Class<?> clazz = StackLocatorUtil.getCallerClass(2);
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java b/log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java
index b105e3be1d..67e2ba97fd 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java
@@ -25,7 +25,7 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 import java.util.function.Supplier;
-import org.apache.logging.log4j.internal.ScopedContextAnchor;
+import org.apache.logging.log4j.spi.ScopedContextDataProvider;
 import org.apache.logging.log4j.status.StatusLogger;
 
 /**
@@ -59,7 +59,7 @@ public class ScopedContext {
      * @return the Map of Renderable objects.
      */
     public static Map<String, Renderable> getContextMap() {
-        Optional<Instance> context = ScopedContextAnchor.getContext();
+        Optional<Instance> context = ScopedContextDataProvider.getContext();
         if (context.isPresent()
                 && context.get().contextMap != null
                 && !context.get().contextMap.isEmpty()) {
@@ -69,13 +69,23 @@ public class ScopedContext {
     }
 
     /**
-     * Return the key from the current ScopedContext, if there is one and the key exists.
+     * @hidden
+     * Returns the number of entries in the context map.
+     * @return the number of items in the context map.
+     */
+    public static int size() {
+        Optional<Instance> context = ScopedContextDataProvider.getContext();
+        return context.map(instance -> instance.contextMap.size()).orElse(0);
+    }
+
+    /**
+     * Return the value of the key from the current ScopedContext, if there is one and the key exists.
      * @param key The key.
      * @return The value of the key in the current ScopedContext.
      */
     @SuppressWarnings("unchecked")
     public static <T> T get(String key) {
-        Optional<Instance> context = ScopedContextAnchor.getContext();
+        Optional<Instance> context = ScopedContextDataProvider.getContext();
         if (context.isPresent()) {
             Renderable renderable = context.get().contextMap.get(key);
             if (renderable != null) {
@@ -85,6 +95,36 @@ public class ScopedContext {
         return null;
     }
 
+    /**
+     * Return String value of the key from the current ScopedContext, if there is one and the key exists.
+     * @param key The key.
+     * @return The value of the key in the current ScopedContext.
+     */
+    public static String getString(String key) {
+        Optional<Instance> context = ScopedContextDataProvider.getContext();
+        if (context.isPresent()) {
+            Renderable renderable = context.get().contextMap.get(key);
+            if (renderable != null) {
+                return renderable.render();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Adds all the String rendered objects in the context map to the provided Map.
+     * @param map The Map to add entries to.
+     */
+    public static void addAll(Map<String, String> map) {
+        Optional<Instance> context = ScopedContextDataProvider.getContext();
+        if (context.isPresent()) {
+            Map<String, Renderable> contextMap = context.get().contextMap;
+            if (contextMap != null && !contextMap.isEmpty()) {
+                contextMap.forEach((key, value) -> map.put(key, value.render()));
+            }
+        }
+    }
+
     /**
      * Creates a ScopedContext Instance with a key/value pair.
      *
@@ -316,7 +356,7 @@ public class ScopedContext {
      * @return an Optional containing the active ScopedContext, if there is one.
      */
     private static Optional<Instance> current() {
-        return ScopedContextAnchor.getContext();
+        return ScopedContextDataProvider.getContext();
     }
 
     public static class Instance {
@@ -460,11 +500,11 @@ public class ScopedContext {
             if (contextStack != null) {
                 ThreadContext.setStack(contextStack);
             }
-            ScopedContextAnchor.addScopedContext(scopedContext);
+            ScopedContextDataProvider.addScopedContext(scopedContext);
             try {
                 op.run();
             } finally {
-                ScopedContextAnchor.removeScopedContext();
+                ScopedContextDataProvider.removeScopedContext();
                 ThreadContext.clearAll();
             }
         }
@@ -511,11 +551,11 @@ public class ScopedContext {
             if (contextStack != null) {
                 ThreadContext.setStack(contextStack);
             }
-            ScopedContextAnchor.addScopedContext(scopedContext);
+            ScopedContextDataProvider.addScopedContext(scopedContext);
             try {
                 return op.call();
             } finally {
-                ScopedContextAnchor.removeScopedContext();
+                ScopedContextDataProvider.removeScopedContext();
                 ThreadContext.clearAll();
             }
         }
@@ -523,6 +563,10 @@ public class ScopedContext {
 
     /**
      * Interface for converting Objects stored in the ContextScope to Strings for logging.
+     *
+     * Users implementing this interface are encouraged to make the render method as lightweight as possible,
+     * Typically by creating the String representation of the object during its construction and just returning
+     * the String.
      */
     public static interface Renderable {
         /**
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLogger.java b/log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLogger.java
index f5529f4258..d914f1c4c9 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLogger.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLogger.java
@@ -23,10 +23,9 @@ import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
+import org.apache.logging.log4j.ContextData;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
-import org.apache.logging.log4j.ScopedContext;
-import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.MessageFactory;
 import org.apache.logging.log4j.spi.AbstractLogger;
@@ -296,8 +295,8 @@ public class SimpleLogger extends AbstractLogger {
         }
         sb.append(msg.getFormattedMessage());
         if (showContextMap) {
-            final Map<String, String> mdc = new HashMap<>(ThreadContext.getImmutableContext());
-            ScopedContext.getContextMap().forEach((key, value) -> mdc.put(key, value.render()));
+            final Map<String, String> mdc = new HashMap<>(ContextData.size());
+            ContextData.addAll(mdc);
             if (!mdc.isEmpty()) {
                 sb.append(SPACE);
                 sb.append(mdc.toString());
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ContextDataProvider.java
similarity index 54%
copy from log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java
copy to log4j-api/src/main/java/org/apache/logging/log4j/spi/ContextDataProvider.java
index 8ac63b6858..d003e9c74f 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ContextDataProvider.java
@@ -14,10 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.core.util;
+package org.apache.logging.log4j.spi;
 
 import java.util.Map;
-import org.apache.logging.log4j.core.impl.JdkMapAdapterStringMap;
 import org.apache.logging.log4j.util.StringMap;
 
 /**
@@ -25,6 +24,15 @@ import org.apache.logging.log4j.util.StringMap;
  */
 public interface ContextDataProvider {
 
+    /**
+     * Returns the key for a value from the context data.
+     * @param key the key to locate.
+     * @return the value or null if it is not found.
+     */
+    default String get(String key) {
+        return null;
+    }
+
     /**
      * Returns a Map containing context data to be injected into the event or null if no context data is to be added.
      * <p>
@@ -36,14 +44,33 @@ public interface ContextDataProvider {
     Map<String, String> supplyContextData();
 
     /**
-     * Returns the context data as a StringMap.
-     * <p>
-     *     Thread-safety note: The returned object can safely be passed off to another thread: future changes in the
-     *     underlying context data will not be reflected in the returned object.
-     * </p>
-     * @return the context data in a StringMap.
+     * Returns the number of items in this context.
+     * @return the number of items in the context.
+     */
+    default int size() {
+        Map<String, String> contextMap = supplyContextData();
+        return contextMap != null ? contextMap.size() : 0;
+    }
+
+    /**
+     * Add all the keys in the current context to the provided Map.
+     * @param map the StringMap to add the keys and values to.
+     */
+    default void addAll(Map<String, String> map) {
+        Map<String, String> contextMap = supplyContextData();
+        if (contextMap != null) {
+            map.putAll(contextMap);
+        }
+    }
+
+    /**
+     * Add all the keys in the current context to the provided StringMap.
+     * @param map the StringMap to add the keys and values to.
      */
-    default StringMap supplyStringMap() {
-        return new JdkMapAdapterStringMap(supplyContextData(), true);
+    default void addAll(StringMap map) {
+        Map<String, String> contextMap = supplyContextData();
+        if (contextMap != null) {
+            contextMap.forEach(map::putValue);
+        }
     }
 }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/ScopedContextAnchor.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextDataProvider.java
similarity index 66%
rename from log4j-api/src/main/java/org/apache/logging/log4j/internal/ScopedContextAnchor.java
rename to log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextDataProvider.java
index c09c4bc78f..f2a14c19d0 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/ScopedContextAnchor.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextDataProvider.java
@@ -14,17 +14,25 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.internal;
+package org.apache.logging.log4j.spi;
 
+import aQute.bnd.annotation.Resolution;
+import aQute.bnd.annotation.spi.ServiceProvider;
 import java.util.ArrayDeque;
+import java.util.Collections;
 import java.util.Deque;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Optional;
 import org.apache.logging.log4j.ScopedContext;
 
 /**
- * Anchor for the ScopedContext. This class is private and not for public consumption.
+ * ContextDataProvider for {@code Map<String, String>} data.
+ * @since 2.24.0
  */
-public class ScopedContextAnchor {
+@ServiceProvider(value = ContextDataProvider.class, resolution = Resolution.OPTIONAL)
+public class ScopedContextDataProvider implements ContextDataProvider {
+
     private static final ThreadLocal<Deque<ScopedContext.Instance>> scopedContext = new ThreadLocal<>();
 
     /**
@@ -66,4 +74,31 @@ public class ScopedContextAnchor {
             }
         }
     }
+
+    @Override
+    public String get(String key) {
+        return ScopedContext.getString(key);
+    }
+
+    @Override
+    public Map<String, String> supplyContextData() {
+        Map<String, ScopedContext.Renderable> contextMap = ScopedContext.getContextMap();
+        if (!contextMap.isEmpty()) {
+            Map<String, String> map = new HashMap<>();
+            contextMap.forEach((key, value) -> map.put(key, value.render()));
+            return map;
+        } else {
+            return Collections.emptyMap();
+        }
+    }
+
+    @Override
+    public int size() {
+        return ScopedContext.size();
+    }
+
+    @Override
+    public void addAll(Map<String, String> map) {
+        ScopedContext.addAll(map);
+    }
 }
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ScopedContextDataProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextDataProvider.java
similarity index 56%
rename from log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ScopedContextDataProvider.java
rename to log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextDataProvider.java
index a4f651b7ea..d6e28f00d8 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ScopedContextDataProvider.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextDataProvider.java
@@ -14,37 +14,36 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.core.impl;
+package org.apache.logging.log4j.spi;
 
 import aQute.bnd.annotation.Resolution;
 import aQute.bnd.annotation.spi.ServiceProvider;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.Map;
-import org.apache.logging.log4j.ScopedContext;
-import org.apache.logging.log4j.core.util.ContextDataProvider;
-import org.apache.logging.log4j.util.StringMap;
+import org.apache.logging.log4j.ThreadContext;
 
 /**
- * ContextDataProvider for {@code Map<String, String>} data.
+ * ContextDataProvider for ThreadContext data.
  */
 @ServiceProvider(value = ContextDataProvider.class, resolution = Resolution.OPTIONAL)
-public class ScopedContextDataProvider implements ContextDataProvider {
+public class ThreadContextDataProvider implements ContextDataProvider {
+
+    @Override
+    public String get(String key) {
+        return ThreadContext.get(key);
+    }
 
     @Override
     public Map<String, String> supplyContextData() {
-        Map<String, ScopedContext.Renderable> contextMap = ScopedContext.getContextMap();
-        if (!contextMap.isEmpty()) {
-            Map<String, String> map = new HashMap<>();
-            contextMap.forEach((key, value) -> map.put(key, value.render()));
-            return map;
-        } else {
-            return Collections.emptyMap();
-        }
+        return ThreadContext.getImmutableContext();
+    }
+
+    @Override
+    public int size() {
+        return ThreadContext.getContext().size();
     }
 
     @Override
-    public StringMap supplyStringMap() {
-        return new JdkMapAdapterStringMap(supplyContextData());
+    public void addAll(Map<String, String> map) {
+        map.putAll(ThreadContext.getContext());
     }
 }
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/ResourceLoggerTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/ResourceLoggerTest.java
index a66f4459ac..738abeec7b 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/ResourceLoggerTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/ResourceLoggerTest.java
@@ -28,6 +28,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.Supplier;
+import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.ResourceLogger;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.LoggerContext;
@@ -54,7 +55,7 @@ public class ResourceLoggerTest {
         Connection connection = new Connection("Test", "dummy");
         connection.useConnection();
         MapSupplier mapSupplier = new MapSupplier(connection);
-        ResourceLogger logger = ResourceLogger.newBuilder()
+        Logger logger = ResourceLogger.newBuilder()
                 .withClass(this.getClass())
                 .withSupplier(mapSupplier)
                 .build();
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java
index 39258f5003..9465460823 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java
@@ -91,7 +91,7 @@ public class ThreadContextDataInjectorTest {
                         : readOnlythreadContextMap.getClass().getName(),
                 is(equalTo(readOnlythreadContextMapClassName)));
 
-        final ContextDataInjector contextDataInjector = createInjector();
+        final ContextDataInjector contextDataInjector = createInjector(true);
         final StringMap stringMap = contextDataInjector.injectContextData(null, new SortedArrayStringMap());
 
         assertThat("thread context map", ThreadContext.getContext(), allOf(hasEntry("foo", "bar"), not(hasKey("baz"))));
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/ContextDataInjector.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/ContextDataInjector.java
index 6d386fe139..56293f59c1 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/ContextDataInjector.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/ContextDataInjector.java
@@ -21,6 +21,7 @@ import org.apache.logging.log4j.core.config.Property;
 import org.apache.logging.log4j.core.impl.ContextDataInjectorFactory;
 import org.apache.logging.log4j.core.impl.ThreadContextDataInjector;
 import org.apache.logging.log4j.util.ReadOnlyStringMap;
+import org.apache.logging.log4j.util.SortedArrayStringMap;
 import org.apache.logging.log4j.util.StringMap;
 
 /**
@@ -105,6 +106,19 @@ public interface ContextDataInjector {
      * the implementation of this method. It is not safe to pass the returned object to another thread.
      * </p>
      * @return a {@code ReadOnlyStringMap} object reflecting the current state of the context, may not return {@code null}
+     * @deprecated - Methods using this have been converted to call getValue(). Will be removed in 3.0.0.
      */
-    ReadOnlyStringMap rawContextData();
+    @Deprecated
+    default ReadOnlyStringMap rawContextData() {
+        return new SortedArrayStringMap();
+    }
+
+    /**
+     * Retrieves a key from the context. This avoids having to construct a composite Map when multiple contexts are available.
+     * @param key the key to retrieve.
+     * @return the String value associated with the key.
+     */
+    default String getValue(String key) {
+        return null;
+    }
 }
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java
index 0378d010cc..5bb479f66c 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java
@@ -19,6 +19,7 @@ package org.apache.logging.log4j.core.async;
 import com.lmax.disruptor.EventTranslatorVararg;
 import com.lmax.disruptor.dsl.Disruptor;
 import java.util.List;
+import org.apache.logging.log4j.ContextData;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.ThreadContext;
@@ -484,6 +485,11 @@ public class AsyncLogger extends Logger implements EventTranslatorVararg<RingBuf
 
         final Thread currentThread = Thread.currentThread();
         final String threadName = THREAD_NAME_CACHING_STRATEGY.getThreadName();
+        if (CONTEXT_DATA_INJECTOR == null) {
+            ContextData.addAll((StringMap) event.getContextData());
+        } else {
+            CONTEXT_DATA_INJECTOR.injectContextData(null, (StringMap) event.getContextData());
+        }
         event.setValues(
                 asyncLogger,
                 asyncLogger.getName(),
@@ -492,9 +498,7 @@ public class AsyncLogger extends Logger implements EventTranslatorVararg<RingBuf
                 level,
                 message,
                 thrown,
-                // config properties are taken care of in the EventHandler thread
-                // in the AsyncLogger#actualAsyncLog method
-                CONTEXT_DATA_INJECTOR.injectContextData(null, (StringMap) event.getContextData()),
+                null,
                 contextStack,
                 currentThread.getId(),
                 threadName,
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEvent.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEvent.java
index d3ab5a10e8..ae8e1bce94 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEvent.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEvent.java
@@ -121,7 +121,9 @@ public class RingBufferLogEvent implements LogEvent, ReusableMessage, CharSequen
         this.marker = aMarker;
         this.fqcn = theFqcn;
         this.location = aLocation;
-        this.contextData = mutableContextData;
+        if (mutableContextData != null) {
+            this.contextData = mutableContextData;
+        }
         this.contextStack = aContextStack;
         this.asyncLogger = anAsyncLogger;
         this.populated = true;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventTranslator.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventTranslator.java
index 9763ff7fce..c965ce974d 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventTranslator.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventTranslator.java
@@ -17,6 +17,7 @@
 package org.apache.logging.log4j.core.async;
 
 import com.lmax.disruptor.EventTranslator;
+import org.apache.logging.log4j.ContextData;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.ThreadContext.ContextStack;
@@ -57,6 +58,13 @@ public class RingBufferLogEventTranslator implements EventTranslator<RingBufferL
     public void translateTo(final RingBufferLogEvent event, final long sequence) {
         try {
             final ReadOnlyStringMap contextData = event.getContextData();
+            if (contextData != null) {
+                if (INJECTOR == null) {
+                    ContextData.addAll((StringMap) contextData);
+                } else {
+                    INJECTOR.injectContextData(null, (StringMap) contextData);
+                }
+            }
             event.setValues(
                     asyncLogger,
                     loggerName,
@@ -67,7 +75,7 @@ public class RingBufferLogEventTranslator implements EventTranslator<RingBufferL
                     thrown,
                     // config properties are taken care of in the EventHandler thread
                     // in the AsyncLogger#actualAsyncLog method
-                    INJECTOR.injectContextData(null, contextData instanceof StringMap ? (StringMap) contextData : null),
+                    null,
                     contextStack,
                     threadId,
                     threadName,
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/DynamicThresholdFilter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/DynamicThresholdFilter.java
index 25187b0f17..d2de85fe4d 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/DynamicThresholdFilter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/DynamicThresholdFilter.java
@@ -19,6 +19,7 @@ package org.apache.logging.log4j.core.filter;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
+import org.apache.logging.log4j.ContextData;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.ThreadContext;
@@ -35,14 +36,14 @@ import org.apache.logging.log4j.core.impl.ContextDataFactory;
 import org.apache.logging.log4j.core.impl.ContextDataInjectorFactory;
 import org.apache.logging.log4j.core.util.KeyValuePair;
 import org.apache.logging.log4j.message.Message;
+import org.apache.logging.log4j.spi.ContextDataProvider;
 import org.apache.logging.log4j.util.PerformanceSensitive;
-import org.apache.logging.log4j.util.ReadOnlyStringMap;
 import org.apache.logging.log4j.util.StringMap;
 
 /**
  * Compares against a log level that is associated with a context value. By default the context is the
- * {@link ThreadContext}, but users may {@linkplain ContextDataInjectorFactory configure} a custom
- * {@link ContextDataInjector} which obtains context data from some other source.
+ * {@link ThreadContext} and/or the {@link org.apache.logging.log4j.ScopedContext}, but users may add a custom
+ * {@link ContextDataProvider} which obtains context data from some other source.
  */
 @Plugin(
         name = "DynamicThresholdFilter",
@@ -125,8 +126,7 @@ public final class DynamicThresholdFilter extends AbstractFilter {
         return true;
     }
 
-    private Result filter(final Level level, final ReadOnlyStringMap contextMap) {
-        final String value = contextMap.getValue(key);
+    private Result filter(final Level level, String value) {
         if (value != null) {
             Level ctxLevel = levelMap.get(value);
             if (ctxLevel == null) {
@@ -139,7 +139,7 @@ public final class DynamicThresholdFilter extends AbstractFilter {
 
     @Override
     public Result filter(final LogEvent event) {
-        return filter(event.getLevel(), event.getContextData());
+        return filter(event.getLevel(), (String) event.getContextData().getValue(key));
     }
 
     @Override
@@ -160,8 +160,11 @@ public final class DynamicThresholdFilter extends AbstractFilter {
         return filter(level, currentContextData());
     }
 
-    private ReadOnlyStringMap currentContextData() {
-        return injector.rawContextData();
+    private String currentContextData() {
+        if (injector == null) {
+            return ContextData.getValue(key);
+        }
+        return injector.rawContextData().getValue(key);
     }
 
     @Override
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/ThreadContextMapFilter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/ThreadContextMapFilter.java
index 21e6e1da1b..7b27f88450 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/ThreadContextMapFilter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/ThreadContextMapFilter.java
@@ -21,9 +21,9 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import org.apache.logging.log4j.ContextData;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
-import org.apache.logging.log4j.core.ContextDataInjector;
 import org.apache.logging.log4j.core.Filter;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.Logger;
@@ -33,14 +33,10 @@ import org.apache.logging.log4j.core.config.plugins.PluginAliases;
 import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
 import org.apache.logging.log4j.core.config.plugins.PluginElement;
 import org.apache.logging.log4j.core.config.plugins.PluginFactory;
-import org.apache.logging.log4j.core.impl.ContextDataFactory;
-import org.apache.logging.log4j.core.impl.ContextDataInjectorFactory;
 import org.apache.logging.log4j.core.util.KeyValuePair;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.util.IndexedReadOnlyStringMap;
 import org.apache.logging.log4j.util.PerformanceSensitive;
-import org.apache.logging.log4j.util.ReadOnlyStringMap;
-import org.apache.logging.log4j.util.StringMap;
 
 /**
  * Filter based on a value in the Thread Context Map (MDC).
@@ -53,7 +49,6 @@ import org.apache.logging.log4j.util.StringMap;
 @PluginAliases("ContextMapFilter")
 @PerformanceSensitive("allocation")
 public class ThreadContextMapFilter extends MapFilter {
-    private final ContextDataInjector injector = ContextDataInjectorFactory.createInjector();
     private final String key;
     private final String value;
 
@@ -62,12 +57,6 @@ public class ThreadContextMapFilter extends MapFilter {
     public ThreadContextMapFilter(
             final Map<String, List<String>> pairs, final boolean oper, final Result onMatch, final Result onMismatch) {
         super(pairs, oper, onMatch, onMismatch);
-        // ContextDataFactory looks up a property. The Spring PropertySource may log which will cause recursion.
-        // By initializing the ContextDataFactory here recursion will be prevented.
-        final StringMap map = ContextDataFactory.createContextData();
-        LOGGER.debug(
-                "Successfully initialized ContextDataFactory by retrieving the context data with {} entries",
-                map.size());
         if (pairs.size() == 1) {
             final Iterator<Map.Entry<String, List<String>>> iter =
                     pairs.entrySet().iterator();
@@ -109,28 +98,20 @@ public class ThreadContextMapFilter extends MapFilter {
     private Result filter() {
         boolean match = false;
         if (useMap) {
-            ReadOnlyStringMap currentContextData = null;
             final IndexedReadOnlyStringMap map = getStringMap();
             for (int i = 0; i < map.size(); i++) {
-                if (currentContextData == null) {
-                    currentContextData = currentContextData();
-                }
-                final String toMatch = currentContextData.getValue(map.getKeyAt(i));
+                final String toMatch = ContextData.getValue(map.getKeyAt(i));
                 match = toMatch != null && ((List<String>) map.getValueAt(i)).contains(toMatch);
                 if ((!isAnd() && match) || (isAnd() && !match)) {
                     break;
                 }
             }
         } else {
-            match = value.equals(currentContextData().getValue(key));
+            match = value.equals(ContextData.getValue(key));
         }
         return match ? onMatch : onMismatch;
     }
 
-    private ReadOnlyStringMap currentContextData() {
-        return injector.rawContextData();
-    }
-
     @Override
     public Result filter(final LogEvent event) {
         return super.filter(event.getContextData()) ? onMatch : onMismatch;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/package-info.java
index 8ad77df471..e84cfe7671 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/package-info.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/package-info.java
@@ -22,7 +22,7 @@
  * {@link org.apache.logging.log4j.core.Filter#ELEMENT_TYPE filter}.
  */
 @Export
-@Version("2.21.0")
+@Version("2.24.0")
 package org.apache.logging.log4j.core.filter;
 
 import org.osgi.annotation.bundle.Export;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataInjectorFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataInjectorFactory.java
index 141604810c..aa73324e8b 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataInjectorFactory.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataInjectorFactory.java
@@ -30,27 +30,25 @@ import org.apache.logging.log4j.util.ReadOnlyStringMap;
  * Factory for ContextDataInjectors. Returns a new {@code ContextDataInjector} instance based on the value of system
  * property {@code log4j2.ContextDataInjector}. Users may use this system property to specify the fully qualified class
  * name of a class that implements the {@code ContextDataInjector} interface.
- * If no value was specified this factory method returns one of the injectors defined in
- * {@code ThreadContextDataInjector}.
  *
  * @see ContextDataInjector
  * @see ReadOnlyStringMap
  * @see ThreadContextDataInjector
  * @see LogEvent#getContextData()
  * @since 2.7
+ * @deprecated Use ContextDataProvider instead.
  */
+@Deprecated
 public class ContextDataInjectorFactory {
 
     private static final String CONTEXT_DATA_INJECTOR_PROPERTY = "log4j2.ContextDataInjector";
 
     /**
      * Returns a new {@code ContextDataInjector} instance based on the value of system property
-     * {@code log4j2.ContextDataInjector}. If no value was specified this factory method returns one of the
-     * {@code ContextDataInjector} classes defined in {@link ThreadContextDataInjector} which is most appropriate for
-     * the ThreadContext implementation.
-     * <p>
+     * {@code log4j2.ContextDataInjector}. If no value was specified then @{link ContextData} will be used.
      * <b>Note:</b> It is no longer recommended that users provide a custom implementation of the ContextDataInjector.
-     * Instead, provide a {@code ContextDataProvider}.
+     * Instead, provide a {@code ContextDataProvider}. Support for ContextDataInjectors will be removed entirely
+     * in 3.0.0.
      * </p>
      * <p>
      * Users may use this system property to specify the fully qualified class name of a class that implements the
@@ -65,16 +63,23 @@ public class ContextDataInjectorFactory {
      * @return a ContextDataInjector that populates the {@code ReadOnlyStringMap} of all {@code LogEvent} objects
      * @see LogEvent#getContextData()
      * @see ContextDataInjector
+     * @deprecated Uses ContextData instead.
      */
+    @Deprecated
     public static ContextDataInjector createInjector() {
+        return createInjector(false);
+    }
+
+    @Deprecated
+    public static ContextDataInjector createInjector(final boolean useDefault) {
         try {
             return LoaderUtil.newCheckedInstanceOfProperty(
                     CONTEXT_DATA_INJECTOR_PROPERTY,
                     ContextDataInjector.class,
-                    ContextDataInjectorFactory::createDefaultInjector);
+                    useDefault ? ContextDataInjectorFactory::createDefaultInjector : () -> null);
         } catch (final ReflectiveOperationException e) {
-            StatusLogger.getLogger().warn("Could not create ContextDataInjector: {}", e.getMessage(), e);
-            return createDefaultInjector();
+            StatusLogger.getLogger().info("Could not create ContextDataInjector: {}", e.getMessage(), e);
+            return null;
         }
     }
 
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java
index 09c4441394..73853865e6 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java
@@ -47,7 +47,7 @@ public class JdkMapAdapterStringMap implements StringMap {
     // It is a cache, no need to synchronise it between threads.
     private static Map<Class<?>, Void> UNMODIFIABLE_MAPS_CACHE = new WeakHashMap<>();
 
-    private final Map<String, String> map;
+    protected final Map<String, String> map;
     private boolean immutable = false;
     private transient String[] sortedKeys;
 
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java
index 7185bc7bc8..bc02b085bb 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java
@@ -24,6 +24,7 @@ import java.rmi.MarshalledObject;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import org.apache.logging.log4j.ContextData;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.ThreadContext;
@@ -680,6 +681,11 @@ public class Log4jLogEvent implements LogEvent {
 
     private static StringMap createContextData(final List<Property> properties) {
         final StringMap reusable = ContextDataFactory.createContextData();
+        if (CONTEXT_DATA_INJECTOR == null) {
+            copyProperties(properties, reusable);
+            ContextData.addAll(reusable);
+            return reusable;
+        }
         return CONTEXT_DATA_INJECTOR.injectContextData(properties, reusable);
     }
 
@@ -979,6 +985,21 @@ public class Log4jLogEvent implements LogEvent {
         throw new IllegalArgumentException("Event is not a serialized LogEvent: " + event.toString());
     }
 
+    /**
+     * Copies key-value pairs from the specified property list into the specified {@code StringMap}.
+     *
+     * @param properties list of configuration properties, may be {@code null}
+     * @param result the {@code StringMap} object to add the key-values to. Must be non-{@code null}.
+     */
+    private static void copyProperties(final List<Property> properties, final StringMap result) {
+        if (properties != null) {
+            for (int i = 0; i < properties.size(); i++) {
+                final Property prop = properties.get(i);
+                result.putValue(prop.getName(), prop.getValue());
+            }
+        }
+    }
+
     private void readObject(final ObjectInputStream stream) throws InvalidObjectException {
         throw new InvalidObjectException("Proxy required");
     }
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java
index e954d9b7f7..5a916d4003 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java
@@ -17,6 +17,7 @@
 package org.apache.logging.log4j.core.impl;
 
 import java.util.List;
+import org.apache.logging.log4j.ContextData;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.ThreadContext;
@@ -100,7 +101,13 @@ public class ReusableLogEventFactory implements LogEventFactory, LocationAwareLo
         result.initTime(CLOCK, Log4jLogEvent.getNanoClock());
         result.setThrown(t);
         result.setSource(location);
-        result.setContextData(injector.injectContextData(properties, (StringMap) result.getContextData()));
+        if (injector != null) {
+            result.setContextData(injector.injectContextData(properties, (StringMap) result.getContextData()));
+        } else {
+            StringMap reusable = (StringMap) result.getContextData();
+            copyProperties(properties, reusable);
+            ContextData.addAll(reusable);
+        }
         result.setContextStack(
                 ThreadContext.getDepth() == 0 ? ThreadContext.EMPTY_STACK : ThreadContext.cloneStack()); // mutable copy
 
@@ -111,6 +118,21 @@ public class ReusableLogEventFactory implements LogEventFactory, LocationAwareLo
         return result;
     }
 
+    /**
+     * Copies key-value pairs from the specified property list into the specified {@code StringMap}.
+     *
+     * @param properties list of configuration properties, may be {@code null}
+     * @param result the {@code StringMap} object to add the key-values to. Must be non-{@code null}.
+     */
+    private static void copyProperties(final List<Property> properties, final StringMap result) {
+        if (properties != null) {
+            for (int i = 0; i < properties.size(); i++) {
+                final Property prop = properties.get(i);
+                result.putValue(prop.getName(), prop.getValue());
+            }
+        }
+    }
+
     private static MutableLogEvent getOrCreateMutableLogEvent() {
         final MutableLogEvent result = mutableLogEventThreadLocal.get();
         return result == null || result.reserved ? createInstance(result) : result;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java
index 332b877a51..839c3c2df5 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java
@@ -16,17 +16,12 @@
  */
 package org.apache.logging.log4j.core.impl;
 
-import aQute.bnd.annotation.Cardinality;
-import aQute.bnd.annotation.Resolution;
-import aQute.bnd.annotation.spi.ServiceConsumer;
-import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.ServiceLoader;
-import java.util.concurrent.ConcurrentLinkedDeque;
+import org.apache.logging.log4j.ContextData;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.core.ContextDataInjector;
@@ -35,7 +30,6 @@ import org.apache.logging.log4j.core.util.ContextDataProvider;
 import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.ReadOnlyStringMap;
-import org.apache.logging.log4j.util.ServiceLoaderUtil;
 import org.apache.logging.log4j.util.StringMap;
 
 /**
@@ -43,7 +37,8 @@ import org.apache.logging.log4j.util.StringMap;
  * {@code ThreadContext} map implementations into a {@code StringMap}. In the case of duplicate keys,
  * thread context values overwrite configuration {@code Property} values.
  * <p>
- * These are the default {@code ContextDataInjector} objects returned by the {@link ContextDataInjectorFactory}.
+ * This class is no longer directly used by Log4j. It is only present in case it is being overridden by a user.
+ * Will be removed in 3.0.0.
  * </p>
  *
  * @see org.apache.logging.log4j.ThreadContext
@@ -52,11 +47,9 @@ import org.apache.logging.log4j.util.StringMap;
  * @see ContextDataInjector
  * @see ContextDataInjectorFactory
  * @since 2.7
+ * @Deprecated Use @{link ContextData} instead.
  */
-@ServiceConsumer(
-        value = ContextDataProvider.class,
-        resolution = Resolution.OPTIONAL,
-        cardinality = Cardinality.MULTIPLE)
+@Deprecated
 public class ThreadContextDataInjector {
 
     private static final Logger LOGGER = StatusLogger.getLogger();
@@ -64,9 +57,7 @@ public class ThreadContextDataInjector {
     /**
      * ContextDataProviders loaded via OSGi.
      */
-    public static Collection<ContextDataProvider> contextDataProviders = new ConcurrentLinkedDeque<>();
-
-    private static final List<ContextDataProvider> SERVICE_PROVIDERS = getServiceProviders();
+    public static Collection<ContextDataProvider> contextDataProviders = new ProviderQueue();
 
     /**
      * Previously this method allowed ContextDataProviders to be loaded eagerly, now they
@@ -77,16 +68,6 @@ public class ThreadContextDataInjector {
     @Deprecated
     public static void initServiceProviders() {}
 
-    private static List<ContextDataProvider> getServiceProviders() {
-        final List<ContextDataProvider> providers = new ArrayList<>();
-        ServiceLoaderUtil.safeStream(
-                        ContextDataProvider.class,
-                        ServiceLoader.load(ContextDataProvider.class, ThreadContextDataInjector.class.getClassLoader()),
-                        LOGGER)
-                .forEach(providers::add);
-        return Collections.unmodifiableList(providers);
-    }
-
     /**
      * Default {@code ContextDataInjector} for the legacy {@code Map<String, String>}-based ThreadContext (which is
      * also the ThreadContext implementation used for web applications).
@@ -95,11 +76,7 @@ public class ThreadContextDataInjector {
      */
     public static class ForDefaultThreadContextMap implements ContextDataInjector {
 
-        private final List<ContextDataProvider> providers;
-
-        public ForDefaultThreadContextMap() {
-            providers = getProviders();
-        }
+        public ForDefaultThreadContextMap() {}
 
         /**
          * Puts key-value pairs from both the specified list of properties as well as the thread context into the
@@ -111,44 +88,12 @@ public class ThreadContextDataInjector {
          */
         @Override
         public StringMap injectContextData(final List<Property> props, final StringMap ignore) {
-
-            final Map<String, String> copy;
-
-            if (providers.size() == 1) {
-                copy = providers.get(0).supplyContextData();
-            } else {
-                copy = new HashMap<>();
-                for (ContextDataProvider provider : providers) {
-                    copy.putAll(provider.supplyContextData());
-                }
-            }
-
-            // The DefaultThreadContextMap stores context data in a Map<String, String>.
-            // This is a copy-on-write data structure so we are sure ThreadContext changes will not affect our copy.
-            // If there are no configuration properties or providers returning a thin wrapper around the copy
-            // is faster than copying the elements into the LogEvent's reusable StringMap.
-            if ((props == null || props.isEmpty())) {
-                // this will replace the LogEvent's context data with the returned instance.
-                // NOTE: must mark as frozen or downstream components may attempt to modify (UnsupportedOperationEx)
-                return copy.isEmpty() ? ContextDataFactory.emptyFrozenContextData() : frozenStringMap(copy);
-            }
-            // If the list of Properties is non-empty we need to combine the properties and the ThreadContext
-            // data. Note that we cannot reuse the specified StringMap: some Loggers may have properties defined
-            // and others not, so the LogEvent's context data may have been replaced with an immutable copy from
-            // the ThreadContext - this will throw an UnsupportedOperationException if we try to modify it.
-            final StringMap result = new JdkMapAdapterStringMap(new HashMap<>(copy), false);
-            for (int i = 0; i < props.size(); i++) {
-                final Property prop = props.get(i);
-                if (!copy.containsKey(prop.getName())) {
-                    result.putValue(prop.getName(), prop.getValue());
-                }
-            }
-            result.freeze();
-            return result;
-        }
-
-        private static JdkMapAdapterStringMap frozenStringMap(final Map<String, String> copy) {
-            return new JdkMapAdapterStringMap(copy, true);
+            Map<String, String> map = new HashMap<>();
+            JdkMapAdapterStringMap stringMap = new JdkMapAdapterStringMap(map, false);
+            copyProperties(props, stringMap);
+            ContextData.addAll(map);
+            stringMap.freeze();
+            return stringMap;
         }
 
         @Override
@@ -172,11 +117,8 @@ public class ThreadContextDataInjector {
      * This injector always puts key-value pairs into the specified reusable StringMap.
      */
     public static class ForGarbageFreeThreadContextMap implements ContextDataInjector {
-        private final List<ContextDataProvider> providers;
 
-        public ForGarbageFreeThreadContextMap() {
-            this.providers = getProviders();
-        }
+        public ForGarbageFreeThreadContextMap() {}
 
         /**
          * Puts key-value pairs from both the specified list of properties as well as the thread context into the
@@ -192,12 +134,13 @@ public class ThreadContextDataInjector {
             // StringMap. We cannot return the ThreadContext's internal data structure because it may be modified later
             // and such modifications should not be reflected in the log event.
             copyProperties(props, reusable);
-            for (int i = 0; i < providers.size(); ++i) {
-                reusable.putAll(providers.get(i).supplyStringMap());
-            }
+            ContextData.addAll(reusable);
             return reusable;
         }
 
+        /*
+           No longer used.
+        */
         @Override
         public ReadOnlyStringMap rawContextData() {
             return ThreadContext.getThreadContextMap().getReadOnlyContextData();
@@ -205,59 +148,9 @@ public class ThreadContextDataInjector {
     }
 
     /**
-     * The {@code ContextDataInjector} used when the ThreadContextMap implementation is a copy-on-write
-     * StringMap-based data structure.
-     * <p>
-     * If there are no configuration properties, this injector will return the thread context's internal data
-     * structure. Otherwise the configuration properties are combined with the thread context key-value pairs into the
-     * specified reusable StringMap.
+     * Th
      */
-    public static class ForCopyOnWriteThreadContextMap implements ContextDataInjector {
-        private final List<ContextDataProvider> providers;
-
-        public ForCopyOnWriteThreadContextMap() {
-            this.providers = getProviders();
-        }
-        /**
-         * If there are no configuration properties, this injector will return the thread context's internal data
-         * structure. Otherwise the configuration properties are combined with the thread context key-value pairs into the
-         * specified reusable StringMap.
-         *
-         * @param props list of configuration properties, may be {@code null}
-         * @param ignore a {@code StringMap} instance from the log event
-         * @return a {@code StringMap} combining configuration properties with thread context data
-         */
-        @Override
-        public StringMap injectContextData(final List<Property> props, final StringMap ignore) {
-            // If there are no configuration properties we want to just return the ThreadContext's StringMap:
-            // it is a copy-on-write data structure so we are sure ThreadContext changes will not affect our copy.
-            if (providers.size() == 1 && (props == null || props.isEmpty())) {
-                // this will replace the LogEvent's context data with the returned instance
-                return providers.get(0).supplyStringMap();
-            }
-            int count = props == null ? 0 : props.size();
-            final StringMap[] maps = new StringMap[providers.size()];
-            for (int i = 0; i < providers.size(); ++i) {
-                maps[i] = providers.get(i).supplyStringMap();
-                count += maps[i].size();
-            }
-            // However, if the list of Properties is non-empty we need to combine the properties and the ThreadContext
-            // data. Note that we cannot reuse the specified StringMap: some Loggers may have properties defined
-            // and others not, so the LogEvent's context data may have been replaced with an immutable copy from
-            // the ThreadContext - this will throw an UnsupportedOperationException if we try to modify it.
-            final StringMap result = ContextDataFactory.createContextData(count);
-            copyProperties(props, result);
-            for (StringMap map : maps) {
-                result.putAll(map);
-            }
-            return result;
-        }
-
-        @Override
-        public ReadOnlyStringMap rawContextData() {
-            return ThreadContext.getThreadContextMap().getReadOnlyContextData();
-        }
-    }
+    public static class ForCopyOnWriteThreadContextMap extends ForDefaultThreadContextMap {}
 
     /**
      * Copies key-value pairs from the specified property list into the specified {@code StringMap}.
@@ -274,11 +167,134 @@ public class ThreadContextDataInjector {
         }
     }
 
-    private static List<ContextDataProvider> getProviders() {
-        final List<ContextDataProvider> providers =
-                new ArrayList<>(contextDataProviders.size() + SERVICE_PROVIDERS.size());
-        providers.addAll(contextDataProviders);
-        providers.addAll(SERVICE_PROVIDERS);
-        return providers;
+    private static class ProviderQueue implements Collection<ContextDataProvider> {
+        @Override
+        public int size() {
+            return ContextData.contextDataProviders.size();
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return ContextData.contextDataProviders.isEmpty();
+        }
+
+        @Override
+        public boolean contains(Object o) {
+            return ContextData.contextDataProviders.contains(o);
+        }
+
+        @Override
+        public Iterator<ContextDataProvider> iterator() {
+            return new ProviderIterator(ContextData.contextDataProviders.iterator());
+        }
+
+        @Override
+        public Object[] toArray() {
+            return ContextData.contextDataProviders.toArray();
+        }
+
+        @Override
+        public <T> T[] toArray(T[] a) {
+            return ContextData.contextDataProviders.toArray(a);
+        }
+
+        @Override
+        public boolean add(ContextDataProvider contextDataProvider) {
+            return ContextData.contextDataProviders.add(contextDataProvider);
+        }
+
+        @Override
+        public boolean remove(Object o) {
+            return ContextData.contextDataProviders.remove(o);
+        }
+
+        @Override
+        public boolean containsAll(Collection<?> c) {
+            return ContextData.contextDataProviders.containsAll(c);
+        }
+
+        @Override
+        public boolean addAll(Collection<? extends ContextDataProvider> c) {
+            return false;
+        }
+
+        @Override
+        public boolean removeAll(Collection<?> c) {
+            return ContextData.contextDataProviders.removeAll(c);
+        }
+
+        @Override
+        public boolean retainAll(Collection<?> c) {
+            return ContextData.contextDataProviders.retainAll(c);
+        }
+
+        @Override
+        public void clear() {
+            ContextData.contextDataProviders.clear();
+        }
+    }
+
+    private static class ProviderIterator implements Iterator<ContextDataProvider> {
+
+        private final Iterator<org.apache.logging.log4j.spi.ContextDataProvider> iter;
+
+        public ProviderIterator(Iterator<org.apache.logging.log4j.spi.ContextDataProvider> iter) {
+            this.iter = iter;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return iter.hasNext();
+        }
+
+        @Override
+        public ContextDataProvider next() {
+            org.apache.logging.log4j.spi.ContextDataProvider next = iter.next();
+            if (next instanceof ContextDataProvider) {
+                return (ContextDataProvider) next;
+            } else if (next != null) {
+                return new ProviderWrapper(next);
+            }
+            return null;
+        }
+    }
+
+    private static class ProviderWrapper implements ContextDataProvider {
+
+        private final org.apache.logging.log4j.spi.ContextDataProvider provider;
+
+        public ProviderWrapper(org.apache.logging.log4j.spi.ContextDataProvider provider) {
+            this.provider = provider;
+        }
+
+        @Override
+        public String get(String key) {
+            return provider.get(key);
+        }
+
+        @Override
+        public int size() {
+            return provider.size();
+        }
+
+        @Override
+        public void addAll(Map<String, String> map) {
+            provider.addAll(map);
+        }
+
+        @Override
+        public void addAll(StringMap map) {
+            provider.addAll(map);
+        }
+
+        @Override
+        public Map<String, String> supplyContextData() {
+            return provider.supplyContextData();
+        }
+
+        @Override
+        public StringMap supplyStringMap() {
+            return new JdkMapAdapterStringMap(supplyContextData(), true);
+        }
     }
 }
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java
index 86778d2bd7..b24f1203a8 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java
@@ -16,17 +16,18 @@
  */
 package org.apache.logging.log4j.core.lookup;
 
+import org.apache.logging.log4j.ContextData;
+import org.apache.logging.log4j.ScopedContext;
 import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.core.ContextDataInjector;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.config.plugins.Plugin;
 import org.apache.logging.log4j.core.impl.ContextDataInjectorFactory;
-import org.apache.logging.log4j.util.ReadOnlyStringMap;
 
 /**
- * Looks up keys from the context. By default this is the {@link ThreadContext}, but users may
- * {@linkplain ContextDataInjectorFactory configure} a custom {@link ContextDataInjector} which obtains context data
- * from some other source.
+ * Looks up keys from the context. By default this is the {@link ThreadContext} or {@link ScopedContext}. Users may
+ * add their own {@link org.apache.logging.log4j.core.util.ContextDataProvider} which can be retrieved via this
+ * Lookup.
  */
 @Plugin(name = "ctx", category = StrLookup.CATEGORY)
 public class ContextMapLookup implements StrLookup {
@@ -40,11 +41,10 @@ public class ContextMapLookup implements StrLookup {
      */
     @Override
     public String lookup(final String key) {
-        return currentContextData().getValue(key);
-    }
-
-    private ReadOnlyStringMap currentContextData() {
-        return injector.rawContextData();
+        if (injector == null) {
+            return ContextData.getValue(key);
+        }
+        return injector.getValue(key);
     }
 
     /**
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/package-info.java
index 12526349ce..d07f21e49e 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/package-info.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/package-info.java
@@ -21,7 +21,7 @@
  * {@link org.apache.logging.log4j.core.lookup.StrLookup#CATEGORY Lookup}.
  */
 @Export
-@Version("2.20.1")
+@Version("2.24.0")
 package org.apache.logging.log4j.core.lookup;
 
 import org.osgi.annotation.bundle.Export;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java
index 4fae66de22..17bde215ff 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java
@@ -18,14 +18,14 @@ package org.apache.logging.log4j.core.osgi;
 
 import java.util.Collection;
 import java.util.concurrent.atomic.AtomicReference;
+import org.apache.logging.log4j.ContextData;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.core.config.plugins.util.PluginRegistry;
 import org.apache.logging.log4j.core.impl.Log4jProvider;
-import org.apache.logging.log4j.core.impl.ThreadContextDataInjector;
-import org.apache.logging.log4j.core.impl.ThreadContextDataProvider;
 import org.apache.logging.log4j.core.util.Constants;
-import org.apache.logging.log4j.core.util.ContextDataProvider;
+import org.apache.logging.log4j.spi.ContextDataProvider;
+import org.apache.logging.log4j.spi.ThreadContextDataProvider;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.PropertiesUtil;
 import org.apache.logging.log4j.util.ProviderActivator;
@@ -99,7 +99,7 @@ public final class Activator extends ProviderActivator implements SynchronousBun
                     bundleContext.getServiceReferences(ContextDataProvider.class, null);
             for (final ServiceReference<ContextDataProvider> serviceReference : serviceReferences) {
                 final ContextDataProvider provider = bundleContext.getService(serviceReference);
-                ThreadContextDataInjector.contextDataProviders.add(provider);
+                ContextData.addProvider(provider);
             }
         } catch (final InvalidSyntaxException ex) {
             LOGGER.error("Error accessing context data provider", ex);
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/package-info.java
index a11875eea4..266256b463 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/package-info.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/package-info.java
@@ -18,7 +18,7 @@
  * Implementation of Log4j 2.
  */
 @Export
-@Version("2.20.2")
+@Version("2.24.0")
 package org.apache.logging.log4j.core;
 
 import org.osgi.annotation.bundle.Export;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java
index 8ac63b6858..086ac93981 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java
@@ -22,17 +22,11 @@ import org.apache.logging.log4j.util.StringMap;
 
 /**
  * Source of context data to be added to each log event.
+ * @deprecated Use ContextDataProvider from Log4j API from 2.24.0.
  */
-public interface ContextDataProvider {
+@Deprecated
+public interface ContextDataProvider extends org.apache.logging.log4j.spi.ContextDataProvider {
 
-    /**
-     * Returns a Map containing context data to be injected into the event or null if no context data is to be added.
-     * <p>
-     *     Thread-safety note: The returned object can safely be passed off to another thread: future changes in the
-     *     underlying context data will not be reflected in the returned object.
-     * </p>
-     * @return A Map containing the context data or null.
-     */
     Map<String, String> supplyContextData();
 
     /**
@@ -42,7 +36,9 @@ public interface ContextDataProvider {
      *     underlying context data will not be reflected in the returned object.
      * </p>
      * @return the context data in a StringMap.
+     * @deprecated No longer used since 2.24.0. Will be removed in 3.0.0.
      */
+    @Deprecated
     default StringMap supplyStringMap() {
         return new JdkMapAdapterStringMap(supplyContextData(), true);
     }
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/package-info.java
index 753b7c456b..6c60254605 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/package-info.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/package-info.java
@@ -18,7 +18,7 @@
  * Log4j 2 helper classes.
  */
 @Export
-@Version("2.20.2")
+@Version("2.24.0")
 package org.apache.logging.log4j.core.util;
 
 import org.osgi.annotation.bundle.Export;