diff --git a/log4j-api-test/pom.xml b/log4j-api-test/pom.xml
index 0ef2578dd8f..e1fb806b9b4 100644
--- a/log4j-api-test/pom.xml
+++ b/log4j-api-test/pom.xml
@@ -37,6 +37,7 @@
org.apache.logging.log4j.test
org.apache.commons.lang3.*;resolution:=optional,
+ org.assertj.*;resolution:=optional,
org.junit.*;resolution:=optional,
org.hamcrest.*;resolution:=optional,
@@ -111,7 +112,6 @@
org.assertjassertj-core
- test
diff --git a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/ThreadContextMapSuite.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/ThreadContextMapSuite.java
new file mode 100644
index 00000000000..23bb42d483a
--- /dev/null
+++ b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/ThreadContextMapSuite.java
@@ -0,0 +1,139 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.entry;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.time.Duration;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.apache.logging.log4j.spi.ThreadContextMap;
+import org.junit.jupiter.api.parallel.Execution;
+import org.junit.jupiter.api.parallel.ExecutionMode;
+
+/**
+ * Provides test cases to apply to all implementations of {@link ThreadContextMap}.
+ * @since 2.24.0
+ */
+@Execution(ExecutionMode.CONCURRENT)
+public abstract class ThreadContextMapSuite {
+
+ private static final String KEY = "key";
+
+ /**
+ * Checks if the context map does not propagate to other threads by default.
+ */
+ protected static void threadLocalNotInheritableByDefault(final ThreadContextMap contextMap) {
+ contextMap.put(KEY, "threadLocalNotInheritableByDefault");
+ verifyThreadContextValueFromANewThread(contextMap, null);
+ }
+
+ /**
+ * Checks if the context map can be configured to propagate to other threads.
+ */
+ protected static void threadLocalInheritableIfConfigured(final ThreadContextMap contextMap) {
+ contextMap.put(KEY, "threadLocalInheritableIfConfigured");
+ verifyThreadContextValueFromANewThread(contextMap, "threadLocalInheritableIfConfigured");
+ }
+
+ /**
+ * Checks basic put/remove pattern.
+ */
+ protected static void singleValue(final ThreadContextMap contextMap) {
+ assertThat(contextMap.isEmpty()).as("Map is empty").isTrue();
+ contextMap.put(KEY, "testPut");
+ assertThat(contextMap.isEmpty()).as("Map is not empty").isFalse();
+ assertThat(contextMap.containsKey(KEY)).as("Map key exists").isTrue();
+ assertThat(contextMap.get(KEY)).as("Map contains expected value").isEqualTo("testPut");
+ contextMap.remove(KEY);
+ assertThat(contextMap.isEmpty()).as("Map is empty").isTrue();
+ }
+
+ /**
+ * Checks mutable copy
+ */
+ protected static void getCopyReturnsMutableCopy(final ThreadContextMap contextMap) {
+ contextMap.put(KEY, "testGetCopyReturnsMutableCopy");
+
+ final Map copy = contextMap.getCopy();
+ assertThat(copy).as("Copy contains same value").containsExactly(entry(KEY, "testGetCopyReturnsMutableCopy"));
+
+ copy.put(KEY, "testGetCopyReturnsMutableCopy2");
+ assertThat(contextMap.get(KEY))
+ .as("Original map is not affected by changes in the copy")
+ .isEqualTo("testGetCopyReturnsMutableCopy");
+
+ contextMap.clear();
+ assertThat(contextMap.isEmpty()).as("Original map is empty").isTrue();
+ assertThat(copy)
+ .as("Copy is not affected by changes in the map.")
+ .containsExactly(entry(KEY, "testGetCopyReturnsMutableCopy2"));
+ }
+
+ /**
+ * The immutable copy must be {@code null} if the map is empty.
+ */
+ protected static void getImmutableMapReturnsNullIfEmpty(final ThreadContextMap contextMap) {
+ assertThat(contextMap.isEmpty()).as("Original map is empty").isTrue();
+ assertThat(contextMap.getImmutableMapOrNull())
+ .as("Immutable copy is null")
+ .isNull();
+ }
+
+ /**
+ * The result of {@link ThreadContextMap#getImmutableMapOrNull()} must be immutable.
+ */
+ protected static void getImmutableMapReturnsImmutableMapIfNonEmpty(final ThreadContextMap contextMap) {
+ contextMap.put(KEY, "getImmutableMapReturnsImmutableMapIfNonEmpty");
+
+ final Map immutable = contextMap.getImmutableMapOrNull();
+ assertThat(immutable)
+ .as("Immutable copy contains same value")
+ .containsExactly(entry(KEY, "getImmutableMapReturnsImmutableMapIfNonEmpty"));
+
+ assertThrows(
+ UnsupportedOperationException.class, () -> immutable.put(KEY, "getImmutableMapReturnsNullIfEmpty2"));
+ }
+
+ /**
+ * The immutable copy is not affected by changes to the original map.
+ */
+ protected static void getImmutableMapCopyNotAffectedByContextMapChanges(final ThreadContextMap contextMap) {
+ contextMap.put(KEY, "getImmutableMapCopyNotAffectedByContextMapChanges");
+
+ final Map immutable = contextMap.getImmutableMapOrNull();
+ contextMap.put(KEY, "getImmutableMapCopyNotAffectedByContextMapChanges2");
+ assertThat(immutable)
+ .as("Immutable copy contains the original value")
+ .containsExactly(entry(KEY, "getImmutableMapCopyNotAffectedByContextMapChanges"));
+ }
+
+ 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/internal/map/StringArrayThreadContextMapTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMapTest.java
deleted file mode 100644
index 2f845d8bf73..00000000000
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMapTest.java
+++ /dev/null
@@ -1,287 +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.internal.map;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import org.apache.logging.log4j.test.junit.UsingThreadContextMap;
-import org.apache.logging.log4j.util.TriConsumer;
-import org.junit.jupiter.api.Test;
-
-/**
- * Tests the {@code StringArrayThreadContextMap} class.
- */
-@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();
- assertNull(map1.get("test"));
- map1.put("test", "test");
- assertEquals("test", map1.get("test"));
- assertNull(map1.get("not_present"));
- assertEquals("test", map1.getValue("test"));
- assertNull(map1.getValue("not_present"));
-
- map1.clear();
- assertNull(map1.get("not_present"));
- }
-
- @Test
- public void testPut() {
- final StringArrayThreadContextMap map = new StringArrayThreadContextMap();
- assertTrue(map.isEmpty());
- assertFalse(map.containsKey("key"));
- map.put("key", "value");
-
- assertFalse(map.isEmpty());
- assertTrue(map.containsKey("key"));
- assertEquals("value", map.get("key"));
- }
-
- @Test
- public void testPutAll() {
- final StringArrayThreadContextMap map = new StringArrayThreadContextMap();
- assertTrue(map.isEmpty());
- assertFalse(map.containsKey("key"));
- final int mapSize = 10;
- final Map newMap = new HashMap<>(mapSize);
- for (int i = 1; i <= mapSize; i++) {
- newMap.put("key" + i, "value" + i);
- }
- map.putAll(newMap);
- assertFalse(map.isEmpty());
- for (int i = 1; i <= mapSize; i++) {
- assertTrue(map.containsKey("key" + i));
- assertEquals("value" + i, map.get("key" + i));
- }
- }
-
- /**
- * Test method for
- * {@link org.apache.logging.log4j.internal.map.StringArrayThreadContextMap#remove(java.lang.String)}
- * .
- */
- @Test
- public void testRemove() {
- final StringArrayThreadContextMap map = createMap();
- assertEquals("value", map.get("key"));
- assertEquals("value2", map.get("key2"));
-
- map.remove("key");
- assertFalse(map.containsKey("key"));
- assertEquals("value2", map.get("key2"));
-
- map.clear();
- map.remove("test");
- }
-
- @Test
- public void testRemoveAll() {
- final StringArrayThreadContextMap map = createMap();
-
- Map newValues = new HashMap<>();
- newValues.put("1", "value1");
- newValues.put("2", "value2");
-
- map.putAll(newValues);
- map.removeAll(newValues.keySet());
-
- map.put("3", "value3");
-
- map.clear();
- map.removeAll(newValues.keySet());
- }
-
- @Test
- public void testClear() {
- final StringArrayThreadContextMap map = createMap();
-
- map.clear();
- assertTrue(map.isEmpty());
- assertFalse(map.containsKey("key"));
- assertFalse(map.containsKey("key2"));
- }
-
- /**
- * @return
- */
- private StringArrayThreadContextMap createMap() {
- final StringArrayThreadContextMap map = new StringArrayThreadContextMap();
- assertTrue(map.isEmpty());
- map.put("key", "value");
- map.put("key2", "value2");
- assertEquals("value", map.get("key"));
- assertEquals("value2", map.get("key2"));
- return map;
- }
-
- @Test
- public void testGetCopyReturnsMutableMap() {
- final StringArrayThreadContextMap map = new StringArrayThreadContextMap();
- assertTrue(map.isEmpty());
- final Map copy = map.getCopy();
- assertTrue(copy.isEmpty());
-
- copy.put("key", "value"); // mutable
- assertEquals("value", copy.get("key"));
-
- // thread context map not affected
- assertTrue(map.isEmpty());
- }
-
- @Test
- public void testGetCopyReturnsMutableCopy() {
- final StringArrayThreadContextMap map = new StringArrayThreadContextMap();
- map.put("key1", "value1");
- assertFalse(map.isEmpty());
- final Map copy = map.getCopy();
- assertEquals("value1", copy.get("key1")); // copy has values too
-
- copy.put("key", "value"); // copy is mutable
- assertEquals("value", copy.get("key"));
-
- // thread context map not affected
- assertFalse(map.containsKey("key"));
-
- // clearing context map does not affect copy
- map.clear();
- assertTrue(map.isEmpty());
-
- assertFalse(copy.isEmpty());
- }
-
- @Test
- public void testGetImmutableMapReturnsNullIfEmpty() {
- final StringArrayThreadContextMap map = new StringArrayThreadContextMap();
- assertTrue(map.isEmpty());
- assertNull(map.getImmutableMapOrNull());
- }
-
- @Test
- public void testGetImmutableMapReturnsImmutableMapIfNonEmpty() {
- final StringArrayThreadContextMap map = new StringArrayThreadContextMap();
- map.put("key1", "value1");
- assertFalse(map.isEmpty());
-
- final Map immutable = map.getImmutableMapOrNull();
- assertEquals("value1", immutable.get("key1")); // copy has values too
-
- // immutable
- assertThrows(UnsupportedOperationException.class, () -> immutable.put("key", "value"));
- }
-
- @Test
- public void testGetImmutableMapCopyNotAffectdByContextMapChanges() {
- final StringArrayThreadContextMap map = new StringArrayThreadContextMap();
- map.put("key1", "value1");
- assertFalse(map.isEmpty());
-
- final Map immutable = map.getImmutableMapOrNull();
- assertEquals("value1", immutable.get("key1")); // copy has values too
-
- // clearing context map does not affect copy
- map.clear();
- assertTrue(map.isEmpty());
-
- assertFalse(immutable.isEmpty());
- }
-
- @Test
- public void testToStringShowsMapContext() {
- final StringArrayThreadContextMap map = new StringArrayThreadContextMap();
- assertEquals("{}", map.toString());
-
- map.put("key1", "value1");
- assertEquals("{key1=value1}", map.toString());
-
- map.remove("key1");
- map.put("key2", "value2");
- assertEquals("{key2=value2}", map.toString());
- }
-
- @Test
- public void testEmptyMap() {
- assertNull(UnmodifiableArrayBackedMap.EMPTY_MAP.get("test"));
- }
-
- @Test
- public void testForEachBiConsumer_Log4jUtil() {
- StringArrayThreadContextMap map = createMap();
- Set keys = new HashSet<>();
- org.apache.logging.log4j.util.BiConsumer log4j_util_action =
- new org.apache.logging.log4j.util.BiConsumer() {
- @Override
- public void accept(String key, String value) {
- keys.add(key);
- }
- };
- map.forEach(log4j_util_action);
- assertEquals(map.toMap().keySet(), keys);
-
- map.clear();
- keys.clear();
- map.forEach(log4j_util_action);
- assertTrue(keys.isEmpty());
- }
-
- @Test
- public void testForEachTriConsumer() {
- StringArrayThreadContextMap map = createMap();
- HashMap iterationResultMap = new HashMap<>();
- TriConsumer> triConsumer =
- new TriConsumer>() {
- @Override
- public void accept(String k, String v, Map s) {
- s.put(k, v);
- }
- };
- map.forEach(triConsumer, iterationResultMap);
- assertEquals(map.toMap(), iterationResultMap);
-
- map.clear();
- iterationResultMap.clear();
- map.forEach(triConsumer, iterationResultMap);
- assertTrue(iterationResultMap.isEmpty());
- }
-}
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMapTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMapTest.java
index 9652034d70f..5957a931bf3 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMapTest.java
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMapTest.java
@@ -352,17 +352,17 @@ public void testState() {
UnmodifiableArrayBackedMap newMap;
originalMap = UnmodifiableArrayBackedMap.EMPTY_MAP;
- newMap = UnmodifiableArrayBackedMap.getInstance(originalMap.getBackingArray());
+ newMap = UnmodifiableArrayBackedMap.getMap(originalMap.getBackingArray());
assertEquals(originalMap, newMap);
originalMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(getTestParameters());
- newMap = UnmodifiableArrayBackedMap.getInstance(originalMap.getBackingArray());
+ newMap = UnmodifiableArrayBackedMap.getMap(originalMap.getBackingArray());
assertEquals(originalMap, newMap);
originalMap = UnmodifiableArrayBackedMap.EMPTY_MAP
.copyAndPutAll(getTestParameters())
.copyAndRemove("1");
- newMap = UnmodifiableArrayBackedMap.getInstance(originalMap.getBackingArray());
+ newMap = UnmodifiableArrayBackedMap.getMap(originalMap.getBackingArray());
assertEquals(originalMap, newMap);
}
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..4e3356aad31 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
@@ -16,67 +16,43 @@
*/
package org.apache.logging.log4j.spi;
-import static org.junit.Assert.assertNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
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.ThreadContextMapSuite;
+import org.apache.logging.log4j.util.PropertiesUtil;
import org.junit.jupiter.api.Test;
/**
* Tests the {@code DefaultThreadContextMap} class.
*/
@UsingThreadContextMap
-public class DefaultThreadContextMapTest {
+class DefaultThreadContextMapTest extends ThreadContextMapSuite {
- @Test
- public void testEqualsVsSameKind() {
- final DefaultThreadContextMap map1 = createMap();
- final DefaultThreadContextMap map2 = createMap();
- assertEquals(map1, map1);
- assertEquals(map2, map2);
- assertEquals(map1, map2);
- assertEquals(map2, map1);
+ private ThreadContextMap createThreadContextMap() {
+ return new DefaultThreadContextMap();
}
- @Test
- public void testHashCodeVsSameKind() {
- final DefaultThreadContextMap map1 = createMap();
- final DefaultThreadContextMap map2 = createMap();
- assertEquals(map1.hashCode(), map2.hashCode());
+ private ThreadContextMap createInheritableThreadContextMap() {
+ final Properties props = new Properties();
+ props.setProperty("log4j2.isThreadContextMapInheritable", "true");
+ final PropertiesUtil util = new PropertiesUtil(props);
+ return new DefaultThreadContextMap(util);
}
@Test
- public void testDoesNothingIfConstructedWithUseMapIsFalse() {
- final DefaultThreadContextMap map = new DefaultThreadContextMap(false);
- assertTrue(map.isEmpty());
- assertFalse(map.containsKey("key"));
- map.put("key", "value");
-
- assertTrue(map.isEmpty());
- assertFalse(map.containsKey("key"));
- assertNull(map.get("key"));
- }
-
- @Test
- public void testPut() {
- final DefaultThreadContextMap map = new DefaultThreadContextMap(true);
- assertTrue(map.isEmpty());
- assertFalse(map.containsKey("key"));
- map.put("key", "value");
-
- assertFalse(map.isEmpty());
- assertTrue(map.containsKey("key"));
- assertEquals("value", map.get("key"));
+ void singleValue() {
+ singleValue(createThreadContextMap());
}
@Test
- public void testPutAll() {
- final DefaultThreadContextMap map = new DefaultThreadContextMap(true);
+ void testPutAll() {
+ final DefaultThreadContextMap map = new DefaultThreadContextMap();
assertTrue(map.isEmpty());
assertFalse(map.containsKey("key"));
final int mapSize = 10;
@@ -92,24 +68,8 @@ public void testPutAll() {
}
}
- /**
- * Test method for
- * {@link org.apache.logging.log4j.spi.DefaultThreadContextMap#remove(java.lang.String)}
- * .
- */
@Test
- public void testRemove() {
- final DefaultThreadContextMap map = createMap();
- assertEquals("value", map.get("key"));
- assertEquals("value2", map.get("key2"));
-
- map.remove("key");
- assertFalse(map.containsKey("key"));
- assertEquals("value2", map.get("key2"));
- }
-
- @Test
- public void testClear() {
+ void testClear() {
final DefaultThreadContextMap map = createMap();
map.clear();
@@ -118,11 +78,8 @@ public void testClear() {
assertFalse(map.containsKey("key2"));
}
- /**
- * @return
- */
private DefaultThreadContextMap createMap() {
- final DefaultThreadContextMap map = new DefaultThreadContextMap(true);
+ final DefaultThreadContextMap map = new DefaultThreadContextMap();
assertTrue(map.isEmpty());
map.put("key", "value");
map.put("key2", "value2");
@@ -132,79 +89,28 @@ private DefaultThreadContextMap createMap() {
}
@Test
- public void testGetCopyReturnsMutableMap() {
- final DefaultThreadContextMap map = new DefaultThreadContextMap(true);
- assertTrue(map.isEmpty());
- final Map copy = map.getCopy();
- assertTrue(copy.isEmpty());
-
- copy.put("key", "value"); // mutable
- assertEquals("value", copy.get("key"));
-
- // thread context map not affected
- assertTrue(map.isEmpty());
- }
-
- @Test
- public void testGetCopyReturnsMutableCopy() {
- final DefaultThreadContextMap map = new DefaultThreadContextMap(true);
- map.put("key1", "value1");
- assertFalse(map.isEmpty());
- final Map copy = map.getCopy();
- assertEquals("value1", copy.get("key1")); // copy has values too
-
- copy.put("key", "value"); // copy is mutable
- assertEquals("value", copy.get("key"));
-
- // thread context map not affected
- assertFalse(map.containsKey("key"));
-
- // clearing context map does not affect copy
- map.clear();
- assertTrue(map.isEmpty());
-
- assertFalse(copy.isEmpty());
+ void getCopyReturnsMutableCopy() {
+ getCopyReturnsMutableCopy(createThreadContextMap());
}
@Test
- public void testGetImmutableMapReturnsNullIfEmpty() {
- final DefaultThreadContextMap map = new DefaultThreadContextMap(true);
- assertTrue(map.isEmpty());
- assertNull(map.getImmutableMapOrNull());
+ void getImmutableMapReturnsNullIfEmpty() {
+ getImmutableMapReturnsNullIfEmpty(createThreadContextMap());
}
@Test
- public void testGetImmutableMapReturnsImmutableMapIfNonEmpty() {
- final DefaultThreadContextMap map = new DefaultThreadContextMap(true);
- map.put("key1", "value1");
- assertFalse(map.isEmpty());
-
- final Map immutable = map.getImmutableMapOrNull();
- assertEquals("value1", immutable.get("key1")); // copy has values too
-
- // immutable
- assertThrows(UnsupportedOperationException.class, () -> immutable.put("key", "value"));
+ void getImmutableMapReturnsImmutableMapIfNonEmpty() {
+ getImmutableMapReturnsImmutableMapIfNonEmpty(createThreadContextMap());
}
@Test
- public void testGetImmutableMapCopyNotAffectdByContextMapChanges() {
- final DefaultThreadContextMap map = new DefaultThreadContextMap(true);
- map.put("key1", "value1");
- assertFalse(map.isEmpty());
-
- final Map immutable = map.getImmutableMapOrNull();
- assertEquals("value1", immutable.get("key1")); // copy has values too
-
- // clearing context map does not affect copy
- map.clear();
- assertTrue(map.isEmpty());
-
- assertFalse(immutable.isEmpty());
+ void getImmutableMapCopyNotAffectedByContextMapChanges() {
+ getImmutableMapCopyNotAffectedByContextMapChanges(createThreadContextMap());
}
@Test
- public void testToStringShowsMapContext() {
- final DefaultThreadContextMap map = new DefaultThreadContextMap(true);
+ void testToStringShowsMapContext() {
+ final DefaultThreadContextMap map = new DefaultThreadContextMap();
assertEquals("{}", map.toString());
map.put("key1", "value1");
@@ -214,4 +120,14 @@ public void testToStringShowsMapContext() {
map.put("key2", "value2");
assertEquals("{key2=value2}", map.toString());
}
+
+ @Test
+ void threadLocalNotInheritableByDefault() {
+ threadLocalNotInheritableByDefault(createThreadContextMap());
+ }
+
+ @Test
+ void threadLocalInheritableIfConfigured() {
+ threadLocalInheritableIfConfigured(createInheritableThreadContextMap());
+ }
}
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextMapTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextMapTest.java
deleted file mode 100644
index f394e71bea0..00000000000
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextMapTest.java
+++ /dev/null
@@ -1,76 +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 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.util.PropertiesUtil;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.MethodSource;
-
-class ThreadContextMapTest {
-
- private static final String KEY = "key";
-
- static Stream defaultMaps() {
- return Stream.of(
- new DefaultThreadContextMap(),
- new CopyOnWriteSortedArrayThreadContextMap(),
- new GarbageFreeSortedArrayThreadContextMap());
- }
-
- static Stream inheritableMaps() {
- final Properties props = new Properties();
- props.setProperty("log4j2.isThreadContextMapInheritable", "true");
- final PropertiesUtil util = new PropertiesUtil(props);
- return Stream.of(
- new DefaultThreadContextMap(true, util),
- new CopyOnWriteSortedArrayThreadContextMap(util),
- new GarbageFreeSortedArrayThreadContextMap(util));
- }
-
- @ParameterizedTest
- @MethodSource("defaultMaps")
- void threadLocalNotInheritableByDefault(final ThreadContextMap contextMap) {
- contextMap.put(KEY, "threadLocalNotInheritableByDefault");
- verifyThreadContextValueFromANewThread(contextMap, null);
- }
-
- @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();
- }
- }
-}
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..d919b3130a1 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,7 +23,6 @@
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;
@@ -276,8 +275,6 @@ public static void putAll(final Map m) {
((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());
@@ -320,8 +317,6 @@ public static void removeAll(final Iterable keys) {
((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);
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java
deleted file mode 100644
index 608ca77ffb4..00000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java
+++ /dev/null
@@ -1,199 +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.internal.map;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-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;
-
-/**
- * An equivalent for DefaultThreadContxtMap, except that it's backed by
- * UnmodifiableArrayBackedMap. An instance of UnmodifiableArrayBackedMap can be
- * represented as a single Object[], which can safely be stored on the
- * ThreadLocal with no fear of classloader-related memory leaks. Performance
- * of the underlying UnmodifiableArrayBackedMap exceeds HashMap in all
- * supported operations other than get(). Note that get() performance scales
- * linearly with the current map size, and callers are advised to minimize this
- * work.
- */
-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
*/
+@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 extends LoggerContextFactory> implementation = loadLoggerContextFactory();
+ if (implementation != null) {
+ try {
+ return LoaderUtil.newInstanceOf(implementation);
+ } 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,9 +254,8 @@ 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;
}
@@ -362,74 +278,28 @@ public Class extends ThreadContextMap> loadThreadContextMap() {
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 Class extends ThreadContextMap> implementation = loadThreadContextMap();
+ if (implementation != null) {
+ try {
+ return LoaderUtil.newInstanceOf(implementation);
+ } catch (final ReflectiveOperationException e) {
+ LOGGER.error("Failed to instantiate logger context factory {}.", implementation.getName(), e);
+ }
+ }
+ final PropertiesUtil props = PropertiesUtil.getProperties();
+ return props.getBooleanProperty(DISABLE_CONTEXT_MAP) || props.getBooleanProperty(DISABLE_THREAD_CONTEXT)
+ ? NoOpThreadContextMap.INSTANCE
+ : new DefaultThreadContextMap();
}
/**
@@ -440,7 +310,7 @@ public ThreadContextMap getThreadContextMapInstance() {
* @deprecated since 2.24.0, without replacement.
*/
@Deprecated
- public URL getUrl() {
+ public @Nullable URL getUrl() {
return url;
}
@@ -448,7 +318,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();
@@ -478,24 +348,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/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/core/async/AbstractAsyncThreadContextTestBase.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AbstractAsyncThreadContextTestBase.java
index a98bce9fdc0..572b2ca4d51 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
@@ -26,24 +26,26 @@
import java.util.concurrent.TimeUnit;
import java.util.function.LongSupplier;
import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
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.impl.Log4jContextFactory;
+import org.apache.logging.log4j.core.impl.ThreadContextTestAccess;
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.spi.ThreadContextMap;
import org.apache.logging.log4j.test.TestProperties;
import org.apache.logging.log4j.test.junit.Log4jStaticResources;
import org.apache.logging.log4j.test.junit.UsingStatusListener;
import org.apache.logging.log4j.test.junit.UsingTestProperties;
+import org.apache.logging.log4j.util.ProviderUtil;
import org.apache.logging.log4j.util.Unbox;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.parallel.ResourceLock;
@@ -89,26 +91,29 @@ void initConfigFile() {
}
protected enum ContextImpl {
- WEBAPP,
- GARBAGE_FREE,
- COPY_ON_WRITE;
+ WEBAPP("WebApp", "org.apache.logging.log4j.spi.DefaultThreadContextMap"),
+ GARBAGE_FREE(
+ "GarbageFree", "org.apache.logging.log4j.core.context.internal.GarbageFreeSortedArrayThreadContextMap");
+
+ private final String threadContextMap;
+ private final String implClass;
+
+ ContextImpl(final String threadContextMap, final String implClass) {
+ this.threadContextMap = threadContextMap;
+ this.implClass = implClass;
+ }
void init() {
- final String PACKAGE = "org.apache.logging.log4j.spi.";
- props.setProperty("log4j2.threadContextMap", PACKAGE + implClassSimpleName());
+ props.setProperty("log4j2.threadContextMap", threadContextMap);
ThreadContextTestAccess.init();
}
- 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 StringUtils.substringAfterLast(implClass, '.');
+ }
+
+ public String getImplClass() {
+ return implClass;
}
}
@@ -116,16 +121,12 @@ private void init(final ContextImpl contextImpl, final Mode asyncMode) {
asyncMode.initSelector();
asyncMode.initConfigFile();
- contextImpl.init();
// Verify that we are using the requested context map
- if (contextImpl == ContextImpl.WEBAPP) {
- assertThat(ThreadContext.getThreadContextMap()).isNull();
- } else {
- assertThat(ThreadContext.getThreadContextMap())
- .isNotNull()
- .extracting(o -> o.getClass().getSimpleName())
- .isEqualTo(contextImpl.implClassSimpleName());
- }
+ contextImpl.init();
+ final ThreadContextMap threadContextMap = ProviderUtil.getProvider().getThreadContextMapInstance();
+ assertThat(threadContextMap.getClass().getName())
+ .as("Check `ThreadContextMap` implementation")
+ .isEqualTo(contextImpl.getImplClass());
}
private LongSupplier remainingCapacity(final LoggerContext loggerContext, final LoggerConfig loggerConfig) {
@@ -197,25 +198,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-core-test/src/test/java/org/apache/logging/log4j/core/async/AsyncThreadContextCopyOnWriteTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AsyncThreadContextCopyOnWriteTest.java
deleted file mode 100644
index d37da33cf58..00000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AsyncThreadContextCopyOnWriteTest.java
+++ /dev/null
@@ -1,40 +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.core.async;
-
-import java.nio.file.Path;
-import org.apache.logging.log4j.core.test.junit.Tags;
-import org.apache.logging.log4j.test.junit.TempLoggingDir;
-import org.junit.jupiter.api.Tag;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.EnumSource;
-
-// Note: the different ThreadContextMap implementations cannot be parameterized:
-// ThreadContext initialization will result in static final fields being set in various components.
-// To use a different ThreadContextMap, the test needs to be run in a new JVM.
-@Tag(Tags.ASYNC_LOGGERS)
-class AsyncThreadContextCopyOnWriteTest extends AbstractAsyncThreadContextTestBase {
-
- @TempLoggingDir
- private static Path loggingPath;
-
- @ParameterizedTest
- @EnumSource
- void testAsyncLogWritesToLog(Mode asyncMode) throws Exception {
- testAsyncLogWritesToLog(ContextImpl.COPY_ON_WRITE, asyncMode, loggingPath);
- }
-}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/internal/GarbageFreeSortedArrayThreadContextMapTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/internal/GarbageFreeSortedArrayThreadContextMapTest.java
new file mode 100644
index 00000000000..192c72e85ce
--- /dev/null
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/internal/GarbageFreeSortedArrayThreadContextMapTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import org.apache.logging.log4j.spi.ThreadContextMap;
+import org.apache.logging.log4j.test.spi.ThreadContextMapSuite;
+import org.apache.logging.log4j.util.PropertiesUtil;
+import org.junit.jupiter.api.Test;
+
+public class GarbageFreeSortedArrayThreadContextMapTest extends ThreadContextMapSuite {
+
+ private GarbageFreeSortedArrayThreadContextMap createThreadContextMap() {
+ return new GarbageFreeSortedArrayThreadContextMap();
+ }
+
+ private ThreadContextMap createInheritableThreadContextMap() {
+ final Properties props = new Properties();
+ props.setProperty("log4j2.isThreadContextMapInheritable", "true");
+ final PropertiesUtil util = new PropertiesUtil(props);
+ return new GarbageFreeSortedArrayThreadContextMap(util);
+ }
+
+ @Test
+ void singleValue() {
+ singleValue(createThreadContextMap());
+ }
+
+ @Test
+ void testPutAll() {
+ final GarbageFreeSortedArrayThreadContextMap map = createThreadContextMap();
+ assertTrue(map.isEmpty());
+ assertFalse(map.containsKey("key"));
+ final int mapSize = 10;
+ final Map newMap = new HashMap<>(mapSize);
+ for (int i = 1; i <= mapSize; i++) {
+ newMap.put("key" + i, "value" + i);
+ }
+ map.putAll(newMap);
+ assertFalse(map.isEmpty());
+ for (int i = 1; i <= mapSize; i++) {
+ assertTrue(map.containsKey("key" + i));
+ assertEquals("value" + i, map.get("key" + i));
+ }
+ }
+
+ @Test
+ void testClear() {
+ final GarbageFreeSortedArrayThreadContextMap map = createMap();
+
+ map.clear();
+ assertTrue(map.isEmpty());
+ assertFalse(map.containsKey("key"));
+ assertFalse(map.containsKey("key2"));
+ }
+
+ private GarbageFreeSortedArrayThreadContextMap createMap() {
+ final GarbageFreeSortedArrayThreadContextMap map = createThreadContextMap();
+ assertTrue(map.isEmpty());
+ map.put("key", "value");
+ map.put("key2", "value2");
+ assertEquals("value", map.get("key"));
+ assertEquals("value2", map.get("key2"));
+ return map;
+ }
+
+ @Test
+ void getCopyReturnsMutableCopy() {
+ getCopyReturnsMutableCopy(createThreadContextMap());
+ }
+
+ @Test
+ void getImmutableMapReturnsNullIfEmpty() {
+ getImmutableMapReturnsNullIfEmpty(createThreadContextMap());
+ }
+
+ @Test
+ void getImmutableMapReturnsImmutableMapIfNonEmpty() {
+ getImmutableMapReturnsImmutableMapIfNonEmpty(createThreadContextMap());
+ }
+
+ @Test
+ void getImmutableMapCopyNotAffectedByContextMapChanges() {
+ getImmutableMapCopyNotAffectedByContextMapChanges(createThreadContextMap());
+ }
+
+ @Test
+ void threadLocalNotInheritableByDefault() {
+ threadLocalNotInheritableByDefault(createThreadContextMap());
+ }
+
+ @Test
+ void threadLocalInheritableIfConfigured() {
+ threadLocalInheritableIfConfigured(createInheritableThreadContextMap());
+ }
+}
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 ff4211ca291..896d0d2d8b7 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,8 +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;
@@ -50,26 +50,23 @@ 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.internal.GarbageFreeSortedArrayThreadContextMap"},
+ {"org.apache.logging.log4j.spi.DefaultThreadContextMap"}
});
}
@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);
}
@@ -82,13 +79,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();
final StringMap stringMap = contextDataInjector.injectContextData(null, new SortedArrayStringMap());
@@ -121,8 +116,8 @@ private void testContextDataInjector() {
private void prepareThreadContext(final boolean isThreadContextMapInheritable) {
System.setProperty("log4j2.isThreadContextMapInheritable", Boolean.toString(isThreadContextMapInheritable));
- ThreadContextUtilityClass.reset();
- ThreadContext.remove("baz");
+ resetThreadContextMap();
+ ThreadContext.clearMap();
ThreadContext.put("foo", "bar");
}
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/core/impl/ThreadContextTestAccess.java
similarity index 80%
rename from log4j-core-test/src/test/java/org/apache/logging/log4j/ThreadContextTestAccess.java
rename to log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextTestAccess.java
index ecf3e9f6c69..661023a46f5 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/ThreadContextTestAccess.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextTestAccess.java
@@ -14,7 +14,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.logging.log4j;
+package org.apache.logging.log4j.core.impl;
+
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.util.ProviderUtil;
/**
*
@@ -29,6 +32,8 @@ private ThreadContextTestAccess() { // prevent instantiation
}
public static void init() {
+ final Log4jProvider provider = (Log4jProvider) ProviderUtil.getProvider();
+ provider.resetThreadContextMap();
ThreadContext.init();
}
}
diff --git a/log4j-core/pom.xml b/log4j-core/pom.xml
index 646f0c47510..7976c800b5e 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,
@@ -127,6 +129,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/internal/GarbageFreeSortedArrayThreadContextMap.java
similarity index 95%
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/internal/GarbageFreeSortedArrayThreadContextMap.java
index 1622a0c455b..b9ee2be759b 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/internal/GarbageFreeSortedArrayThreadContextMap.java
@@ -14,12 +14,15 @@
* 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.internal;
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.ThreadContextMap;
import org.apache.logging.log4j.util.PropertiesUtil;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.apache.logging.log4j.util.SortedArrayStringMap;
@@ -34,7 +37,7 @@
*
* @since 2.7
*/
-class GarbageFreeSortedArrayThreadContextMap implements ReadOnlyThreadContextMap, ObjectThreadContextMap {
+public class GarbageFreeSortedArrayThreadContextMap implements ReadOnlyThreadContextMap, ObjectThreadContextMap {
/**
* Property name ({@value} ) for selecting {@code InheritableThreadLocal} (value "true") or plain
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java
index bd0b62337cb..9c3a05dabf7 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java
@@ -18,14 +18,133 @@
import aQute.bnd.annotation.Resolution;
import aQute.bnd.annotation.spi.ServiceProvider;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.core.context.internal.GarbageFreeSortedArrayThreadContextMap;
+import org.apache.logging.log4j.spi.DefaultThreadContextMap;
+import org.apache.logging.log4j.spi.LoggerContextFactory;
+import org.apache.logging.log4j.spi.NoOpThreadContextMap;
import org.apache.logging.log4j.spi.Provider;
+import org.apache.logging.log4j.spi.ThreadContextMap;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.Lazy;
+import org.apache.logging.log4j.util.LoaderUtil;
+import org.apache.logging.log4j.util.PropertiesUtil;
+import org.jspecify.annotations.NullMarked;
/**
* Binding for the Log4j API.
*/
@ServiceProvider(value = Provider.class, resolution = Resolution.OPTIONAL)
+@NullMarked
public class Log4jProvider extends Provider {
+
+ /**
+ * Constant used to disable the {@link ThreadContextMap}.
+ *
+ * Warning: the value of this constant does not point to a concrete class name.
+ *
+ * @see #getThreadContextMap
+ */
+ private 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
+ */
+ private static final String WEB_APP_CONTEXT_MAP = "WebApp";
+
+ /**
+ * 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
+ */
+ private 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";
+
+ // Name of the context map implementations
+ private static final String WEB_APP_CLASS_NAME = "org.apache.logging.log4j.spi.DefaultThreadContextMap";
+ private static final String GARBAGE_FREE_CLASS_NAME =
+ "org.apache.logging.log4j.core.context.internal.GarbageFreeSortedArrayThreadContextMap";
+
+ private static final Logger LOGGER = StatusLogger.getLogger();
+
+ private final Lazy loggerContextFactoryLazy = Lazy.lazy(Log4jContextFactory::new);
+ private final Lazy threadContextMapLazy = Lazy.lazy(this::createThreadContextMap);
+
public Log4jProvider() {
super(10, CURRENT_VERSION, Log4jContextFactory.class);
}
+
+ @Override
+ public LoggerContextFactory getLoggerContextFactory() {
+ return loggerContextFactoryLazy.get();
+ }
+
+ @Override
+ public ThreadContextMap getThreadContextMapInstance() {
+ return threadContextMapLazy.get();
+ }
+
+ private ThreadContextMap createThreadContextMap() {
+ // Properties
+ final PropertiesUtil props = PropertiesUtil.getProperties();
+ if (props.getBooleanProperty(DISABLE_CONTEXT_MAP) || props.getBooleanProperty(DISABLE_THREAD_CONTEXT)) {
+ return NoOpThreadContextMap.INSTANCE;
+ }
+ String threadContextMapClass = props.getStringProperty(THREAD_CONTEXT_MAP_PROPERTY);
+ // Default based on properties
+ if (threadContextMapClass == null) {
+ threadContextMapClass = props.getBooleanProperty(GC_FREE_THREAD_CONTEXT_PROPERTY)
+ ? GARBAGE_FREE_CONTEXT_MAP
+ : WEB_APP_CONTEXT_MAP;
+ }
+ /*
+ * 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 (threadContextMapClass) {
+ case NO_OP_CONTEXT_MAP:
+ return NoOpThreadContextMap.INSTANCE;
+ case WEB_APP_CONTEXT_MAP:
+ case WEB_APP_CLASS_NAME:
+ return new DefaultThreadContextMap();
+ // Old FQCN of the garbage-free context map
+ case "org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap":
+ case GARBAGE_FREE_CONTEXT_MAP:
+ case GARBAGE_FREE_CLASS_NAME:
+ return new GarbageFreeSortedArrayThreadContextMap();
+ default:
+ try {
+ return LoaderUtil.newCheckedInstanceOf(threadContextMapClass, ThreadContextMap.class);
+ } catch (final Exception e) {
+ LOGGER.error("Unable to create instance of class {}.", threadContextMapClass, e);
+ }
+ }
+ LOGGER.warn("Falling back to {}.", NoOpThreadContextMap.class.getName());
+ return NoOpThreadContextMap.INSTANCE;
+ }
+
+ // Used in tests
+ void resetThreadContextMap() {
+ threadContextMapLazy.set(null);
+ }
}
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/appender/StringAppender.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/appender/StringAppender.java
new file mode 100644
index 00000000000..b6bc60eaf78
--- /dev/null
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/appender/StringAppender.java
@@ -0,0 +1,98 @@
+/*
+ * 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.perf.appender;
+
+import java.io.Serializable;
+import java.util.concurrent.TimeUnit;
+import org.apache.logging.log4j.core.Filter;
+import org.apache.logging.log4j.core.Layout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.Property;
+import org.apache.logging.log4j.core.layout.SerializedLayout;
+
+/**
+ * This appender is primarily used for testing.
+ * This appender simply saves the last message logged.
+ *
+ * This appender will use {@link Layout#toByteArray(LogEvent)}.
+ */
+public class StringAppender extends AbstractAppender {
+ private String message;
+
+ public StringAppender(final String name, final Filter filter, final Layout extends Serializable> layout) {
+ super(name, filter, layout, true, Property.EMPTY_ARRAY);
+ if (layout != null && !(layout instanceof SerializedLayout)) {
+ final byte[] bytes = layout.getHeader();
+ if (bytes != null) {
+ message = new String(bytes);
+ }
+ }
+ }
+
+ @Override
+ public void append(final LogEvent event) {
+ final Layout extends Serializable> layout = getLayout();
+ if (layout instanceof SerializedLayout) {
+ final byte[] header = layout.getHeader();
+ final byte[] content = layout.toByteArray(event);
+ final byte[] record = new byte[header.length + content.length];
+ System.arraycopy(header, 0, record, 0, header.length);
+ System.arraycopy(content, 0, record, header.length, content.length);
+ message = new String(record);
+ } else {
+ message = new String(layout.toByteArray(event));
+ }
+ }
+
+ @Override
+ public boolean stop(final long timeout, final TimeUnit timeUnit) {
+ setStopped();
+ return true;
+ }
+
+ public StringAppender clear() {
+ message = null;
+ return this;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public static StringAppender createAppender(
+ final String name, final Layout extends Serializable> layout, final Filter filter) {
+ return new StringAppender(name, filter, layout);
+ }
+
+ /**
+ * Gets the named StringAppender if it has been registered.
+ *
+ * @param name the name of the ListAppender
+ * @return the named StringAppender or {@code null} if it does not exist
+ */
+ public static StringAppender getStringAppender(final String name) {
+ return ((StringAppender)
+ (LoggerContext.getContext(false)).getConfiguration().getAppender(name));
+ }
+
+ @Override
+ public String toString() {
+ return "StringAppender message=" + message;
+ }
+}
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextBenchmark.java
index 8b153e97670..a03708569d6 100644
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextBenchmark.java
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextBenchmark.java
@@ -29,7 +29,6 @@
import org.apache.logging.log4j.core.config.Property;
import org.apache.logging.log4j.core.impl.ContextDataInjectorFactory;
import org.apache.logging.log4j.perf.nogc.OpenHashStringMap;
-import org.apache.logging.log4j.spi.CopyOnWriteOpenHashMapThreadContextMap;
import org.apache.logging.log4j.spi.DefaultThreadContextMap;
import org.apache.logging.log4j.spi.GarbageFreeOpenHashMapThreadContextMap;
import org.apache.logging.log4j.spi.ThreadContextMap;
@@ -71,26 +70,19 @@
@State(Scope.Benchmark)
public class ThreadContextBenchmark {
private static final String DEFAULT_CONTEXT_MAP = "Default";
- private static final String COPY_OPENHASH_MAP = "CopyOpenHash";
- private static final String COPY_ARRAY_MAP = "CopySortedArray";
private static final String NO_GC_OPENHASH_MAP = "NoGcOpenHash";
private static final String NO_GC_ARRAY_MAP = "NoGcSortedArray";
private static final Map> IMPLEMENTATIONS = new HashMap<>();
static {
IMPLEMENTATIONS.put(DEFAULT_CONTEXT_MAP, DefaultThreadContextMap.class);
- IMPLEMENTATIONS.put(COPY_OPENHASH_MAP, CopyOnWriteOpenHashMapThreadContextMap.class);
- IMPLEMENTATIONS.put(
- COPY_ARRAY_MAP,
- CopyOnWriteOpenHashMapThreadContextMap.SUPER); // CopyOnWriteSortedArrayThreadContextMap.class);
IMPLEMENTATIONS.put(NO_GC_OPENHASH_MAP, GarbageFreeOpenHashMapThreadContextMap.class);
IMPLEMENTATIONS.put(
NO_GC_ARRAY_MAP,
GarbageFreeOpenHashMapThreadContextMap.SUPER); // GarbageFreeSortedArrayThreadContextMap.class);
}
- @Param({"Default", "CopyOpenHash", "CopySortedArray", "NoGcOpenHash", "NoGcSortedArray"})
- // @Param({ "Default", }) // for legecyInject benchmarks
+ @Param({"Default", "NoGcOpenHash", "NoGcSortedArray"})
public String threadContextMapAlias;
@Param({"5", "50", "500"})
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextBenchmark2.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextBenchmark2.java
new file mode 100644
index 00000000000..7d5c29e4c4b
--- /dev/null
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextBenchmark2.java
@@ -0,0 +1,264 @@
+/*
+ * 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.perf.jmh;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.apache.logging.log4j.CloseableThreadContext;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.LoggerConfig;
+import org.apache.logging.log4j.core.layout.PatternLayout;
+import org.apache.logging.log4j.perf.appender.StringAppender;
+import org.apache.logging.log4j.spi.DefaultThreadContextMap;
+import org.apache.logging.log4j.spi.GarbageFreeOpenHashMapThreadContextMap;
+import org.apache.logging.log4j.spi.ThreadContextMap;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.infra.Blackhole;
+
+/**
+ * Compares performance of ThreadContextMap implementations and ScopedContext
+ */
+// ============================== HOW TO RUN THIS TEST: ====================================
+// (Quick build: mvn -DskipTests=true clean package -pl log4j-perf -am )
+//
+// single thread:
+// java -jar log4j-perf/target/benchmarks.jar ".*ThreadContextBenchmark2.*"
+//
+// Usage help:
+// java -jar log4j-perf/target/benchmarks.jar -help
+//
+
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+public class ThreadContextBenchmark2 {
+
+ private static final String[] KEYS = new String[] {
+ "One",
+ "Two",
+ "Three",
+ "Four",
+ "Five",
+ "Six",
+ "Seven",
+ "Eight",
+ "Nine",
+ "Ten",
+ "Eleven",
+ "Twelve",
+ "Thriteen",
+ "Fourteen",
+ "Fifteen",
+ "Sixteen"
+ };
+ private static final String[] VALUES =
+ new String[] {"Alpha", "Beta", "Gamma", "Delta", "10", "100", "1000", "Hello"};
+ private static final String[] NESTED =
+ new String[] {Long.toString(System.currentTimeMillis()), "40", "Apache", "Logging"};
+
+ private static final int LOOP_COUNT = 100;
+
+ private static final Logger LOGGER = LogManager.getLogger(ThreadContextBenchmark2.class);
+
+ private static final String DEFAULT_CONTEXT_MAP = "Default";
+ private static final String NO_GC_OPENHASH_MAP = "NoGcOpenHash";
+ private static final String NO_GC_ARRAY_MAP = "NoGcSortedArray";
+ private static final Map> IMPLEMENTATIONS = new HashMap<>();
+
+ static {
+ IMPLEMENTATIONS.put(DEFAULT_CONTEXT_MAP, DefaultThreadContextMap.class);
+ IMPLEMENTATIONS.put(NO_GC_OPENHASH_MAP, GarbageFreeOpenHashMapThreadContextMap.class);
+ IMPLEMENTATIONS.put(
+ NO_GC_ARRAY_MAP,
+ GarbageFreeOpenHashMapThreadContextMap.SUPER); // GarbageFreeSortedArrayThreadContextMap.class);
+ }
+
+ @State(Scope.Benchmark)
+ public static class ReadThreadContextState {
+
+ @Param({"Default", "NoGcSortedArray"})
+ public String threadContextMapAlias;
+
+ @Setup
+ public void setup() {
+ System.setProperty(
+ "log4j2.threadContextMap",
+ IMPLEMENTATIONS.get(threadContextMapAlias).getName());
+ for (int i = 0; i < VALUES.length; i++) {
+ ThreadContext.put(KEYS[i], VALUES[i]);
+ }
+ }
+
+ @TearDown
+ public void teardown() {
+ ThreadContext.clearMap();
+ }
+ }
+
+ @State(Scope.Benchmark)
+ public static class ThreadContextState {
+
+ @Param({"Default", "CopySortedArray", "NoGcSortedArray", "StringArray"})
+ public String threadContextMapAlias;
+
+ @Setup
+ public void setup() {
+ System.setProperty(
+ "log4j2.threadContextMap",
+ IMPLEMENTATIONS.get(threadContextMapAlias).getName());
+ }
+
+ @TearDown
+ public void teardown() {
+ ThreadContext.clearMap();
+ }
+ }
+
+ @State(Scope.Benchmark)
+ public static class LogThreadContextState extends ReadThreadContextState {
+
+ public StringAppender appender;
+ public LoggerContext context;
+ public int counter;
+
+ @Setup
+ @Override
+ public void setup() {
+ super.setup();
+ context = (LoggerContext) LogManager.getContext(false);
+ Configuration config = context.getConfiguration();
+ PatternLayout layout = PatternLayout.newBuilder()
+ .withConfiguration(config)
+ .withPattern("%X %m%n")
+ .build();
+ appender = StringAppender.createAppender("String", layout, null);
+ appender.start();
+ config.getAppenders().forEach((name, app) -> app.stop());
+ config.getAppenders().clear();
+ config.addAppender(appender);
+ final LoggerConfig root = config.getRootLogger();
+ root.getAppenders().forEach((name, appender) -> {
+ root.removeAppender(name);
+ });
+ root.addAppender(appender, Level.DEBUG, null);
+ root.setLevel(Level.DEBUG);
+ context.updateLoggers();
+ }
+
+ @TearDown
+ public void teardown() {
+ System.out.println("Last entry: " + appender.getMessage());
+ context.stop();
+ counter = 0;
+ super.teardown();
+ }
+ }
+
+ @State(Scope.Benchmark)
+ public static class LogBaselineState {
+
+ public StringAppender appender;
+ public LoggerContext context;
+ public int counter;
+
+ @Setup
+ public void setup() {
+ context = (LoggerContext) LogManager.getContext(false);
+ Configuration config = context.getConfiguration();
+ PatternLayout layout = PatternLayout.newBuilder()
+ .withConfiguration(config)
+ .withPattern("%X %m%n")
+ .build();
+ appender = StringAppender.createAppender("String", layout, null);
+ appender.start();
+ config.getAppenders().forEach((name, app) -> app.stop());
+ config.getAppenders().clear();
+ config.addAppender(appender);
+ final LoggerConfig root = config.getRootLogger();
+ root.getAppenders().forEach((name, appender) -> {
+ root.removeAppender(name);
+ });
+ root.addAppender(appender, Level.DEBUG, null);
+ root.setLevel(Level.DEBUG);
+ context.updateLoggers();
+ }
+
+ @TearDown
+ public void teardown() {
+ System.out.println("Last entry: " + appender.getMessage());
+ context.stop();
+ counter = 0;
+ }
+ }
+
+ @Benchmark
+ public void populateThreadContext(final Blackhole blackhole, ThreadContextState state) {
+ for (int i = 0; i < VALUES.length; i++) {
+ ThreadContext.put(KEYS[i], VALUES[i]);
+ }
+ }
+
+ @Benchmark
+ public void threadContextMap(final Blackhole blackhole, ReadThreadContextState state) {
+ for (int i = 0; i < VALUES.length; i++) {
+ blackhole.consume(ThreadContext.get(KEYS[i]));
+ }
+ }
+
+ /*
+ * This is equivalent to the typical ScopedContext case.
+ */
+ @Benchmark
+ public void logThreadContextMap(final Blackhole blackhole, LogThreadContextState state) {
+ LOGGER.info("log count: {}", state.counter++);
+ }
+
+ @Benchmark
+ public void nestedThreadContextMap(final Blackhole blackhole, LogThreadContextState state) {
+ for (int i = 0; i < 10; ++i) {
+ LOGGER.info("outer log count: {}", i);
+ }
+ try (final CloseableThreadContext.Instance ignored = CloseableThreadContext.put(KEYS[8], NESTED[0])
+ .put(KEYS[9], NESTED[1])
+ .put(KEYS[10], NESTED[2])) {
+ for (int i = 0; i < 100; ++i) {
+ LOGGER.info("inner log count: {}", i);
+ }
+ }
+ }
+
+ /*
+ * Log the baseline - no context variables.
+ */
+ @Benchmark
+ public void logBaseline(final Blackhole blackhole, LogBaselineState state) {
+ LOGGER.info("log count: {}", state.counter++);
+ }
+}
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteOpenHashMapThreadContextMap.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteOpenHashMapThreadContextMap.java
deleted file mode 100644
index f43717494f9..00000000000
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteOpenHashMapThreadContextMap.java
+++ /dev/null
@@ -1,46 +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 org.apache.logging.log4j.perf.nogc.OpenHashStringMap;
-import org.apache.logging.log4j.util.PropertiesUtil;
-import org.apache.logging.log4j.util.ReadOnlyStringMap;
-import org.apache.logging.log4j.util.StringMap;
-
-/**
- * {@code OpenHashStringMap}-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 2.7
- */
-public class CopyOnWriteOpenHashMapThreadContextMap extends CopyOnWriteSortedArrayThreadContextMap {
-
- /** Constant used in benchmark code */
- public static final Class extends ThreadContextMap> SUPER = CopyOnWriteSortedArrayThreadContextMap.class;
-
- @Override
- protected StringMap createStringMap() {
- return new OpenHashStringMap<>(PropertiesUtil.getProperties()
- .getIntegerProperty(PROPERTY_NAME_INITIAL_CAPACITY, DEFAULT_INITIAL_CAPACITY));
- }
-
- @Override
- protected StringMap createStringMap(final ReadOnlyStringMap original) {
- return new OpenHashStringMap<>(original);
- }
-}
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/spi/GarbageFreeOpenHashMapThreadContextMap.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/spi/GarbageFreeOpenHashMapThreadContextMap.java
index 45283a2fb07..a39ce88abcf 100644
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/spi/GarbageFreeOpenHashMapThreadContextMap.java
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/spi/GarbageFreeOpenHashMapThreadContextMap.java
@@ -16,6 +16,7 @@
*/
package org.apache.logging.log4j.spi;
+import org.apache.logging.log4j.core.context.internal.GarbageFreeSortedArrayThreadContextMap;
import org.apache.logging.log4j.perf.nogc.OpenHashStringMap;
import org.apache.logging.log4j.util.PropertiesUtil;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
diff --git a/log4j-to-jul/pom.xml b/log4j-to-jul/pom.xml
index 0c4babfe547..25caed1639f 100644
--- a/log4j-to-jul/pom.xml
+++ b/log4j-to-jul/pom.xml
@@ -28,7 +28,20 @@
Apache Log4j to JUL BridgeThe Apache Log4j binding between Log4j 2 API and java.util.logging (JUL).2022
+
+
+
+
+ org.jspecify.*;resolution:=optional
+
+
+
+
+ org.jspecify
+ jspecify
+ provided
+ org.osgiorg.osgi.core
diff --git a/log4j-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULProvider.java b/log4j-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULProvider.java
index 0f371f43ef0..7497e484f85 100644
--- a/log4j-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULProvider.java
+++ b/log4j-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULProvider.java
@@ -19,19 +19,23 @@
import aQute.bnd.annotation.Resolution;
import aQute.bnd.annotation.spi.ServiceProvider;
import org.apache.logging.log4j.spi.LoggerContextFactory;
+import org.apache.logging.log4j.spi.NoOpThreadContextMap;
import org.apache.logging.log4j.spi.Provider;
+import org.apache.logging.log4j.spi.ThreadContextMap;
+import org.jspecify.annotations.NullMarked;
/**
* Bind the Log4j API to JUL.
*
* @author Michael Vorburger.ch for Google
*/
+@NullMarked
@ServiceProvider(value = Provider.class, resolution = Resolution.OPTIONAL)
public class JULProvider extends Provider {
private static final LoggerContextFactory CONTEXT_FACTORY = new JULLoggerContextFactory();
public JULProvider() {
- super(20, CURRENT_VERSION);
+ super(20, CURRENT_VERSION, JULLoggerContextFactory.class, NoOpThreadContextMap.class);
}
@Override
@@ -40,8 +44,8 @@ public LoggerContextFactory getLoggerContextFactory() {
}
@Override
- public String getThreadContextMap() {
+ public ThreadContextMap getThreadContextMapInstance() {
// JUL does not provide an MDC implementation
- return NO_OP_CONTEXT_MAP;
+ return NoOpThreadContextMap.INSTANCE;
}
}
diff --git a/log4j-to-slf4j/pom.xml b/log4j-to-slf4j/pom.xml
index c21a3262c1d..45907097dc5 100644
--- a/log4j-to-slf4j/pom.xml
+++ b/log4j-to-slf4j/pom.xml
@@ -40,6 +40,8 @@
-->
[1.7,3)
+
+ org.jspecify.*;resolution:=optional,
org.slf4j.*;version="${slf4j.support.range}"
@@ -63,6 +65,11 @@
org.osgi.coreprovided
+
+ org.jspecify
+ jspecify
+ provided
+ org.apache.logging.log4jlog4j-api
diff --git a/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JProvider.java b/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JProvider.java
index 318d7937f66..52a11a55603 100644
--- a/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JProvider.java
+++ b/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JProvider.java
@@ -21,10 +21,12 @@
import org.apache.logging.log4j.spi.LoggerContextFactory;
import org.apache.logging.log4j.spi.Provider;
import org.apache.logging.log4j.spi.ThreadContextMap;
+import org.jspecify.annotations.NullMarked;
/**
* Bind the Log4j API to SLF4J.
*/
+@NullMarked
@ServiceProvider(value = Provider.class, resolution = Resolution.OPTIONAL)
public class SLF4JProvider extends Provider {
@@ -32,7 +34,7 @@ public class SLF4JProvider extends Provider {
private static final ThreadContextMap THREAD_CONTEXT_MAP = new MDCContextMap();
public SLF4JProvider() {
- super(15, CURRENT_VERSION);
+ super(15, CURRENT_VERSION, SLF4JLoggerContextFactory.class, MDCContextMap.class);
}
@Override
diff --git a/src/changelog/.2.x.x/2330_add_faster_web_app_context_map.xml b/src/changelog/.2.x.x/2330_add_faster_web_app_context_map.xml
index 34c1bd653da..f3076419d0b 100644
--- a/src/changelog/.2.x.x/2330_add_faster_web_app_context_map.xml
+++ b/src/changelog/.2.x.x/2330_add_faster_web_app_context_map.xml
@@ -4,5 +4,5 @@
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="added">
- Add a faster `ThreadContextMap` for web app users: `org.apache.logging.log4j.internal.map.StringArrayThreadContextMap`.
+ Add a faster `DefaultThreadContextMap` implementation.
diff --git a/src/site/antora/modules/ROOT/pages/manual/garbagefree.adoc b/src/site/antora/modules/ROOT/pages/manual/garbagefree.adoc
index 5aed3980676..fb6cba10f78 100644
--- a/src/site/antora/modules/ROOT/pages/manual/garbagefree.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/garbagefree.adoc
@@ -69,7 +69,7 @@ include::partial$manual/systemproperties/properties-meta.adoc[leveloffset=+2]
include::partial$manual/systemproperties/properties-garbage-collection.adoc[leveloffset=+2]
-include::partial$manual/systemproperties/properties-thread-context.adoc[leveloffset=+2,tag=gcfree]
+include::partial$manual/systemproperties/properties-thread-context-core.adoc[leveloffset=+2,tag=gcfree]
[#Layouts]
=== Layouts
diff --git a/src/site/antora/modules/ROOT/pages/manual/simple-logger.adoc b/src/site/antora/modules/ROOT/pages/manual/simple-logger.adoc
index e9f8f7370fb..15325abbb2c 100644
--- a/src/site/antora/modules/ROOT/pages/manual/simple-logger.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/simple-logger.adoc
@@ -24,11 +24,17 @@ This is a convenience for environments where either a fully-fledged logging impl
[#config]
== Configuration
+[#logger]
+=== Logger
+
`SimpleLogger` can be configured using the following system properties:
-include::partial$manual/systemproperties/properties-simple-logger.adoc[leveloffset=+1]
+include::partial$manual/systemproperties/properties-simple-logger.adoc[leveloffset=+2]
+[#thread-context]
=== Thread context
-Simple Logger supports the same properties as Log4j Core for the configuration of the thread context.
-See xref:manual/systemproperties.adoc#properties-thread-context[] for details.
+For the configuration of the thread context,
+Simple Logger supports a subset of the properties supported by Log4j Core:
+
+include::partial$manual/systemproperties/properties-thread-context-simple-logger.adoc[leveloffset=+2]
\ No newline at end of file
diff --git a/src/site/antora/modules/ROOT/pages/manual/systemproperties.adoc b/src/site/antora/modules/ROOT/pages/manual/systemproperties.adoc
index ffa0e1e21b1..e72136de40f 100644
--- a/src/site/antora/modules/ROOT/pages/manual/systemproperties.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/systemproperties.adoc
@@ -184,7 +184,7 @@ The `log4j-to-slf4j` logging bridge delegates `ThreadContext` calls to {slf4j-ur
The `log4j-to-jul` logging bridge ignores all `ThreadContext` method calls.
====
-include::partial$manual/systemproperties/properties-thread-context.adoc[leveloffset=+2]
+include::partial$manual/systemproperties/properties-thread-context-core.adoc[leveloffset=+2]
[id=properties-transport-security]
=== Transport security
diff --git a/src/site/antora/modules/ROOT/pages/manual/thread-context.adoc b/src/site/antora/modules/ROOT/pages/manual/thread-context.adoc
index c22a14a3961..31f34849db7 100644
--- a/src/site/antora/modules/ROOT/pages/manual/thread-context.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/thread-context.adoc
@@ -120,9 +120,25 @@ executor.submit(() -> {
[#config]
== Configuration
-You can configure thread context using following properties:
+Since the thread context is inherently linked to the logging implementation, its configuration options depend on the logging implementation used:
-include::partial$manual/systemproperties/properties-thread-context.adoc[leveloffset=+1]
+Simple Logger::
++
+See xref:manual/simple-logger.adoc#thread-context[Thread context configuration of Simple Logger].
+
+Log4j Core::
++
+See xref:manual/systemproperties.adoc#properties-thread-context[Thread context configuration of Log4j Core].
+
+Log4j API to SLF4J bridge::
++
+All `ThreadContext` method calls are translated into equivalent
+https://www.slf4j.org/api/org/slf4j/MDC.html[`org.slf4j.MDC`]
+method calls.
+
+JUL::
++
+All `ThreadContext` method calls are a no-op.
[#extending]
== Extending
diff --git a/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-thread-context.adoc b/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-thread-context-core.adoc
similarity index 95%
rename from src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-thread-context.adoc
rename to src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-thread-context-core.adoc
index 684974cb8df..6dec0400e26 100644
--- a/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-thread-context.adoc
+++ b/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-thread-context-core.adoc
@@ -67,16 +67,14 @@ or predefined constant
| Default value
| `WebApp`
-(GC-free mode: `CopyOnWrite`)
|===
Fully specified class name of a custom
link:../javadoc/log4j-api/org/apache/logging/log4j/spi/ThreadContextMap.html[`ThreadContextMap`]
-implementation class or one of the predefined constants:
+implementation class or (since version `2.24.0`) one of the predefined constants:
NoOp:: to disable the thread context,
WebApp:: a web application-safe implementation, that only binds JRE classes to `ThreadLocal` to prevent memory leaks,
-CopyOnWrite:: a copy-on-write implementation,
GarbageFree:: a garbage-free implementation.
// end::gcfree[]
diff --git a/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-thread-context-simple-logger.adoc b/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-thread-context-simple-logger.adoc
new file mode 100644
index 00000000000..35a9060b47d
--- /dev/null
+++ b/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-thread-context-simple-logger.adoc
@@ -0,0 +1,87 @@
+////
+ 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.
+////
+[id=log4j2.disableThreadContext]
+== `log4j2.disableThreadContext`
+
+[cols="1h,5"]
+|===
+| Env. variable | `LOG4J_DISABLE_THREAD_CONTEXT`
+| Type | `boolean`
+| Default value | `false`
+|===
+
+If `true`, the `ThreadContext` stack and map are disabled.
+
+[id=log4j2.disableThreadContextStack]
+== `log4j2.disableThreadContextStack`
+
+[cols="1h,5"]
+|===
+| Env. variable | `LOG4J_DISABLE_THREAD_CONTEXT_STACK`
+| Type | `boolean`
+| Default value | `false`
+|===
+
+If `true`, the `ThreadContext` stack is disabled.
+
+[id=log4j2.disableThreadContextMap]
+== `log4j2.disableThreadContextMap`
+
+[cols="1h,5"]
+|===
+| Env. variable | `LOG4J_DISABLE_THREAD_CONTEXT_MAP`
+| Type | `boolean`
+| Default value | `false`
+|===
+
+If `true`, the `ThreadContext` map is disabled.
+
+[id=log4j2.threadContextMap]
+== `log4j2.threadContextMap`
+
+[cols="1h,5"]
+|===
+| Env. variable
+| `LOG4J_THREAD_CONTEXT_MAP`
+
+| Type
+| link:../javadoc/log4j-api/org/apache/logging/log4j/spi/ThreadContextMap.html[`Class extends ThreadContextMap>`]
+
+| Default value
+| link:../javadoc/log4j-api/org/apache/logging/log4j/spi/DefaultThreadContextMap.html[`DefaultThreadContextMap`]
+
+|===
+
+Fully specified class name of a custom
+link:../javadoc/log4j-api/org/apache/logging/log4j/spi/ThreadContextMap.html[`ThreadContextMap`]
+implementation class.
+
+[id=isThreadContextMapInheritable]
+== `log4j2.isThreadContextMapInheritable`
+
+[cols="1h,5"]
+|===
+| Env. variable | `LOG4J_IS_THREAD_CONTEXT_MAP_INHERITABLE`
+| Type | `boolean`
+| Default value | `false`
+|===
+
+If `true` uses an `InheritableThreadLocal` to copy the thread context map to newly created threads.
+
+Note that, as explained in
+https://docs.oracle.com/javase/{java-target-version}/docs/api/java/util/concurrent/Executors.html#privilegedThreadFactory()[Java's `Executors#privilegedThreadFactory()`], when you are dealing with _privileged threads_, thread context might not get propagated completely.
+