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.assertj assertj-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.osgi org.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 threadLocalMapState; - - public StringArrayThreadContextMap() { - threadLocalMapState = new ThreadLocal<>(); - } - - @Override - public void put(final String key, final String value) { - final Object[] state = threadLocalMapState.get(); - final UnmodifiableArrayBackedMap modifiedMap = - UnmodifiableArrayBackedMap.getInstance(state).copyAndPut(key, value); - threadLocalMapState.set(modifiedMap.getBackingArray()); - } - - public void putAll(final Map m) { - final Object[] state = threadLocalMapState.get(); - final UnmodifiableArrayBackedMap modifiedMap = - UnmodifiableArrayBackedMap.getInstance(state).copyAndPutAll(m); - threadLocalMapState.set(modifiedMap.getBackingArray()); - } - - @Override - public String get(final String key) { - final Object[] state = threadLocalMapState.get(); - if (state == null) { - return null; - } - return UnmodifiableArrayBackedMap.getInstance(state).get(key); - } - - @Override - public void remove(final String key) { - final Object[] state = threadLocalMapState.get(); - if (state != null) { - final UnmodifiableArrayBackedMap modifiedMap = - UnmodifiableArrayBackedMap.getInstance(state).copyAndRemove(key); - threadLocalMapState.set(modifiedMap.getBackingArray()); - } - } - - public void removeAll(final Iterable keys) { - final Object[] state = threadLocalMapState.get(); - if (state != null) { - final UnmodifiableArrayBackedMap modifiedMap = - UnmodifiableArrayBackedMap.getInstance(state).copyAndRemoveAll(keys); - threadLocalMapState.set(modifiedMap.getBackingArray()); - } - } - - @Override - public void clear() { - threadLocalMapState.remove(); - } - - @Override - public Map toMap() { - return getCopy(); - } - - @Override - public boolean containsKey(final String key) { - final Object[] state = threadLocalMapState.get(); - return (state == null ? false : (UnmodifiableArrayBackedMap.getInstance(state)).containsKey(key)); - } - - @Override - public void forEach(final BiConsumer action) { - final Object[] state = threadLocalMapState.get(); - if (state == null) { - return; - } - final UnmodifiableArrayBackedMap map = UnmodifiableArrayBackedMap.getInstance(state); - map.forEach(action); - } - - @Override - public void forEach(final TriConsumer action, final S state) { - final Object[] localState = threadLocalMapState.get(); - if (localState == null) { - return; - } - final UnmodifiableArrayBackedMap map = UnmodifiableArrayBackedMap.getInstance(localState); - map.forEach(action, state); - } - - @SuppressWarnings("unchecked") - @Override - public V getValue(final String key) { - return (V) get(key); - } - - @Override - public Map getCopy() { - final Object[] state = threadLocalMapState.get(); - if (state == null) { - return new HashMap<>(0); - } - return new HashMap<>(UnmodifiableArrayBackedMap.getInstance(state)); - } - - @Override - public Map getImmutableMapOrNull() { - final Object[] state = threadLocalMapState.get(); - return (state == null ? null : UnmodifiableArrayBackedMap.getInstance(state)); - } - - @Override - public boolean isEmpty() { - return (size() == 0); - } - - @Override - public int size() { - final Object[] state = threadLocalMapState.get(); - return UnmodifiableArrayBackedMap.getInstance(state).size(); - } - - @Override - public String toString() { - final Object[] state = threadLocalMapState.get(); - return state == null - ? "{}" - : UnmodifiableArrayBackedMap.getInstance(state).toString(); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - final Object[] state = threadLocalMapState.get(); - result = prime * result - + ((state == null) - ? 0 - : UnmodifiableArrayBackedMap.getInstance(state).hashCode()); - return result; - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof ThreadContextMap)) { - return false; - } - final ThreadContextMap other = (ThreadContextMap) obj; - final Map map = UnmodifiableArrayBackedMap.getInstance(this.threadLocalMapState.get()); - final Map otherMap = other.getImmutableMapOrNull(); - return Objects.equals(map, otherMap); - } -} diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMap.java index 84d2868b911..cab4dc2eb71 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMap.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMap.java @@ -57,7 +57,7 @@ * * */ -class UnmodifiableArrayBackedMap extends AbstractMap implements Serializable, ReadOnlyStringMap { +public class UnmodifiableArrayBackedMap extends AbstractMap implements Serializable, ReadOnlyStringMap { /** * Implementation of Map.Entry. The implementation is simple since each instance * contains an index in the array, then getKey() and getValue() retrieve from @@ -160,7 +160,7 @@ private static int getArrayIndexForValue(int entryIndex) { return 2 * entryIndex + 1 + NUM_FIXED_ARRAY_ENTRIES; } - static UnmodifiableArrayBackedMap getInstance(Object[] backingArray) { + public static UnmodifiableArrayBackedMap getMap(Object[] backingArray) { if (backingArray == null || backingArray.length == 1) { return EMPTY_MAP; } else { @@ -224,7 +224,7 @@ public boolean containsKey(String key) { return false; } - Object[] getBackingArray() { + public Object[] getBackingArray() { return backingArray; } @@ -255,7 +255,7 @@ public boolean containsValue(Object value) { * @param value * @return */ - UnmodifiableArrayBackedMap copyAndPut(String key, String value) { + public UnmodifiableArrayBackedMap copyAndPut(String key, String value) { UnmodifiableArrayBackedMap newMap = new UnmodifiableArrayBackedMap(numEntries + 1); // include the numEntries value (array index 0) if (this.numEntries > 0) { @@ -275,7 +275,7 @@ UnmodifiableArrayBackedMap copyAndPut(String key, String value) { * @param value * @return */ - UnmodifiableArrayBackedMap copyAndPutAll(Map entriesToAdd) { + public UnmodifiableArrayBackedMap copyAndPutAll(Map entriesToAdd) { // create a new array that can hold the maximum output size UnmodifiableArrayBackedMap newMap = new UnmodifiableArrayBackedMap(numEntries + entriesToAdd.size()); @@ -309,9 +309,9 @@ UnmodifiableArrayBackedMap copyAndPutAll(Map entriesToAdd) { * @param value * @return */ - UnmodifiableArrayBackedMap copyAndRemove(String key) { + public UnmodifiableArrayBackedMap copyAndRemove(String key) { int indexToRemove = -1; - for (int oldIndex = 0; oldIndex < numEntries; oldIndex++) { + for (int oldIndex = numEntries - 1; oldIndex >= 0; oldIndex--) { if (backingArray[getArrayIndexForKey(oldIndex)].hashCode() == key.hashCode() && backingArray[getArrayIndexForKey(oldIndex)].equals(key)) { indexToRemove = oldIndex; @@ -356,7 +356,7 @@ UnmodifiableArrayBackedMap copyAndRemove(String key) { * @param value * @return */ - UnmodifiableArrayBackedMap copyAndRemoveAll(Iterable keysToRemoveIterable) { + public UnmodifiableArrayBackedMap copyAndRemoveAll(Iterable keysToRemoveIterable) { if (isEmpty()) { // shortcut: if this map is empty, the result will continue to be empty return EMPTY_MAP; diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWrite.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWrite.java index 4f969c2e467..ff958f1516f 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWrite.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWrite.java @@ -21,5 +21,7 @@ * * @see ReadOnlyThreadContextMap#getReadOnlyContextData() * @since 2.7 + * @deprecated Since 2.24.0 no class implements this. */ +@Deprecated public interface CopyOnWrite {} diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java deleted file mode 100644 index ca9f501c217..00000000000 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to you under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.logging.log4j.spi; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import org.apache.logging.log4j.util.PropertiesUtil; -import org.apache.logging.log4j.util.ReadOnlyStringMap; -import org.apache.logging.log4j.util.SortedArrayStringMap; -import org.apache.logging.log4j.util.StringMap; - -/** - * {@code SortedArrayStringMap}-based implementation of the {@code ThreadContextMap} interface that creates a copy of - * the data structure on every modification. Any particular instance of the data structure is a snapshot of the - * ThreadContext at some point in time and can safely be passed off to other threads. Since it is - * expected that the Map will be passed to many more log events than the number of keys it contains the performance - * should be much better than if the Map was copied for each event. - * - * @since 2.7 - */ -class CopyOnWriteSortedArrayThreadContextMap implements ReadOnlyThreadContextMap, ObjectThreadContextMap, CopyOnWrite { - - /** - * Property name ({@value} ) for selecting {@code InheritableThreadLocal} (value "true") or plain - * {@code ThreadLocal} (value is not "true") in the implementation. - */ - public static final String INHERITABLE_MAP = "isThreadContextMapInheritable"; - - /** - * The default initial capacity. - */ - protected static final int DEFAULT_INITIAL_CAPACITY = 16; - - /** - * System property name that can be used to control the data structure's initial capacity. - */ - protected static final String PROPERTY_NAME_INITIAL_CAPACITY = "log4j2.ThreadContext.initial.capacity"; - - private static final StringMap EMPTY_CONTEXT_DATA = new SortedArrayStringMap(1); - - static { - EMPTY_CONTEXT_DATA.freeze(); - } - - private final int initialCapacity; - private final ThreadLocal localMap; - - public CopyOnWriteSortedArrayThreadContextMap() { - this(PropertiesUtil.getProperties()); - } - - CopyOnWriteSortedArrayThreadContextMap(final PropertiesUtil properties) { - initialCapacity = properties.getIntegerProperty(PROPERTY_NAME_INITIAL_CAPACITY, DEFAULT_INITIAL_CAPACITY); - localMap = properties.getBooleanProperty(INHERITABLE_MAP) - ? new InheritableThreadLocal() { - @Override - protected StringMap childValue(final StringMap parentValue) { - if (parentValue == null) { - return null; - } - final StringMap stringMap = createStringMap(parentValue); - stringMap.freeze(); - return stringMap; - } - } - : new ThreadLocal(); - } - - /** - * Returns an implementation of the {@code StringMap} used to back this thread context map. - *

- * Subclasses may override. - *

- * @return an implementation of the {@code StringMap} used to back this thread context map - */ - protected StringMap createStringMap() { - return new SortedArrayStringMap(initialCapacity); - } - - /** - * Returns an implementation of the {@code StringMap} used to back this thread context map, pre-populated - * with the contents of the specified context data. - *

- * Subclasses may override. - *

- * @param original the key-value pairs to initialize the returned context data with - * @return an implementation of the {@code StringMap} used to back this thread context map - */ - protected StringMap createStringMap(final ReadOnlyStringMap original) { - return new SortedArrayStringMap(original); - } - - @Override - public void put(final String key, final String value) { - putValue(key, value); - } - - @Override - public void putValue(final String key, final Object value) { - StringMap map = localMap.get(); - map = map == null ? createStringMap() : createStringMap(map); - map.putValue(key, value); - map.freeze(); - localMap.set(map); - } - - @Override - public void putAll(final Map values) { - if (values == null || values.isEmpty()) { - return; - } - StringMap map = localMap.get(); - map = map == null ? createStringMap() : createStringMap(map); - for (final Map.Entry entry : values.entrySet()) { - map.putValue(entry.getKey(), entry.getValue()); - } - map.freeze(); - localMap.set(map); - } - - @Override - public void putAllValues(final Map values) { - if (values == null || values.isEmpty()) { - return; - } - StringMap map = localMap.get(); - map = map == null ? createStringMap() : createStringMap(map); - for (final Map.Entry entry : values.entrySet()) { - map.putValue(entry.getKey(), entry.getValue()); - } - map.freeze(); - localMap.set(map); - } - - @Override - public String get(final String key) { - return (String) getValue(key); - } - - @Override - public V getValue(final String key) { - final StringMap map = localMap.get(); - return map == null ? null : map.getValue(key); - } - - @Override - public void remove(final String key) { - final StringMap map = localMap.get(); - if (map != null) { - final StringMap copy = createStringMap(map); - copy.remove(key); - copy.freeze(); - localMap.set(copy); - } - } - - @Override - public void removeAll(final Iterable keys) { - final StringMap map = localMap.get(); - if (map != null) { - final StringMap copy = createStringMap(map); - for (final String key : keys) { - copy.remove(key); - } - copy.freeze(); - localMap.set(copy); - } - } - - @Override - public void clear() { - localMap.remove(); - } - - @Override - public boolean containsKey(final String key) { - final StringMap map = localMap.get(); - return map != null && map.containsKey(key); - } - - @Override - public Map getCopy() { - final StringMap map = localMap.get(); - return map == null ? new HashMap<>() : map.toMap(); - } - - /** - * {@inheritDoc} - */ - @Override - public StringMap getReadOnlyContextData() { - final StringMap map = localMap.get(); - return map == null ? EMPTY_CONTEXT_DATA : map; - } - - @Override - public Map getImmutableMapOrNull() { - final StringMap map = localMap.get(); - return map == null ? null : Collections.unmodifiableMap(map.toMap()); - } - - @Override - public boolean isEmpty() { - final StringMap map = localMap.get(); - return map == null || map.isEmpty(); - } - - @Override - public String toString() { - final StringMap map = localMap.get(); - return map == null ? "{}" : map.toString(); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - final StringMap map = this.localMap.get(); - result = prime * result + ((map == null) ? 0 : map.hashCode()); - return result; - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof ThreadContextMap)) { - return false; - } - final ThreadContextMap other = (ThreadContextMap) obj; - final Map map = this.getImmutableMapOrNull(); - final Map otherMap = other.getImmutableMapOrNull(); - return Objects.equals(map, otherMap); - } -} diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java index a07992d371f..471dba72fda 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java @@ -16,7 +16,8 @@ */ package org.apache.logging.log4j.spi; -import java.util.Collections; +import static org.apache.logging.log4j.internal.map.UnmodifiableArrayBackedMap.getMap; + import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -26,102 +27,86 @@ import org.apache.logging.log4j.util.TriConsumer; /** - * The actual ThreadContext Map. A new ThreadContext Map is created each time it is updated and the Map stored is always - * immutable. This means the Map can be passed to other threads without concern that it will be updated. Since it is - * expected that the Map will be passed to many more log events than the number of keys it contains the performance - * should be much better than if the Map was copied for each event. + * The default implementation of {@link ThreadContextMap} + *

+ * An instance of UnmodifiableArrayBackedMap can be represented as a single {@code Object[]}, which can safely + * be stored on the {@code ThreadLocal} with no fear of classloader-related memory leaks. + *

+ *

+ * Performance of the underlying {@link org.apache.logging.log4j.internal.map.UnmodifiableArrayBackedMap} exceeds + * {@link HashMap} in all supported operations other than {@code get()}. Note that {@code get()} performance scales + * linearly with the current map size, and callers are advised to minimize this work. + *

*/ public class DefaultThreadContextMap implements ThreadContextMap, ReadOnlyStringMap { - private static final long serialVersionUID = 8218007901108944053L; + private static final long serialVersionUID = -2635197170958057849L; /** - * Property name ({@value} ) for selecting {@code InheritableThreadLocal} (value "true") or plain + * 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 final boolean useMap; - private final ThreadLocal> localMap; + private ThreadLocal localState; public DefaultThreadContextMap() { - this(true); + this(PropertiesUtil.getProperties()); } /** - * @deprecated Since 2.24.0. See {@link Provider#getThreadContextMap()} on how to obtain a no-op map. + * @deprecated Since 2.24.0. Use {@link NoOpThreadContextMap} for a no-op implementation. */ @Deprecated - public DefaultThreadContextMap(final boolean useMap) { - this(useMap, PropertiesUtil.getProperties()); + public DefaultThreadContextMap(final boolean ignored) { + this(PropertiesUtil.getProperties()); } - DefaultThreadContextMap(final boolean useMap, final PropertiesUtil properties) { - this.useMap = useMap; - localMap = properties.getBooleanProperty(INHERITABLE_MAP) - ? new InheritableThreadLocal>() { + DefaultThreadContextMap(final PropertiesUtil properties) { + localState = properties.getBooleanProperty(INHERITABLE_MAP) + ? new InheritableThreadLocal() { @Override - protected Map childValue(final Map parentValue) { - return parentValue != null && useMap - ? Collections.unmodifiableMap(new HashMap<>(parentValue)) - : null; + protected Object[] childValue(final Object[] parentValue) { + return parentValue; } } - : new ThreadLocal>(); + : new ThreadLocal<>(); } @Override public void put(final String key, final String value) { - if (!useMap) { - return; - } - Map map = localMap.get(); - map = map == null ? new HashMap<>(1) : new HashMap<>(map); - map.put(key, value); - localMap.set(Collections.unmodifiableMap(map)); + final Object[] state = localState.get(); + localState.set(getMap(state).copyAndPut(key, value).getBackingArray()); } public void putAll(final Map m) { - if (!useMap) { - return; - } - Map map = localMap.get(); - map = map == null ? new HashMap<>(m.size()) : new HashMap<>(map); - for (final Map.Entry e : m.entrySet()) { - map.put(e.getKey(), e.getValue()); - } - localMap.set(Collections.unmodifiableMap(map)); + final Object[] state = localState.get(); + localState.set(getMap(state).copyAndPutAll(m).getBackingArray()); } @Override public String get(final String key) { - final Map map = localMap.get(); - return map == null ? null : map.get(key); + final Object[] state = localState.get(); + return state == null ? null : getMap(state).get(key); } @Override public void remove(final String key) { - final Map map = localMap.get(); - if (map != null) { - final Map copy = new HashMap<>(map); - copy.remove(key); - localMap.set(Collections.unmodifiableMap(copy)); + final Object[] state = localState.get(); + if (state != null) { + localState.set(getMap(state).copyAndRemove(key).getBackingArray()); } } public void removeAll(final Iterable keys) { - final Map map = localMap.get(); - if (map != null) { - final Map copy = new HashMap<>(map); - for (final String key : keys) { - copy.remove(key); - } - localMap.set(Collections.unmodifiableMap(copy)); + final Object[] state = localState.get(); + if (state != null) { + localState.set(getMap(state).copyAndRemoveAll(keys).getBackingArray()); } } @Override public void clear() { - localMap.remove(); + localState.remove(); } @Override @@ -131,81 +116,72 @@ public Map toMap() { @Override public boolean containsKey(final String key) { - final Map map = localMap.get(); - return map != null && map.containsKey(key); + final Object[] state = localState.get(); + return state != null && getMap(state).containsKey(key); } @Override public void forEach(final BiConsumer action) { - final Map map = localMap.get(); - if (map == null) { + final Object[] state = localState.get(); + if (state == null) { return; } - for (final Map.Entry entry : map.entrySet()) { - // BiConsumer should be able to handle values of any type V. In our case the values are of type String. - @SuppressWarnings("unchecked") - final V value = (V) entry.getValue(); - action.accept(entry.getKey(), value); - } + getMap(state).forEach(action); } @Override public void forEach(final TriConsumer action, final S state) { - final Map map = localMap.get(); - if (map == null) { + final Object[] localState = this.localState.get(); + if (localState == null) { return; } - for (final Map.Entry entry : map.entrySet()) { - // TriConsumer should be able to handle values of any type V. In our case the values are of type String. - @SuppressWarnings("unchecked") - final V value = (V) entry.getValue(); - action.accept(entry.getKey(), value, state); - } + getMap(localState).forEach(action, state); } @SuppressWarnings("unchecked") @Override public V getValue(final String key) { - final Map map = localMap.get(); - return (V) (map == null ? null : map.get(key)); + return (V) get(key); } @Override public Map getCopy() { - final Map map = localMap.get(); - return map == null ? new HashMap<>() : new HashMap<>(map); + final Object[] state = localState.get(); + if (state == null) { + return new HashMap<>(0); + } + return new HashMap<>(getMap(state)); } @Override public Map getImmutableMapOrNull() { - return localMap.get(); + final Object[] state = localState.get(); + return (state == null ? null : getMap(state)); } @Override public boolean isEmpty() { - final Map map = localMap.get(); - return map == null || map.isEmpty(); + return size() == 0; } @Override public int size() { - final Map map = localMap.get(); - return map == null ? 0 : map.size(); + final Object[] state = localState.get(); + return getMap(state).size(); } @Override public String toString() { - final Map map = localMap.get(); - return map == null ? "{}" : map.toString(); + final Object[] state = localState.get(); + return state == null ? "{}" : getMap(state).toString(); } @Override public int hashCode() { final int prime = 31; int result = 1; - final Map map = this.localMap.get(); - result = prime * result + ((map == null) ? 0 : map.hashCode()); - result = prime * result + Boolean.valueOf(this.useMap).hashCode(); + final Object[] state = localState.get(); + result = prime * result + ((state == null) ? 0 : getMap(state).hashCode()); return result; } @@ -217,17 +193,11 @@ public boolean equals(final Object obj) { if (obj == null) { return false; } - if (obj instanceof DefaultThreadContextMap) { - final DefaultThreadContextMap other = (DefaultThreadContextMap) obj; - if (this.useMap != other.useMap) { - return false; - } - } if (!(obj instanceof ThreadContextMap)) { return false; } final ThreadContextMap other = (ThreadContextMap) obj; - final Map map = this.localMap.get(); + final Map map = getMap(localState.get()); final Map otherMap = other.getImmutableMapOrNull(); return Objects.equals(map, otherMap); } diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/NoOpThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/NoOpThreadContextMap.java index e79626f25a9..6de97d6235a 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/NoOpThreadContextMap.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/NoOpThreadContextMap.java @@ -18,17 +18,23 @@ import java.util.HashMap; import java.util.Map; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * {@code ThreadContextMap} implementation used when either of system properties {@code disableThreadContextMap} or . * {@code disableThreadContext} is {@code true}. This implementation does nothing. * * @since 2.7 - * @deprecated since 2.24.0. Return the {@value Provider#NO_OP_CONTEXT_MAP} constant in - * {@link Provider#getThreadContextMap()} instead. */ -@Deprecated +@NullMarked public class NoOpThreadContextMap implements ThreadContextMap { + + /** + * @since 2.24.0 + */ + public static final ThreadContextMap INSTANCE = new NoOpThreadContextMap(); + @Override public void clear() {} @@ -38,7 +44,7 @@ public boolean containsKey(final String key) { } @Override - public String get(final String key) { + public @Nullable String get(final String key) { return null; } @@ -48,7 +54,7 @@ public Map getCopy() { } @Override - public Map getImmutableMapOrNull() { + public @Nullable Map getImmutableMapOrNull() { return null; } diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java index 4b82c9b9cca..4f15843e284 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java @@ -23,10 +23,10 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.simple.SimpleLoggerContextFactory; import org.apache.logging.log4j.status.StatusLogger; -import org.apache.logging.log4j.util.Constants; -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; +import org.jspecify.annotations.Nullable; /** * Service class used to bind the Log4j API with an implementation. @@ -39,6 +39,7 @@ * be dropped in a future version. *

*/ +@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 loggerContextFactoryClass; - private final Lazy loggerContextFactoryLazy = Lazy.lazy(this::createLoggerContextFactory); + private final @Nullable Class loggerContextFactoryClass; // ThreadContextMap @Deprecated - private final String threadContextMap; + private final @Nullable String threadContextMap; - private final Class threadContextMapClass; - private final Lazy threadContextMapLazy = Lazy.lazy(this::createThreadContextMap); - private final String versions; + private final @Nullable Class 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 loggerContextFactoryClass) { + final @Nullable Class 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 loggerContextFactoryClass, - final Class threadContextMapClass) { + final @Nullable Class loggerContextFactoryClass, + final @Nullable Class 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 loadLoggerContextFactory() { + public @Nullable Class loadLoggerContextFactory() { if (loggerContextFactoryClass != null) { return loggerContextFactoryClass; } @@ -271,65 +223,30 @@ public Class loadLoggerContextFactory() { return null; } - private LoggerContextFactory createLoggerContextFactory() { - final Class factoryClass = loadLoggerContextFactory(); - if (factoryClass != null) { - try { - return LoaderUtil.newInstanceOf(factoryClass); - } catch (final Exception e) { - LOGGER.error( - "Unable to create instance of class {} specified in {}", factoryClass.getName(), getUrl(), e); - } - } - LOGGER.warn("Falling back to {}", SimpleLoggerContextFactory.INSTANCE); - return SimpleLoggerContextFactory.INSTANCE; - } - /** * @return The logger context factory to be used by {@link org.apache.logging.log4j.LogManager}. * @since 2.24.0 */ public LoggerContextFactory getLoggerContextFactory() { - return loggerContextFactoryLazy.get(); + final Class implementation = loadLoggerContextFactory(); + if (implementation != null) { + try { + return LoaderUtil.newInstanceOf(implementation); + } 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: - *

    - *
  1. {@code null} if {@link #loadThreadContextMap} is implemented,
  2. - *
  3. {@link #NO_OP_CONTEXT_MAP},
  4. - *
  5. {@link #WEB_APP_CONTEXT_MAP},
  6. - *
  7. {@link #COPY_ON_WRITE_CONTEXT_MAP},
  8. - *
  9. {@link #GARBAGE_FREE_CONTEXT_MAP}.
  10. - *
- *

+ * 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 loadThreadContextMap() { + public @Nullable Class loadThreadContextMap() { if (threadContextMapClass != null) { return threadContextMapClass; } @@ -362,74 +278,28 @@ public Class 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: - *
    - *
  1. calls {@link #loadThreadContextMap},
  2. - *
  3. if the previous call returns {@code null}, it calls {@link #getThreadContextMap} to instantiate one of - * the internal implementations,
  4. - *
  5. it returns a no-op map otherwise.
  6. - *
- */ - @SuppressWarnings("deprecation") - ThreadContextMap createThreadContextMap() { - final Class 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 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 @@ provided true + + 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 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 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 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 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 Bridge The Apache Log4j binding between Log4j 2 API and java.util.logging (JUL). 2022 + + + + + org.jspecify.*;resolution:=optional + + + + + org.jspecify + jspecify + provided + org.osgi org.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.core provided + + org.jspecify + jspecify + provided + org.apache.logging.log4j log4j-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`] + +| 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. +