From f43b42de9b4863b0b79f02fed362cb1e23f76384 Mon Sep 17 00:00:00 2001
From: "Piotr P. Karwasz"
Date: Sun, 12 May 2024 21:26:45 +0200
Subject: [PATCH 1/4] Move `ThreadContextMap` implementations to `log4j-core`
We move `ThreadContextMap` alternative implementations to `log4j-core`.
These thread context maps are not useful to other `log4j-api`
implementations:
* `log4j-to-slf4j` delegates everything to SLF4J's `MDCAdapter`,
* `log4j-to-jul` uses the no-op context map.
---
.../log4j/test/ThreadContextUtilityClass.java | 4 -
.../test/junit/InitializesThreadContext.java | 39 ---
.../test/junit/ThreadContextInitializer.java | 58 ----
.../spi/AbstractThreadContextMapTest.java | 64 +++++
.../log4j/ThreadContextInheritanceTest.java | 28 --
.../spi/DefaultThreadContextMapTest.java | 35 ++-
log4j-api/pom.xml | 11 +-
.../apache/logging/log4j/ThreadContext.java | 31 +--
...opyOnWriteSortedArrayThreadContextMap.java | 255 -----------------
.../log4j/spi/DefaultThreadContextMap.java | 38 +--
.../log4j/spi/NoOpThreadContextMap.java | 22 +-
.../apache/logging/log4j/spi/Provider.java | 260 ++++--------------
.../logging/log4j/spi/ThreadContextMap.java | 64 +++--
.../log4j/spi/ThreadContextMapFactory.java | 4 +-
.../log4j/ThreadContextTestAccess.java | 34 ---
.../AbstractAsyncThreadContextTestBase.java | 55 ++--
.../StringArrayThreadContextMapTest.java | 22 +-
.../core/context}/ThreadContextMapTest.java | 37 +--
.../UnmodifiableArrayBackedMapTest.java | 2 +-
.../impl/ThreadContextDataInjectorTest.java | 40 ++-
log4j-core/pom.xml | 7 +
.../AbstractSortedArrayThreadContextMap.java | 134 ++++-----
...opyOnWriteSortedArrayThreadContextMap.java | 100 +++++++
...arbageFreeSortedArrayThreadContextMap.java | 54 ++++
.../context}/StringArrayThreadContextMap.java | 26 +-
.../internal}/UnmodifiableArrayBackedMap.java | 28 +-
.../log4j/core/impl/Log4jProvider.java | 128 +++++++++
...opyOnWriteOpenHashMapThreadContextMap.java | 1 +
...arbageFreeOpenHashMapThreadContextMap.java | 1 +
.../2330_add_faster_web_app_context_map.xml | 2 +-
30 files changed, 665 insertions(+), 919 deletions(-)
delete mode 100644 log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/InitializesThreadContext.java
delete mode 100644 log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/ThreadContextInitializer.java
create mode 100644 log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/AbstractThreadContextMapTest.java
delete mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java
delete mode 100644 log4j-core-test/src/test/java/org/apache/logging/log4j/ThreadContextTestAccess.java
rename {log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map => log4j-core-test/src/test/java/org/apache/logging/log4j/core/context}/StringArrayThreadContextMapTest.java (92%)
rename {log4j-api-test/src/test/java/org/apache/logging/log4j/spi => log4j-core-test/src/test/java/org/apache/logging/log4j/core/context}/ThreadContextMapTest.java (60%)
rename {log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map => log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/internal}/UnmodifiableArrayBackedMapTest.java (99%)
rename log4j-api/src/main/java/org/apache/logging/log4j/spi/GarbageFreeSortedArrayThreadContextMap.java => log4j-core/src/main/java/org/apache/logging/log4j/core/context/AbstractSortedArrayThreadContextMap.java (59%)
create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/context/CopyOnWriteSortedArrayThreadContextMap.java
create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/context/GarbageFreeSortedArrayThreadContextMap.java
rename {log4j-api/src/main/java/org/apache/logging/log4j/internal/map => log4j-core/src/main/java/org/apache/logging/log4j/core/context}/StringArrayThreadContextMap.java (90%)
rename {log4j-api/src/main/java/org/apache/logging/log4j/internal/map => log4j-core/src/main/java/org/apache/logging/log4j/core/context/internal}/UnmodifiableArrayBackedMap.java (96%)
diff --git a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/ThreadContextUtilityClass.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/ThreadContextUtilityClass.java
index 396039a50b1..e8985f1a934 100644
--- a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/ThreadContextUtilityClass.java
+++ b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/ThreadContextUtilityClass.java
@@ -112,8 +112,4 @@ public static void testPut() {
ThreadContext.put("testKey", "testValue");
assertEquals("testValue", ThreadContext.get("testKey"));
}
-
- public static void reset() {
- ThreadContext.init();
- }
}
diff --git a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/InitializesThreadContext.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/InitializesThreadContext.java
deleted file mode 100644
index abbb616dc0e..00000000000
--- a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/InitializesThreadContext.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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.test.junit;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Inherited;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-import org.apache.logging.log4j.ThreadContext;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.jupiter.api.parallel.ResourceAccessMode;
-import org.junit.jupiter.api.parallel.ResourceLock;
-
-/**
- * Marks a test class that initializes the {@link ThreadContext} class;
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.TYPE, ElementType.METHOD})
-@Documented
-@Inherited
-@ExtendWith(ThreadContextInitializer.class)
-@ResourceLock(value = Log4jStaticResources.THREAD_CONTEXT, mode = ResourceAccessMode.READ_WRITE)
-public @interface InitializesThreadContext {}
diff --git a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/ThreadContextInitializer.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/ThreadContextInitializer.java
deleted file mode 100644
index 8afceef7378..00000000000
--- a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/ThreadContextInitializer.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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.test.junit;
-
-import org.apache.logging.log4j.ThreadContext;
-import org.apache.logging.log4j.test.ThreadContextUtilityClass;
-import org.junit.jupiter.api.extension.BeforeAllCallback;
-import org.junit.jupiter.api.extension.BeforeEachCallback;
-import org.junit.jupiter.api.extension.ExtensionContext;
-import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;
-import org.junit.platform.commons.support.AnnotationSupport;
-
-class ThreadContextInitializer implements BeforeAllCallback, BeforeEachCallback {
-
- @Override
- public void beforeAll(final ExtensionContext context) throws Exception {
- if (AnnotationSupport.isAnnotated(context.getRequiredTestClass(), InitializesThreadContext.class)) {
- resetThreadContext(context);
- }
- }
-
- @Override
- public void beforeEach(final ExtensionContext context) throws Exception {
- if (AnnotationSupport.isAnnotated(context.getRequiredTestMethod(), InitializesThreadContext.class)) {
- resetThreadContext(context);
- }
- }
-
- private void resetThreadContext(final ExtensionContext context) {
- ThreadContextUtilityClass.reset();
- // We use `CloseableResource` instead of `afterAll` to reset the
- // ThreadContextFactory
- // *after* the `@SetSystemProperty` extension has restored the properties
- ExtensionContextAnchor.setAttribute(
- ThreadContext.class,
- new CloseableResource() {
- @Override
- public void close() throws Throwable {
- ThreadContextUtilityClass.reset();
- }
- },
- context);
- }
-}
diff --git a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/AbstractThreadContextMapTest.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/AbstractThreadContextMapTest.java
new file mode 100644
index 00000000000..4c7c9c157be
--- /dev/null
+++ b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/AbstractThreadContextMapTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.test.spi;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Duration;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.apache.logging.log4j.spi.ThreadContextMap;
+
+/**
+ * Provides a set of utility methods to test implementations of {@link ThreadContextMap}.
+ */
+public abstract class AbstractThreadContextMapTest {
+
+ private static final String KEY = "key";
+
+ /**
+ * Implementations SHOULD not propagate the context to newly created threads by default.
+ *
+ * @param contextMap A {@link ThreadContextMap implementation}.
+ */
+ protected static void assertThreadLocalNotInheritable(final ThreadContextMap contextMap) {
+ contextMap.put(KEY, "threadLocalNotInheritableByDefault");
+ verifyThreadContextValueFromANewThread(contextMap, null);
+ }
+
+ /**
+ * Implementations MAY offer a configuration that propagates the context to newly created threads.
+ *
+ * @param contextMap A {@link ThreadContextMap implementation}.
+ */
+ protected static void assertThreadLocalInheritable(final ThreadContextMap contextMap) {
+ contextMap.put(KEY, "threadLocalInheritableIfConfigured");
+ verifyThreadContextValueFromANewThread(contextMap, "threadLocalInheritableIfConfigured");
+ }
+
+ private static void verifyThreadContextValueFromANewThread(
+ final ThreadContextMap contextMap, final String expected) {
+ final ExecutorService executorService = Executors.newSingleThreadExecutor();
+ try {
+ assertThat(executorService.submit(() -> contextMap.get(KEY)))
+ .succeedsWithin(Duration.ofSeconds(1))
+ .isEqualTo(expected);
+ } finally {
+ executorService.shutdown();
+ }
+ }
+}
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextInheritanceTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextInheritanceTest.java
index 96783ae600c..c800b396a61 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextInheritanceTest.java
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextInheritanceTest.java
@@ -23,7 +23,6 @@
import org.apache.logging.log4j.spi.DefaultThreadContextMap;
import org.apache.logging.log4j.test.ThreadContextUtilityClass;
-import org.apache.logging.log4j.test.junit.InitializesThreadContext;
import org.apache.logging.log4j.test.junit.SetTestProperty;
import org.apache.logging.log4j.test.junit.UsingThreadContextMap;
import org.apache.logging.log4j.test.junit.UsingThreadContextStack;
@@ -31,13 +30,11 @@
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
-import org.junitpioneer.jupiter.SetSystemProperty;
/**
* Tests {@link ThreadContext}.
*/
@SetTestProperty(key = DefaultThreadContextMap.INHERITABLE_MAP, value = "true")
-@InitializesThreadContext
@UsingThreadContextMap
@UsingThreadContextStack
public class ThreadContextInheritanceTest {
@@ -63,31 +60,6 @@ public void testPush() {
assertEquals(ThreadContext.pop(), "Hello", "Incorrect simple stack value");
}
- @Test
- @SetSystemProperty(key = DefaultThreadContextMap.INHERITABLE_MAP, value = "true")
- @InitializesThreadContext
- public void testInheritanceSwitchedOn() throws Exception {
- System.setProperty(DefaultThreadContextMap.INHERITABLE_MAP, "true");
- try {
- ThreadContext.clearMap();
- ThreadContext.put("Greeting", "Hello");
- StringBuilder sb = new StringBuilder();
- TestThread thread = new TestThread(sb);
- thread.start();
- thread.join();
- String str = sb.toString();
- assertEquals("Hello", str, "Unexpected ThreadContext value. Expected Hello. Actual " + str);
- sb = new StringBuilder();
- thread = new TestThread(sb);
- thread.start();
- thread.join();
- str = sb.toString();
- assertEquals("Hello", str, "Unexpected ThreadContext value. Expected Hello. Actual " + str);
- } finally {
- System.clearProperty(DefaultThreadContextMap.INHERITABLE_MAP);
- }
- }
-
@Test
@Tag("performance")
public void perfTest() {
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/DefaultThreadContextMapTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/DefaultThreadContextMapTest.java
index dedc5be86b8..43c92dbb5ae 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/DefaultThreadContextMapTest.java
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/DefaultThreadContextMapTest.java
@@ -24,31 +24,17 @@
import java.util.HashMap;
import java.util.Map;
+import java.util.Properties;
import org.apache.logging.log4j.test.junit.UsingThreadContextMap;
+import org.apache.logging.log4j.test.spi.AbstractThreadContextMapTest;
+import org.apache.logging.log4j.util.PropertiesUtil;
import org.junit.jupiter.api.Test;
/**
* Tests the {@code DefaultThreadContextMap} class.
*/
@UsingThreadContextMap
-public class DefaultThreadContextMapTest {
-
- @Test
- public void testEqualsVsSameKind() {
- final DefaultThreadContextMap map1 = createMap();
- final DefaultThreadContextMap map2 = createMap();
- assertEquals(map1, map1);
- assertEquals(map2, map2);
- assertEquals(map1, map2);
- assertEquals(map2, map1);
- }
-
- @Test
- public void testHashCodeVsSameKind() {
- final DefaultThreadContextMap map1 = createMap();
- final DefaultThreadContextMap map2 = createMap();
- assertEquals(map1.hashCode(), map2.hashCode());
- }
+class DefaultThreadContextMapTest extends AbstractThreadContextMapTest {
@Test
public void testDoesNothingIfConstructedWithUseMapIsFalse() {
@@ -214,4 +200,17 @@ public void testToStringShowsMapContext() {
map.put("key2", "value2");
assertEquals("{key2=value2}", map.toString());
}
+
+ @Test
+ void threadLocalNotInheritableByDefault() {
+ assertThreadLocalNotInheritable(new DefaultThreadContextMap());
+ }
+
+ @Test
+ void threadLocalInheritableIfConfigured() {
+ final Properties props = new Properties();
+ props.setProperty("log4j2.isThreadContextMapInheritable", "true");
+ final PropertiesUtil util = new PropertiesUtil(props);
+ assertThreadLocalInheritable(new DefaultThreadContextMap(true, util));
+ }
}
diff --git a/log4j-api/pom.xml b/log4j-api/pom.xml
index af22fee1659..5423c4f99fb 100644
--- a/log4j-api/pom.xml
+++ b/log4j-api/pom.xml
@@ -46,7 +46,9 @@
org.apache.logging.log4j
- !sun.reflect
+ !sun.reflect,
+
+ org.jspecify.*;resolution:=optional
@@ -57,6 +59,13 @@
+
+
+ org.jspecify
+ jspecify
+ provided
+
+
org.osgiorg.osgi.core
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java b/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java
index ddc36de0304..50cebbee446 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java
@@ -23,15 +23,12 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
-import org.apache.logging.log4j.internal.map.StringArrayThreadContextMap;
import org.apache.logging.log4j.message.ParameterizedMessage;
-import org.apache.logging.log4j.spi.CleanableThreadContextMap;
import org.apache.logging.log4j.spi.DefaultThreadContextMap;
import org.apache.logging.log4j.spi.DefaultThreadContextStack;
import org.apache.logging.log4j.spi.MutableThreadContextStack;
import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
import org.apache.logging.log4j.spi.ThreadContextMap;
-import org.apache.logging.log4j.spi.ThreadContextMap2;
import org.apache.logging.log4j.spi.ThreadContextMapFactory;
import org.apache.logging.log4j.spi.ThreadContextStack;
import org.apache.logging.log4j.util.PropertiesUtil;
@@ -268,21 +265,11 @@ public static void putIfNull(final String key, final String value) {
*
*
If the current thread does not have a context map it is
* created as a side effect.
- * @param m The map.
+ * @param map The map.
* @since 2.7
*/
- public static void putAll(final Map m) {
- if (contextMap instanceof ThreadContextMap2) {
- ((ThreadContextMap2) contextMap).putAll(m);
- } else if (contextMap instanceof DefaultThreadContextMap) {
- ((DefaultThreadContextMap) contextMap).putAll(m);
- } else if (contextMap instanceof StringArrayThreadContextMap) {
- ((StringArrayThreadContextMap) contextMap).putAll(m);
- } else {
- for (final Map.Entry entry : m.entrySet()) {
- contextMap.put(entry.getKey(), entry.getValue());
- }
- }
+ public static void putAll(final Map map) {
+ contextMap.putAll(map);
}
/**
@@ -316,17 +303,7 @@ public static void remove(final String key) {
* @since 2.8
*/
public static void removeAll(final Iterable keys) {
- if (contextMap instanceof CleanableThreadContextMap) {
- ((CleanableThreadContextMap) contextMap).removeAll(keys);
- } else if (contextMap instanceof DefaultThreadContextMap) {
- ((DefaultThreadContextMap) contextMap).removeAll(keys);
- } else if (contextMap instanceof StringArrayThreadContextMap) {
- ((StringArrayThreadContextMap) contextMap).removeAll(keys);
- } else {
- for (final String key : keys) {
- contextMap.remove(key);
- }
- }
+ contextMap.removeAll(keys);
}
/**
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java
deleted file mode 100644
index ca9f501c217..00000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java
+++ /dev/null
@@ -1,255 +0,0 @@
-/*
- * 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.spi;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import org.apache.logging.log4j.util.PropertiesUtil;
-import org.apache.logging.log4j.util.ReadOnlyStringMap;
-import org.apache.logging.log4j.util.SortedArrayStringMap;
-import org.apache.logging.log4j.util.StringMap;
-
-/**
- * {@code SortedArrayStringMap}-based implementation of the {@code ThreadContextMap} interface that creates a copy of
- * the data structure on every modification. Any particular instance of the data structure is a snapshot of the
- * ThreadContext at some point in time and can safely be passed off to other threads. Since it is
- * expected that the Map will be passed to many more log events than the number of keys it contains the performance
- * should be much better than if the Map was copied for each event.
- *
- * @since 2.7
- */
-class CopyOnWriteSortedArrayThreadContextMap implements ReadOnlyThreadContextMap, ObjectThreadContextMap, CopyOnWrite {
-
- /**
- * Property name ({@value} ) for selecting {@code InheritableThreadLocal} (value "true") or plain
- * {@code ThreadLocal} (value is not "true") in the implementation.
- */
- public static final String INHERITABLE_MAP = "isThreadContextMapInheritable";
-
- /**
- * The default initial capacity.
- */
- protected static final int DEFAULT_INITIAL_CAPACITY = 16;
-
- /**
- * System property name that can be used to control the data structure's initial capacity.
- */
- protected static final String PROPERTY_NAME_INITIAL_CAPACITY = "log4j2.ThreadContext.initial.capacity";
-
- private static final StringMap EMPTY_CONTEXT_DATA = new SortedArrayStringMap(1);
-
- static {
- EMPTY_CONTEXT_DATA.freeze();
- }
-
- private final int initialCapacity;
- private final ThreadLocal localMap;
-
- public CopyOnWriteSortedArrayThreadContextMap() {
- this(PropertiesUtil.getProperties());
- }
-
- CopyOnWriteSortedArrayThreadContextMap(final PropertiesUtil properties) {
- initialCapacity = properties.getIntegerProperty(PROPERTY_NAME_INITIAL_CAPACITY, DEFAULT_INITIAL_CAPACITY);
- localMap = properties.getBooleanProperty(INHERITABLE_MAP)
- ? new InheritableThreadLocal() {
- @Override
- protected StringMap childValue(final StringMap parentValue) {
- if (parentValue == null) {
- return null;
- }
- final StringMap stringMap = createStringMap(parentValue);
- stringMap.freeze();
- return stringMap;
- }
- }
- : new ThreadLocal();
- }
-
- /**
- * Returns an implementation of the {@code StringMap} used to back this thread context map.
- *
- * Subclasses may override.
- *
- * @return an implementation of the {@code StringMap} used to back this thread context map
- */
- protected StringMap createStringMap() {
- return new SortedArrayStringMap(initialCapacity);
- }
-
- /**
- * Returns an implementation of the {@code StringMap} used to back this thread context map, pre-populated
- * with the contents of the specified context data.
- *
- * Subclasses may override.
- *
- * @param original the key-value pairs to initialize the returned context data with
- * @return an implementation of the {@code StringMap} used to back this thread context map
- */
- protected StringMap createStringMap(final ReadOnlyStringMap original) {
- return new SortedArrayStringMap(original);
- }
-
- @Override
- public void put(final String key, final String value) {
- putValue(key, value);
- }
-
- @Override
- public void putValue(final String key, final Object value) {
- StringMap map = localMap.get();
- map = map == null ? createStringMap() : createStringMap(map);
- map.putValue(key, value);
- map.freeze();
- localMap.set(map);
- }
-
- @Override
- public void putAll(final Map values) {
- if (values == null || values.isEmpty()) {
- return;
- }
- StringMap map = localMap.get();
- map = map == null ? createStringMap() : createStringMap(map);
- for (final Map.Entry entry : values.entrySet()) {
- map.putValue(entry.getKey(), entry.getValue());
- }
- map.freeze();
- localMap.set(map);
- }
-
- @Override
- public void putAllValues(final Map values) {
- if (values == null || values.isEmpty()) {
- return;
- }
- StringMap map = localMap.get();
- map = map == null ? createStringMap() : createStringMap(map);
- for (final Map.Entry entry : values.entrySet()) {
- map.putValue(entry.getKey(), entry.getValue());
- }
- map.freeze();
- localMap.set(map);
- }
-
- @Override
- public String get(final String key) {
- return (String) getValue(key);
- }
-
- @Override
- public V getValue(final String key) {
- final StringMap map = localMap.get();
- return map == null ? null : map.getValue(key);
- }
-
- @Override
- public void remove(final String key) {
- final StringMap map = localMap.get();
- if (map != null) {
- final StringMap copy = createStringMap(map);
- copy.remove(key);
- copy.freeze();
- localMap.set(copy);
- }
- }
-
- @Override
- public void removeAll(final Iterable keys) {
- final StringMap map = localMap.get();
- if (map != null) {
- final StringMap copy = createStringMap(map);
- for (final String key : keys) {
- copy.remove(key);
- }
- copy.freeze();
- localMap.set(copy);
- }
- }
-
- @Override
- public void clear() {
- localMap.remove();
- }
-
- @Override
- public boolean containsKey(final String key) {
- final StringMap map = localMap.get();
- return map != null && map.containsKey(key);
- }
-
- @Override
- public Map getCopy() {
- final StringMap map = localMap.get();
- return map == null ? new HashMap<>() : map.toMap();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public StringMap getReadOnlyContextData() {
- final StringMap map = localMap.get();
- return map == null ? EMPTY_CONTEXT_DATA : map;
- }
-
- @Override
- public Map getImmutableMapOrNull() {
- final StringMap map = localMap.get();
- return map == null ? null : Collections.unmodifiableMap(map.toMap());
- }
-
- @Override
- public boolean isEmpty() {
- final StringMap map = localMap.get();
- return map == null || map.isEmpty();
- }
-
- @Override
- public String toString() {
- final StringMap map = localMap.get();
- return map == null ? "{}" : map.toString();
- }
-
- @Override
- public int hashCode() {
- final int prime = 31;
- int result = 1;
- final StringMap map = this.localMap.get();
- result = prime * result + ((map == null) ? 0 : map.hashCode());
- return result;
- }
-
- @Override
- public boolean equals(final Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null) {
- return false;
- }
- if (!(obj instanceof ThreadContextMap)) {
- return false;
- }
- final ThreadContextMap other = (ThreadContextMap) obj;
- final Map map = this.getImmutableMapOrNull();
- final Map otherMap = other.getImmutableMapOrNull();
- return Objects.equals(map, otherMap);
- }
-}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java
index a07992d371f..61495a7172f 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java
@@ -24,6 +24,8 @@
import org.apache.logging.log4j.util.PropertiesUtil;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.apache.logging.log4j.util.TriConsumer;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
/**
* The actual ThreadContext Map. A new ThreadContext Map is created each time it is updated and the Map stored is always
@@ -31,6 +33,7 @@
* expected that the Map will be passed to many more log events than the number of keys it contains the performance
* should be much better than if the Map was copied for each event.
*/
+@NullMarked
public class DefaultThreadContextMap implements ThreadContextMap, ReadOnlyStringMap {
private static final long serialVersionUID = 8218007901108944053L;
@@ -40,8 +43,13 @@ public class DefaultThreadContextMap implements ThreadContextMap, ReadOnlyString
*/
public static final String INHERITABLE_MAP = "isThreadContextMapInheritable";
+ /**
+ * @since 2.24.0
+ */
+ public static final ThreadContextMap INSTANCE = new DefaultThreadContextMap();
+
private final boolean useMap;
- private final ThreadLocal
*/
+@NullMarked
public class Provider {
/**
* Constant inlined by the compiler
@@ -72,75 +73,27 @@ public class Provider {
*/
public static final String PROVIDER_PROPERTY_NAME = "log4j.provider";
- /**
- * Constant used to disable the {@link ThreadContextMap}.
- *
- * Warning: the value of this constant does not point to a concrete class name.
- *
- * @see #getThreadContextMap
- */
- protected static final String NO_OP_CONTEXT_MAP = "NoOp";
-
- /**
- * Constant used to select a web application-safe implementation of {@link ThreadContextMap}.
- *
- * This implementation only binds JRE classes to {@link ThreadLocal} variables.
- *
- *
- * Warning: the value of this constant does not point to a concrete class name.
- *
- * @see #getThreadContextMap
- */
- protected static final String WEB_APP_CONTEXT_MAP = "WebApp";
-
- /**
- * Constant used to select a copy-on-write implementation of {@link ThreadContextMap}.
- *
- * Warning: the value of this constant does not point to a concrete class name.
- *
- * @see #getThreadContextMap
- */
- protected static final String COPY_ON_WRITE_CONTEXT_MAP = "CopyOnWrite";
-
- /**
- * Constant used to select a garbage-free implementation of {@link ThreadContextMap}.
- *
- * This implementation must ensure that common operations don't create new object instances. The drawback is
- * the necessity to bind custom classes to {@link ThreadLocal} variables.
- *
- *
- * Warning: the value of this constant does not point to a concrete class name.
- *
- * @see #getThreadContextMap
- */
- protected static final String GARBAGE_FREE_CONTEXT_MAP = "GarbageFree";
-
- // Property keys relevant for context map selection
private static final String DISABLE_CONTEXT_MAP = "log4j2.disableThreadContextMap";
private static final String DISABLE_THREAD_CONTEXT = "log4j2.disableThreadContext";
- private static final String THREAD_CONTEXT_MAP_PROPERTY = "log4j2.threadContextMap";
- private static final String GC_FREE_THREAD_CONTEXT_PROPERTY = "log4j2.garbagefree.threadContextMap";
- private static final Integer DEFAULT_PRIORITY = -1;
+ private static final int DEFAULT_PRIORITY = -1;
private static final Logger LOGGER = StatusLogger.getLogger();
- private final Integer priority;
+ private final int priority;
// LoggerContextFactory
@Deprecated
- private final String className;
+ private final @Nullable String className;
- private final Class extends LoggerContextFactory> loggerContextFactoryClass;
- private final Lazy loggerContextFactoryLazy = Lazy.lazy(this::createLoggerContextFactory);
+ private final @Nullable Class extends LoggerContextFactory> loggerContextFactoryClass;
// ThreadContextMap
@Deprecated
- private final String threadContextMap;
+ private final @Nullable String threadContextMap;
- private final Class extends ThreadContextMap> threadContextMapClass;
- private final Lazy threadContextMapLazy = Lazy.lazy(this::createThreadContextMap);
- private final String versions;
+ private final @Nullable Class extends ThreadContextMap> threadContextMapClass;
+ private final @Nullable String versions;
@Deprecated
- private final URL url;
+ private final @Nullable URL url;
@Deprecated
private final WeakReference classLoader;
@@ -154,7 +107,7 @@ public Provider(final Properties props, final URL url, final ClassLoader classLo
this.url = url;
this.classLoader = new WeakReference<>(classLoader);
final String weight = props.getProperty(FACTORY_PRIORITY);
- priority = weight == null ? DEFAULT_PRIORITY : Integer.valueOf(weight);
+ priority = weight == null ? DEFAULT_PRIORITY : Integer.parseInt(weight);
className = props.getProperty(LOGGER_CONTEXT_FACTORY);
threadContextMap = props.getProperty(THREAD_CONTEXT_MAP);
loggerContextFactoryClass = null;
@@ -167,7 +120,7 @@ public Provider(final Properties props, final URL url, final ClassLoader classLo
* @param versions Minimal API version required, should be set to {@link #CURRENT_VERSION}.
* @since 2.24.0
*/
- public Provider(final Integer priority, final String versions) {
+ public Provider(final @Nullable Integer priority, final String versions) {
this(priority, versions, null, null);
}
@@ -175,12 +128,12 @@ public Provider(final Integer priority, final String versions) {
* @param priority A positive number specifying the provider's priority or {@code null} if default,
* @param versions Minimal API version required, should be set to {@link #CURRENT_VERSION},
* @param loggerContextFactoryClass A public exported implementation of {@link LoggerContextFactory} or {@code
- * null} if {@link #createLoggerContextFactory()} is also implemented.
+ * null} if {@link #getLoggerContextFactory()} is also implemented.
*/
public Provider(
- final Integer priority,
+ final @Nullable Integer priority,
final String versions,
- final Class extends LoggerContextFactory> loggerContextFactoryClass) {
+ final @Nullable Class extends LoggerContextFactory> loggerContextFactoryClass) {
this(priority, versions, loggerContextFactoryClass, null);
}
@@ -188,15 +141,15 @@ public Provider(
* @param priority A positive number specifying the provider's priority or {@code null} if default,
* @param versions Minimal API version required, should be set to {@link #CURRENT_VERSION},
* @param loggerContextFactoryClass A public exported implementation of {@link LoggerContextFactory} or {@code
- * null} if {@link #createLoggerContextFactory()} is also implemented,
+ * null} if {@link #getLoggerContextFactory()} is also implemented,
* @param threadContextMapClass A public exported implementation of {@link ThreadContextMap} or {@code null} if
- * {@link #createThreadContextMap()} is implemented.
+ * {@link #getThreadContextMapInstance()} is implemented.
*/
public Provider(
- final Integer priority,
+ final @Nullable Integer priority,
final String versions,
- final Class extends LoggerContextFactory> loggerContextFactoryClass,
- final Class extends ThreadContextMap> threadContextMapClass) {
+ final @Nullable Class extends LoggerContextFactory> loggerContextFactoryClass,
+ final @Nullable Class extends ThreadContextMap> threadContextMapClass) {
this.priority = priority != null ? priority : DEFAULT_PRIORITY;
this.versions = versions;
this.loggerContextFactoryClass = loggerContextFactoryClass;
@@ -213,7 +166,7 @@ public Provider(
* @return A String containing the Log4j versions supported.
*/
public String getVersions() {
- return versions;
+ return versions != null ? versions : "";
}
/**
@@ -233,7 +186,7 @@ public Integer getPriority() {
* @return the class name of a LoggerContextFactory implementation or {@code null} if unspecified.
* @see #loadLoggerContextFactory()
*/
- public String getClassName() {
+ public @Nullable String getClassName() {
return loggerContextFactoryClass != null ? loggerContextFactoryClass.getName() : className;
}
@@ -241,9 +194,8 @@ public String getClassName() {
* Loads the {@link LoggerContextFactory} class specified by this Provider.
*
* @return the LoggerContextFactory implementation class or {@code null} if unspecified or a loader error occurred.
- * @see #createLoggerContextFactory()
*/
- public Class extends LoggerContextFactory> loadLoggerContextFactory() {
+ public @Nullable Class extends LoggerContextFactory> loadLoggerContextFactory() {
if (loggerContextFactoryClass != null) {
return loggerContextFactoryClass;
}
@@ -271,65 +223,30 @@ public Class extends LoggerContextFactory> loadLoggerContextFactory() {
return null;
}
- private LoggerContextFactory createLoggerContextFactory() {
- final Class extends LoggerContextFactory> factoryClass = loadLoggerContextFactory();
- if (factoryClass != null) {
- try {
- return LoaderUtil.newInstanceOf(factoryClass);
- } catch (final Exception e) {
- LOGGER.error(
- "Unable to create instance of class {} specified in {}", factoryClass.getName(), getUrl(), e);
- }
- }
- LOGGER.warn("Falling back to {}", SimpleLoggerContextFactory.INSTANCE);
- return SimpleLoggerContextFactory.INSTANCE;
- }
-
/**
* @return The logger context factory to be used by {@link org.apache.logging.log4j.LogManager}.
* @since 2.24.0
*/
public LoggerContextFactory getLoggerContextFactory() {
- return loggerContextFactoryLazy.get();
+ final Class> implementation = loadLoggerContextFactory();
+ if (implementation != null) {
+ try {
+ return LoaderUtil.newInstanceOf(implementation.asSubclass(LoggerContextFactory.class));
+ } catch (final ReflectiveOperationException e) {
+ LOGGER.error("Failed to instantiate logger context factory {}.", implementation.getName(), e);
+ }
+ }
+ LOGGER.error("Falling back to simple logger context factory: {}", SimpleLoggerContextFactory.class.getName());
+ return SimpleLoggerContextFactory.INSTANCE;
}
/**
- * Gets the class name of the {@link ThreadContextMap} implementation of this Provider.
- *
- * This method should return one of the internal implementations:
- *
- *
{@code null} if {@link #loadThreadContextMap} is implemented,
- *
{@link #NO_OP_CONTEXT_MAP},
- *
{@link #WEB_APP_CONTEXT_MAP},
- *
{@link #COPY_ON_WRITE_CONTEXT_MAP},
- *
{@link #GARBAGE_FREE_CONTEXT_MAP}.
- *
- *
+ * Gets the class name of the {@link org.apache.logging.log4j.spi.ThreadContextMap} implementation of this Provider.
+ *
* @return the class name of a ThreadContextMap implementation
- * @see #loadThreadContextMap()
*/
- public String getThreadContextMap() {
- if (threadContextMapClass != null) {
- return threadContextMapClass.getName();
- }
- // Field value
- if (threadContextMap != null) {
- return threadContextMap;
- }
- // Properties
- final PropertiesUtil props = PropertiesUtil.getProperties();
- if (props.getBooleanProperty(DISABLE_CONTEXT_MAP) || props.getBooleanProperty(DISABLE_THREAD_CONTEXT)) {
- return NO_OP_CONTEXT_MAP;
- }
- final String threadContextMapClass = props.getStringProperty(THREAD_CONTEXT_MAP_PROPERTY);
- if (threadContextMapClass != null) {
- return threadContextMapClass;
- }
- // Default based on properties
- if (props.getBooleanProperty(GC_FREE_THREAD_CONTEXT_PROPERTY)) {
- return GARBAGE_FREE_CONTEXT_MAP;
- }
- return Constants.ENABLE_THREADLOCALS ? COPY_ON_WRITE_CONTEXT_MAP : WEB_APP_CONTEXT_MAP;
+ public @Nullable String getThreadContextMap() {
+ return threadContextMapClass != null ? threadContextMapClass.getName() : threadContextMap;
}
/**
@@ -337,17 +254,18 @@ public String getThreadContextMap() {
*
* @return the {@code ThreadContextMap} implementation class or {@code null} if unspecified or a loading error
* occurred.
- * @see #createThreadContextMap()
*/
- public Class extends ThreadContextMap> loadThreadContextMap() {
+ public @Nullable Class extends ThreadContextMap> loadThreadContextMap() {
if (threadContextMapClass != null) {
return threadContextMapClass;
}
- final String threadContextMap = getThreadContextMap();
+ if (threadContextMap == null) {
+ return null;
+ }
final ClassLoader loader = classLoader.get();
// Support for deprecated {@code META-INF/log4j-provider.properties} format.
// In the remaining cases {@code loader == null}.
- if (loader == null || threadContextMap == null) {
+ if (loader == null) {
return null;
}
try {
@@ -358,78 +276,24 @@ public Class extends ThreadContextMap> loadThreadContextMap() {
LOGGER.error(
"Class {} specified in {} does not extend {}",
threadContextMap,
- getUrl(),
+ url,
ThreadContextMap.class.getName());
}
} catch (final Exception e) {
- LOGGER.error("Unable to load class {} specified in {}", threadContextMap, url.toString(), e);
+ LOGGER.error("Unable to load class {} specified in {}", threadContextMap, url, e);
}
return null;
}
- /**
- * Creates a {@link ThreadContextMap} using the legacy {@link #loadThreadContextMap()} and
- * {@link #getThreadContextMap()} methods:
- *
- *
calls {@link #loadThreadContextMap},
- *
if the previous call returns {@code null}, it calls {@link #getThreadContextMap} to instantiate one of
- * the internal implementations,
- *
it returns a no-op map otherwise.
- *
- */
- @SuppressWarnings("deprecation")
- ThreadContextMap createThreadContextMap() {
- final Class extends ThreadContextMap> threadContextMapClass = loadThreadContextMap();
- if (threadContextMapClass != null) {
- try {
- return LoaderUtil.newInstanceOf(threadContextMapClass);
- } catch (final Exception e) {
- LOGGER.error(
- "Unable to create instance of class {} specified in {}",
- threadContextMapClass.getName(),
- getUrl(),
- e);
- }
- }
- // Standard Log4j API implementations are internal and can be only specified by name:
- final String threadContextMap = getThreadContextMap();
- if (threadContextMap != null) {
- /*
- * The constructors are called explicitly to improve GraalVM support.
- *
- * The class names of the package-private implementations from version 2.23.1 must be recognized even
- * if the class is moved.
- */
- switch (threadContextMap) {
- case NO_OP_CONTEXT_MAP:
- case "org.apache.logging.log4j.spi.NoOpThreadContextMap":
- return new NoOpThreadContextMap();
- case WEB_APP_CONTEXT_MAP:
- case "org.apache.logging.log4j.spi.DefaultThreadContextMap":
- return new DefaultThreadContextMap();
- case GARBAGE_FREE_CONTEXT_MAP:
- case "org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap":
- return new GarbageFreeSortedArrayThreadContextMap();
- case COPY_ON_WRITE_CONTEXT_MAP:
- case "org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap":
- return new CopyOnWriteSortedArrayThreadContextMap();
- }
- }
- LOGGER.warn("Falling back to {}", NoOpThreadContextMap.class.getName());
- return new NoOpThreadContextMap();
- }
-
- // Used for testing
- void resetThreadContextMap() {
- threadContextMapLazy.set(null);
- }
-
/**
* @return The thread context map to be used by {@link org.apache.logging.log4j.ThreadContext}.
* @since 2.24.0
*/
public ThreadContextMap getThreadContextMapInstance() {
- return threadContextMapLazy.get();
+ final PropertiesUtil props = PropertiesUtil.getProperties();
+ return props.getBooleanProperty(DISABLE_CONTEXT_MAP) || props.getBooleanProperty(DISABLE_THREAD_CONTEXT)
+ ? NoOpThreadContextMap.INSTANCE
+ : DefaultThreadContextMap.INSTANCE;
}
/**
@@ -447,7 +311,7 @@ public ScopedContextProvider getScopedContextProvider() {
* @deprecated since 2.24.0, without replacement.
*/
@Deprecated
- public URL getUrl() {
+ public @Nullable URL getUrl() {
return url;
}
@@ -455,7 +319,7 @@ public URL getUrl() {
public String toString() {
final StringBuilder result =
new StringBuilder("Provider '").append(getClass().getName()).append("'");
- if (!DEFAULT_PRIORITY.equals(priority)) {
+ if (priority != DEFAULT_PRIORITY) {
result.append("\n\tpriority = ").append(priority);
}
final String threadContextMap = getThreadContextMap();
@@ -485,24 +349,18 @@ public boolean equals(final Object o) {
if (this == o) {
return true;
}
- if (!(o instanceof Provider)) {
- return false;
+ if (o instanceof Provider) {
+ final Provider provider = (Provider) o;
+ return Objects.equals(priority, provider.priority)
+ && Objects.equals(className, provider.className)
+ && Objects.equals(loggerContextFactoryClass, provider.loggerContextFactoryClass)
+ && Objects.equals(versions, provider.versions);
}
-
- final Provider provider = (Provider) o;
-
- return Objects.equals(priority, provider.priority)
- && Objects.equals(className, provider.className)
- && Objects.equals(loggerContextFactoryClass, provider.loggerContextFactoryClass)
- && Objects.equals(versions, provider.versions);
+ return false;
}
@Override
public int hashCode() {
- int result = priority != null ? priority.hashCode() : 0;
- result = 31 * result + (className != null ? className.hashCode() : 0);
- result = 31 * result + (loggerContextFactoryClass != null ? loggerContextFactoryClass.hashCode() : 0);
- result = 31 * result + (versions != null ? versions.hashCode() : 0);
- return result;
+ return Objects.hash(priority, className, loggerContextFactoryClass, versions);
}
}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap.java
index f0af90ddbb2..26290da51d7 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap.java
@@ -18,6 +18,9 @@
import java.util.Map;
import org.apache.logging.log4j.ThreadContext;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+import org.osgi.annotation.versioning.ProviderType;
/**
* Service provider interface to implement custom MDC behavior for {@link org.apache.logging.log4j.ThreadContext}.
@@ -26,6 +29,8 @@
* are accessible to applications via the {@link ThreadContext#getThreadContextMap()} method.
*
*/
+@ProviderType
+@NullMarked
public interface ThreadContextMap {
/**
@@ -35,18 +40,20 @@ public interface ThreadContextMap {
/**
* Determines if the key is in the context.
- * @param key The key to locate.
+ * @param key The key to locate, not {@code null}.
* @return True if the key is in the context, false otherwise.
*/
boolean containsKey(final String key);
/**
- * Gets the context identified by the key parameter.
- *
- *
This method has no side effects.
- * @param key The key to locate.
- * @return The value associated with the key or null.
+ * Gets the {@link String} value for the specified key if it exists or {@code null}.
+ *
+ * This method has no side effects.
+ *
+ * @param key The key to locate, not {@code null}.
+ * @return The value associated with the key or {@code null}.
*/
+ @Nullable
String get(final String key);
/**
@@ -59,6 +66,7 @@ public interface ThreadContextMap {
* Returns an immutable view on the context Map or {@code null} if the context map is empty.
* @return an immutable context Map or {@code null}.
*/
+ @Nullable
Map getImmutableMapOrNull();
/**
@@ -68,21 +76,43 @@ public interface ThreadContextMap {
boolean isEmpty();
/**
- * Puts a context value (the o parameter) as identified
- * with the key parameter into the current thread's
- * context map.
- *
- *
If the current thread does not have a context map it is
- * created as a side effect.
- * @param key The key name.
- * @param value The key value.
+ * Sets the context value for the given key in the current thread's context map.
+ *
+ * If the current thread does not have a context map it is created as a side effect.
+ *
+ * @param key The key to add, not {@code null}.
+ * @param value The value to add, not {@code null}.
*/
void put(final String key, final String value);
/**
- * Removes the context identified by the key
- * parameter.
- * @param key The key to remove.
+ * Puts all given context map entries into the current thread's context map.
+ *
+ * If the current thread does not have a context map it is created as a side effect.
+ *
+ * @param map The map to add, not {@code null}.
+ * @since 2.24.0
+ */
+ default void putAll(final Map map) {
+ map.forEach(this::put);
+ }
+
+ /**
+ * Removes the context entry corresponding to the given key.
+ * @param key The key to remove, not {@code null}.
*/
void remove(final String key);
+
+ /**
+ * Removes all given context map keys from the current thread's context map.
+ *
+ * If the current thread does not have a context map it is created as a side effect.
+ *
+ *
+ * @param keys The keys, {@code null}.
+ * @since 2.24.0
+ */
+ default void removeAll(final Iterable keys) {
+ keys.forEach(this::remove);
+ }
}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java
index b00bd979858..418beb4583c 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java
@@ -48,12 +48,12 @@ public final class ThreadContextMapFactory {
* and when Log4j is reconfigured.
*/
public static void init() {
- ProviderUtil.getProvider().resetThreadContextMap();
+ ProviderUtil.getProvider().getThreadContextMapInstance();
}
private ThreadContextMapFactory() {}
public static ThreadContextMap createThreadContextMap() {
- return ProviderUtil.getProvider().createThreadContextMap();
+ return ProviderUtil.getProvider().getThreadContextMapInstance();
}
}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/ThreadContextTestAccess.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/ThreadContextTestAccess.java
deleted file mode 100644
index ecf3e9f6c69..00000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/ThreadContextTestAccess.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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;
-
-/**
- *
- * Utility class to access package protected methods in {@code ThreadContext}.
- *
- *
- * @see ThreadContext
- * @since 2.7
- */
-public final class ThreadContextTestAccess {
- private ThreadContextTestAccess() { // prevent instantiation
- }
-
- public static void init() {
- ThreadContext.init();
- }
-}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AbstractAsyncThreadContextTestBase.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AbstractAsyncThreadContextTestBase.java
index a98bce9fdc0..29ddda9717b 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AbstractAsyncThreadContextTestBase.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AbstractAsyncThreadContextTestBase.java
@@ -29,15 +29,16 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
-import org.apache.logging.log4j.ThreadContextTestAccess;
import org.apache.logging.log4j.core.config.ConfigurationFactory;
import org.apache.logging.log4j.core.config.LoggerConfig;
+import org.apache.logging.log4j.core.context.CopyOnWriteSortedArrayThreadContextMap;
+import org.apache.logging.log4j.core.context.GarbageFreeSortedArrayThreadContextMap;
+import org.apache.logging.log4j.core.context.StringArrayThreadContextMap;
import org.apache.logging.log4j.core.impl.Log4jContextFactory;
import org.apache.logging.log4j.core.jmx.RingBufferAdmin;
import org.apache.logging.log4j.core.selector.ClassLoaderContextSelector;
import org.apache.logging.log4j.core.selector.ContextSelector;
import org.apache.logging.log4j.core.test.CoreLoggerContexts;
-import org.apache.logging.log4j.spi.DefaultThreadContextMap;
import org.apache.logging.log4j.spi.LoggerContext;
import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
import org.apache.logging.log4j.test.TestProperties;
@@ -89,26 +90,24 @@ void initConfigFile() {
}
protected enum ContextImpl {
- WEBAPP,
- GARBAGE_FREE,
- COPY_ON_WRITE;
+ WEBAPP("WebApp", StringArrayThreadContextMap.class),
+ GARBAGE_FREE("GarbageFree", GarbageFreeSortedArrayThreadContextMap.class),
+ COPY_ON_WRITE("CopyOnWrite", CopyOnWriteSortedArrayThreadContextMap.class);
+
+ private final String threadContextMap;
+ private final Class> implClass;
+
+ ContextImpl(final String threadContextMap, final Class> implClass) {
+ this.threadContextMap = threadContextMap;
+ this.implClass = implClass;
+ }
void init() {
- final String PACKAGE = "org.apache.logging.log4j.spi.";
- props.setProperty("log4j2.threadContextMap", PACKAGE + implClassSimpleName());
- ThreadContextTestAccess.init();
+ props.setProperty("log4j2.threadContextMap", threadContextMap);
}
- public String implClassSimpleName() {
- switch (this) {
- case WEBAPP:
- return DefaultThreadContextMap.class.getSimpleName();
- case GARBAGE_FREE:
- return "GarbageFreeSortedArrayThreadContextMap";
- case COPY_ON_WRITE:
- return "CopyOnWriteSortedArrayThreadContextMap";
- }
- throw new IllegalStateException("Unknown state " + this);
+ public String getImplClassSimpleName() {
+ return implClass.getSimpleName();
}
}
@@ -123,8 +122,8 @@ private void init(final ContextImpl contextImpl, final Mode asyncMode) {
} else {
assertThat(ThreadContext.getThreadContextMap())
.isNotNull()
- .extracting(o -> o.getClass().getSimpleName())
- .isEqualTo(contextImpl.implClassSimpleName());
+ .extracting(Object::getClass)
+ .isEqualTo(contextImpl.implClass);
}
}
@@ -197,25 +196,25 @@ protected void testAsyncLogWritesToLog(final ContextImpl contextImpl, final Mode
private static String contextMap() {
final ReadOnlyThreadContextMap impl = ThreadContext.getThreadContextMap();
return impl == null
- ? ContextImpl.WEBAPP.implClassSimpleName()
+ ? ContextImpl.WEBAPP.getImplClassSimpleName()
: impl.getClass().getSimpleName();
}
private void checkResult(final Path file, final String loggerContextName, final ContextImpl contextImpl)
throws IOException {
- final String contextDesc = contextImpl + " " + contextImpl.implClassSimpleName() + " " + loggerContextName;
+ final String contextDesc = contextImpl + " " + contextImpl.getImplClassSimpleName() + " " + loggerContextName;
try (final BufferedReader reader = Files.newBufferedReader(file)) {
String expect;
for (int i = 0; i < LINE_COUNT; i++) {
final String line = reader.readLine();
if ((i & 1) == 1) {
- expect =
- "INFO c.f.Bar mapvalue [stackvalue] {KEY=mapvalue, configProp=configValue, configProp2=configValue2, count="
- + i + "} " + contextDesc + " i=" + i;
+ expect = "INFO c.f.Bar mapvalue [stackvalue] {KEY=mapvalue, configProp=configValue,"
+ + " configProp2=configValue2, count="
+ + i + "} " + contextDesc + " i=" + i;
} else {
- expect =
- "INFO c.f.Bar mapvalue [stackvalue] {KEY=mapvalue, configProp=configValue, configProp2=configValue2} "
- + contextDesc + " i=" + i;
+ expect = "INFO c.f.Bar mapvalue [stackvalue] {KEY=mapvalue, configProp=configValue,"
+ + " configProp2=configValue2} "
+ + contextDesc + " i=" + i;
}
assertThat(line).as("Log file '%s'", file.getFileName()).isEqualTo(expect);
}
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMapTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/StringArrayThreadContextMapTest.java
similarity index 92%
rename from log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMapTest.java
rename to log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/StringArrayThreadContextMapTest.java
index 2f845d8bf73..d71186ff14b 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMapTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/StringArrayThreadContextMapTest.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.logging.log4j.internal.map;
+package org.apache.logging.log4j.core.context;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -26,6 +26,7 @@
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
+import org.apache.logging.log4j.core.context.internal.UnmodifiableArrayBackedMap;
import org.apache.logging.log4j.test.junit.UsingThreadContextMap;
import org.apache.logging.log4j.util.TriConsumer;
import org.junit.jupiter.api.Test;
@@ -36,23 +37,6 @@
@UsingThreadContextMap
public class StringArrayThreadContextMapTest {
- @Test
- public void testEqualsVsSameKind() {
- final StringArrayThreadContextMap map1 = createMap();
- final StringArrayThreadContextMap map2 = createMap();
- assertEquals(map1, map1);
- assertEquals(map2, map2);
- assertEquals(map1, map2);
- assertEquals(map2, map1);
- }
-
- @Test
- public void testHashCodeVsSameKind() {
- final StringArrayThreadContextMap map1 = createMap();
- final StringArrayThreadContextMap map2 = createMap();
- assertEquals(map1.hashCode(), map2.hashCode());
- }
-
@Test
public void testGet() {
final StringArrayThreadContextMap map1 = createMap();
@@ -99,7 +83,7 @@ public void testPutAll() {
/**
* Test method for
- * {@link org.apache.logging.log4j.internal.map.StringArrayThreadContextMap#remove(java.lang.String)}
+ * {@link StringArrayThreadContextMap#remove(java.lang.String)}
* .
*/
@Test
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextMapTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/ThreadContextMapTest.java
similarity index 60%
rename from log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextMapTest.java
rename to log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/ThreadContextMapTest.java
index f394e71bea0..7f8e0fa5f99 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextMapTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/ThreadContextMapTest.java
@@ -14,26 +14,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.logging.log4j.spi;
+package org.apache.logging.log4j.core.context;
-import static org.assertj.core.api.Assertions.assertThat;
-
-import java.time.Duration;
import java.util.Properties;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
import java.util.stream.Stream;
+import org.apache.logging.log4j.spi.ThreadContextMap;
+import org.apache.logging.log4j.test.spi.AbstractThreadContextMapTest;
import org.apache.logging.log4j.util.PropertiesUtil;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
-class ThreadContextMapTest {
-
- private static final String KEY = "key";
+class ThreadContextMapTest extends AbstractThreadContextMapTest {
static Stream defaultMaps() {
return Stream.of(
- new DefaultThreadContextMap(),
+ new StringArrayThreadContextMap(),
new CopyOnWriteSortedArrayThreadContextMap(),
new GarbageFreeSortedArrayThreadContextMap());
}
@@ -43,34 +38,18 @@ static Stream inheritableMaps() {
props.setProperty("log4j2.isThreadContextMapInheritable", "true");
final PropertiesUtil util = new PropertiesUtil(props);
return Stream.of(
- new DefaultThreadContextMap(true, util),
- new CopyOnWriteSortedArrayThreadContextMap(util),
- new GarbageFreeSortedArrayThreadContextMap(util));
+ new CopyOnWriteSortedArrayThreadContextMap(util), new GarbageFreeSortedArrayThreadContextMap(util));
}
@ParameterizedTest
@MethodSource("defaultMaps")
void threadLocalNotInheritableByDefault(final ThreadContextMap contextMap) {
- contextMap.put(KEY, "threadLocalNotInheritableByDefault");
- verifyThreadContextValueFromANewThread(contextMap, null);
+ assertThreadLocalNotInheritable(contextMap);
}
@ParameterizedTest
@MethodSource("inheritableMaps")
void threadLocalInheritableIfConfigured(final ThreadContextMap contextMap) {
- contextMap.put(KEY, "threadLocalInheritableIfConfigured");
- verifyThreadContextValueFromANewThread(contextMap, "threadLocalInheritableIfConfigured");
- }
-
- private static void verifyThreadContextValueFromANewThread(
- final ThreadContextMap contextMap, final String expected) {
- final ExecutorService executorService = Executors.newSingleThreadExecutor();
- try {
- assertThat(executorService.submit(() -> contextMap.get(KEY)))
- .succeedsWithin(Duration.ofSeconds(1))
- .isEqualTo(expected);
- } finally {
- executorService.shutdown();
- }
+ assertThreadLocalInheritable(contextMap);
}
}
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMapTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/internal/UnmodifiableArrayBackedMapTest.java
similarity index 99%
rename from log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMapTest.java
rename to log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/internal/UnmodifiableArrayBackedMapTest.java
index 9652034d70f..9362311306e 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMapTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/internal/UnmodifiableArrayBackedMapTest.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.logging.log4j.internal.map;
+package org.apache.logging.log4j.core.context.internal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
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 94654608239..efa723bad0c 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
@@ -18,7 +18,6 @@
import static java.util.Arrays.asList;
import static java.util.concurrent.Executors.newSingleThreadExecutor;
-import static org.apache.logging.log4j.ThreadContext.getThreadContextMap;
import static org.apache.logging.log4j.core.impl.ContextDataInjectorFactory.createInjector;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
@@ -33,9 +32,9 @@
import java.util.concurrent.ExecutionException;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.ContextDataInjector;
-import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
-import org.apache.logging.log4j.test.ThreadContextUtilityClass;
+import org.apache.logging.log4j.spi.ThreadContextMap;
import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.ProviderUtil;
import org.apache.logging.log4j.util.SortedArrayStringMap;
import org.apache.logging.log4j.util.StringMap;
import org.junit.After;
@@ -51,26 +50,24 @@ public class ThreadContextDataInjectorTest {
@Parameters(name = "{0}")
public static Collection threadContextMapClassNames() {
return asList(new String[][] {
- {
- "org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap",
- "org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap"
- },
- {
- "org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap",
- "org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap"
- },
- {"org.apache.logging.log4j.spi.DefaultThreadContextMap", null}
+ {"org.apache.logging.log4j.core.context.CopyOnWriteSortedArrayThreadContextMap"},
+ {"org.apache.logging.log4j.core.context.GarbageFreeSortedArrayThreadContextMap"},
+ // {"org.apache.logging.log4j.core.context.StringArrayThreadContextMap"}
});
}
@Parameter
public String threadContextMapClassName;
- @Parameter(value = 1)
- public String readOnlythreadContextMapClassName;
+ private static void resetThreadContextMap() {
+ PropertiesUtil.getProperties().reload();
+ final Log4jProvider provider = (Log4jProvider) ProviderUtil.getProvider();
+ provider.resetThreadContextMap();
+ ThreadContext.init();
+ }
@Before
- public void before() {
+ public void before() throws ReflectiveOperationException {
System.setProperty("log4j2.threadContextMap", threadContextMapClassName);
}
@@ -83,13 +80,11 @@ public void after() {
}
private void testContextDataInjector() {
- final ReadOnlyThreadContextMap readOnlythreadContextMap = getThreadContextMap();
+ final ThreadContextMap threadContextMap = ProviderUtil.getProvider().getThreadContextMapInstance();
assertThat(
"thread context map class name",
- (readOnlythreadContextMap == null)
- ? null
- : readOnlythreadContextMap.getClass().getName(),
- is(equalTo(readOnlythreadContextMapClassName)));
+ threadContextMap.getClass().getName(),
+ is(equalTo(threadContextMapClassName)));
final ContextDataInjector contextDataInjector = createInjector(true);
final StringMap stringMap = contextDataInjector.injectContextData(null, new SortedArrayStringMap());
@@ -122,9 +117,8 @@ private void testContextDataInjector() {
private void prepareThreadContext(final boolean isThreadContextMapInheritable) {
System.setProperty("log4j2.isThreadContextMapInheritable", Boolean.toString(isThreadContextMapInheritable));
- PropertiesUtil.getProperties().reload();
- ThreadContextUtilityClass.reset();
- ThreadContext.remove("baz");
+ resetThreadContextMap();
+ ThreadContext.clearMap();
ThreadContext.put("foo", "bar");
}
diff --git a/log4j-core/pom.xml b/log4j-core/pom.xml
index 05a6de655d0..1d376c994bc 100644
--- a/log4j-core/pom.xml
+++ b/log4j-core/pom.xml
@@ -53,6 +53,8 @@
-->
true
+
+ org.jspecify.*;resolution:=optional,
com.conversantmedia.util.concurrent;resolution:=optional;
com.fasterxml.jackson.*;resolution:=optional,
@@ -122,6 +124,11 @@
providedtrue
+
+ org.jspecify
+ jspecify
+ provided
+ org.osgi
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/GarbageFreeSortedArrayThreadContextMap.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/context/AbstractSortedArrayThreadContextMap.java
similarity index 59%
rename from log4j-api/src/main/java/org/apache/logging/log4j/spi/GarbageFreeSortedArrayThreadContextMap.java
rename to log4j-core/src/main/java/org/apache/logging/log4j/core/context/AbstractSortedArrayThreadContextMap.java
index 1622a0c455b..508ce701f7e 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/GarbageFreeSortedArrayThreadContextMap.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/context/AbstractSortedArrayThreadContextMap.java
@@ -14,27 +14,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.logging.log4j.spi;
+package org.apache.logging.log4j.core.context;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
+import org.apache.logging.log4j.spi.ObjectThreadContextMap;
+import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
+import org.apache.logging.log4j.spi.ThreadContextMap2;
import org.apache.logging.log4j.util.PropertiesUtil;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.apache.logging.log4j.util.SortedArrayStringMap;
import org.apache.logging.log4j.util.StringMap;
+import org.jspecify.annotations.Nullable;
/**
- * {@code SortedArrayStringMap}-based implementation of the {@code ThreadContextMap} interface that attempts not to
- * create temporary objects. Adding and removing key-value pairs will not create temporary objects.
- *
- * This implementation does not make a copy of its contents on every operation, so this data structure cannot
- * be passed to log events. Instead, client code needs to copy the contents when interacting with another thread.
- *
- * @since 2.7
+ * Commons base class for {@link StringMap}-based implementations of {@code ThreadContextMap}.
*/
-class GarbageFreeSortedArrayThreadContextMap implements ReadOnlyThreadContextMap, ObjectThreadContextMap {
+public abstract class AbstractSortedArrayThreadContextMap implements ReadOnlyThreadContextMap, ObjectThreadContextMap {
/**
* Property name ({@value} ) for selecting {@code InheritableThreadLocal} (value "true") or plain
@@ -52,25 +50,35 @@ class GarbageFreeSortedArrayThreadContextMap implements ReadOnlyThreadContextMap
*/
protected static final String PROPERTY_NAME_INITIAL_CAPACITY = "log4j2.ThreadContext.initial.capacity";
- private final int initialCapacity;
- protected final ThreadLocal localMap;
+ private static final StringMap EMPTY_CONTEXT_DATA = new SortedArrayStringMap(1);
- public GarbageFreeSortedArrayThreadContextMap() {
- this(PropertiesUtil.getProperties());
+ static {
+ EMPTY_CONTEXT_DATA.freeze();
}
- GarbageFreeSortedArrayThreadContextMap(final PropertiesUtil properties) {
+ private final int initialCapacity;
+ protected final ThreadLocal<@Nullable StringMap> localMap;
+
+ protected AbstractSortedArrayThreadContextMap(final PropertiesUtil properties) {
initialCapacity = properties.getIntegerProperty(PROPERTY_NAME_INITIAL_CAPACITY, DEFAULT_INITIAL_CAPACITY);
localMap = properties.getBooleanProperty(INHERITABLE_MAP)
? new InheritableThreadLocal() {
@Override
- protected StringMap childValue(final StringMap parentValue) {
- return parentValue != null ? createStringMap(parentValue) : null;
+ protected @Nullable StringMap childValue(final @Nullable StringMap parentValue) {
+ return copyStringMap(parentValue);
}
}
: new ThreadLocal<>();
}
+ /**
+ * Creates a copy of {@link StringMap} used to back this thread context map.
+ *
+ * @param value the context data of the current thread.
+ * @return the context data for a new thread.
+ */
+ protected abstract @Nullable StringMap copyStringMap(@Nullable StringMap value);
+
/**
* Returns an implementation of the {@code StringMap} used to back this thread context map.
*
@@ -95,8 +103,12 @@ protected StringMap createStringMap(final ReadOnlyStringMap original) {
return new SortedArrayStringMap(original);
}
- private StringMap getThreadLocalMap() {
- StringMap map = localMap.get();
+ protected @Nullable StringMap getMutableLocalMapOrNull() {
+ return localMap.get();
+ }
+
+ protected StringMap getMutableLocalMap() {
+ StringMap map = getMutableLocalMapOrNull();
if (map == null) {
map = createStringMap();
localMap.set(map);
@@ -105,51 +117,44 @@ private StringMap getThreadLocalMap() {
}
@Override
- public void put(final String key, final String value) {
- getThreadLocalMap().putValue(key, value);
+ public final void put(final String key, final String value) {
+ putValue(key, value);
}
@Override
public void putValue(final String key, final Object value) {
- getThreadLocalMap().putValue(key, value);
+ getMutableLocalMap().putValue(key, value);
}
@Override
- public void putAll(final Map values) {
- if (values == null || values.isEmpty()) {
- return;
- }
- final StringMap map = getThreadLocalMap();
- for (final Map.Entry entry : values.entrySet()) {
- map.putValue(entry.getKey(), entry.getValue());
- }
+ public final void putAll(final Map values) {
+ putAllValues(values);
}
@Override
public void putAllValues(final Map values) {
- if (values == null || values.isEmpty()) {
+ if (values.isEmpty()) {
return;
}
- final StringMap map = getThreadLocalMap();
- for (final Map.Entry entry : values.entrySet()) {
- map.putValue(entry.getKey(), entry.getValue());
- }
+ final StringMap map = getMutableLocalMap();
+ values.forEach(map::putValue);
}
@Override
- public String get(final String key) {
- return (String) getValue(key);
+ public final @Nullable String get(final String key) {
+ final Object value = getValue(key);
+ return value != null ? value.toString() : null;
}
@Override
- public V getValue(final String key) {
+ public final @Nullable V getValue(final String key) {
final StringMap map = localMap.get();
- return map == null ? null : map.getValue(key);
+ return map == null ? null : (V) map.getValue(key);
}
@Override
public void remove(final String key) {
- final StringMap map = localMap.get();
+ final StringMap map = getMutableLocalMapOrNull();
if (map != null) {
map.remove(key);
}
@@ -157,19 +162,9 @@ public void remove(final String key) {
@Override
public void removeAll(final Iterable keys) {
- final StringMap map = localMap.get();
+ final StringMap map = getMutableLocalMapOrNull();
if (map != null) {
- for (final String key : keys) {
- map.remove(key);
- }
- }
- }
-
- @Override
- public void clear() {
- final StringMap map = localMap.get();
- if (map != null) {
- map.clear();
+ keys.forEach(map::remove);
}
}
@@ -185,21 +180,14 @@ public Map getCopy() {
return map == null ? new HashMap<>() : map.toMap();
}
- /**
- * {@inheritDoc}
- */
@Override
public StringMap getReadOnlyContextData() {
- StringMap map = localMap.get();
- if (map == null) {
- map = createStringMap();
- localMap.set(map);
- }
- return map;
+ final StringMap map = localMap.get();
+ return map == null ? EMPTY_CONTEXT_DATA : map;
}
@Override
- public Map getImmutableMapOrNull() {
+ public @Nullable Map getImmutableMapOrNull() {
final StringMap map = localMap.get();
return map == null ? null : Collections.unmodifiableMap(map.toMap());
}
@@ -218,27 +206,13 @@ public String toString() {
@Override
public int hashCode() {
- final int prime = 31;
- int result = 1;
- final StringMap map = this.localMap.get();
- result = prime * result + ((map == null) ? 0 : map.hashCode());
- return result;
+ return Objects.hashCode(localMap.get());
}
@Override
- public boolean equals(final Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null) {
- return false;
- }
- if (!(obj instanceof ThreadContextMap)) {
- return false;
- }
- final ThreadContextMap other = (ThreadContextMap) obj;
- final Map map = this.getImmutableMapOrNull();
- final Map otherMap = other.getImmutableMapOrNull();
- return Objects.equals(map, otherMap);
+ public boolean equals(final @Nullable Object obj) {
+ return obj == this
+ || obj instanceof ObjectThreadContextMap
+ && Objects.equals(getReadOnlyContextData(), ((ThreadContextMap2) obj).getReadOnlyContextData());
}
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/context/CopyOnWriteSortedArrayThreadContextMap.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/context/CopyOnWriteSortedArrayThreadContextMap.java
new file mode 100644
index 00000000000..e8a0114fd00
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/context/CopyOnWriteSortedArrayThreadContextMap.java
@@ -0,0 +1,100 @@
+/*
+ * 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.core.context;
+
+import java.util.Map;
+import org.apache.logging.log4j.spi.CopyOnWrite;
+import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.StringMap;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * {@code SortedArrayStringMap}-based implementation of the {@code ThreadContextMap} interface that creates a copy of
+ * the data structure on every modification. Any particular instance of the data structure is a snapshot of the
+ * ThreadContext at some point in time and can safely be passed off to other threads. Since it is
+ * expected that the Map will be passed to many more log events than the number of keys it contains the performance
+ * should be much better than if the Map was copied for each event.
+ *
+ * @since 2.7
+ */
+public class CopyOnWriteSortedArrayThreadContextMap extends AbstractSortedArrayThreadContextMap implements CopyOnWrite {
+
+ public CopyOnWriteSortedArrayThreadContextMap() {
+ this(PropertiesUtil.getProperties());
+ }
+
+ CopyOnWriteSortedArrayThreadContextMap(final PropertiesUtil properties) {
+ super(properties);
+ }
+
+ @Override
+ protected @Nullable StringMap copyStringMap(@Nullable StringMap value) {
+ if (value != null) {
+ final StringMap stringMap = createStringMap(value);
+ stringMap.freeze();
+ return stringMap;
+ }
+ return null;
+ }
+
+ @Override
+ protected @Nullable StringMap getMutableLocalMapOrNull() {
+ final StringMap map = localMap.get();
+ if (map != null) {
+ final StringMap mutableMap = createStringMap(map);
+ localMap.set(mutableMap);
+ return mutableMap;
+ }
+ return null;
+ }
+
+ private void freezeLocalMap() {
+ final StringMap map = localMap.get();
+ if (map != null) {
+ map.freeze();
+ }
+ }
+
+ @Override
+ public void putValue(final String key, final Object value) {
+ super.putValue(key, value);
+ freezeLocalMap();
+ }
+
+ @Override
+ public void putAllValues(final Map values) {
+ super.putAllValues(values);
+ freezeLocalMap();
+ }
+
+ @Override
+ public void remove(final String key) {
+ super.remove(key);
+ freezeLocalMap();
+ }
+
+ @Override
+ public void removeAll(final Iterable keys) {
+ super.removeAll(keys);
+ freezeLocalMap();
+ }
+
+ @Override
+ public void clear() {
+ localMap.remove();
+ }
+}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/context/GarbageFreeSortedArrayThreadContextMap.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/context/GarbageFreeSortedArrayThreadContextMap.java
new file mode 100644
index 00000000000..0eddbea2fb1
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/context/GarbageFreeSortedArrayThreadContextMap.java
@@ -0,0 +1,54 @@
+/*
+ * 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.core.context;
+
+import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.StringMap;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * {@code SortedArrayStringMap}-based implementation of the {@code ThreadContextMap} interface that attempts not to
+ * create temporary objects. Adding and removing key-value pairs will not create temporary objects.
+ *
+ * This implementation does not make a copy of its contents on every operation, so this data structure cannot
+ * be passed to log events. Instead, client code needs to copy the contents when interacting with another thread.
+ *
+ * @since 2.7
+ */
+public class GarbageFreeSortedArrayThreadContextMap extends AbstractSortedArrayThreadContextMap {
+
+ public GarbageFreeSortedArrayThreadContextMap() {
+ this(PropertiesUtil.getProperties());
+ }
+
+ GarbageFreeSortedArrayThreadContextMap(final PropertiesUtil properties) {
+ super(properties);
+ }
+
+ @Override
+ protected @Nullable StringMap copyStringMap(@Nullable StringMap value) {
+ return value != null ? createStringMap(value) : null;
+ }
+
+ @Override
+ public void clear() {
+ final StringMap map = localMap.get();
+ if (map != null) {
+ map.clear();
+ }
+ }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/context/StringArrayThreadContextMap.java
similarity index 90%
rename from log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java
rename to log4j-core/src/main/java/org/apache/logging/log4j/core/context/StringArrayThreadContextMap.java
index 608ca77ffb4..0416a00ef30 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/context/StringArrayThreadContextMap.java
@@ -14,15 +14,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.logging.log4j.internal.map;
+package org.apache.logging.log4j.core.context;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
+import org.apache.logging.log4j.core.context.internal.UnmodifiableArrayBackedMap;
import org.apache.logging.log4j.spi.ThreadContextMap;
import org.apache.logging.log4j.util.BiConsumer;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.apache.logging.log4j.util.TriConsumer;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
/**
* An equivalent for DefaultThreadContxtMap, except that it's backed by
@@ -34,16 +37,11 @@
* linearly with the current map size, and callers are advised to minimize this
* work.
*/
+@NullMarked
public class StringArrayThreadContextMap implements ThreadContextMap, ReadOnlyStringMap {
private static final long serialVersionUID = -2635197170958057849L;
- /**
- * Property name ({@value} ) for selecting {@code InheritableThreadLocal} (value "true") or plain
- * {@code ThreadLocal} (value is not "true") in the implementation.
- */
- public static final String INHERITABLE_MAP = "isThreadContextMapInheritable";
-
- private ThreadLocal