This is an automated email from the ASF dual-hosted git repository. pkarwasz pushed a commit to branch feature/move-thread-context in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit f61d9fdee5abc3f02b534c8f04f938e2fc465bcd Author: Piotr P. Karwasz <[email protected]> AuthorDate: Fri Jun 21 16:53:26 2024 +0200 Make `StringArray` the default thread context map --- .../log4j/ThreadContextInheritanceTest.java | 28 -- .../map/StringArrayThreadContextMapTest.java | 287 --------------------- .../map/UnmodifiableArrayBackedMapTest.java | 6 +- .../log4j/spi/DefaultThreadContextMapTest.java | 12 - .../logging/log4j/spi/ThreadContextMapTest.java | 2 +- log4j-api/pom.xml | 11 +- .../org/apache/logging/log4j/ThreadContext.java | 5 - .../internal/map/StringArrayThreadContextMap.java | 199 -------------- .../internal/map/UnmodifiableArrayBackedMap.java | 14 +- .../logging/log4j/spi/DefaultThreadContextMap.java | 150 +++++------ .../logging/log4j/spi/NoOpThreadContextMap.java | 16 +- .../org/apache/logging/log4j/spi/Provider.java | 256 +++++------------- .../logging/log4j/spi/ThreadContextMapFactory.java | 4 +- .../async/AbstractAsyncThreadContextTestBase.java | 69 ++--- .../core/impl/ThreadContextDataInjectorTest.java | 40 ++- .../{ => core/impl}/ThreadContextTestAccess.java | 7 +- log4j-core/pom.xml | 7 + .../logging/log4j/core/impl/Log4jProvider.java | 167 ++++++++++++ log4j-osgi-test/pom.xml | 2 +- .../jmh/ThreadContextVsScopedContextBenchmark.java | 3 - log4j-to-jul/pom.xml | 13 + .../apache/logging/log4j/tojul/JULProvider.java | 10 +- log4j-to-slf4j/pom.xml | 7 + .../org/apache/logging/slf4j/SLF4JProvider.java | 4 +- .../.2.x.x/2330_add_faster_web_app_context_map.xml | 2 +- 25 files changed, 420 insertions(+), 901 deletions(-) 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 96783ae600..c800b396a6 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 static org.junit.jupiter.api.Assertions.assertTrue; 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.AfterAll; 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 class ThreadContextInheritanceTest { 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 2f845d8bf7..0000000000 --- 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String> keys = new HashSet<>(); - org.apache.logging.log4j.util.BiConsumer<String, String> log4j_util_action = - new org.apache.logging.log4j.util.BiConsumer<String, String>() { - @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<String, String> iterationResultMap = new HashMap<>(); - TriConsumer<String, String, Map<String, String>> triConsumer = - new TriConsumer<String, String, Map<String, String>>() { - @Override - public void accept(String k, String v, Map<String, String> 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 9652034d70..5957a931bf 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 class UnmodifiableArrayBackedMapTest { 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 dedc5be86b..243ce0e9fc 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 @@ -50,18 +50,6 @@ public class DefaultThreadContextMapTest { assertEquals(map1.hashCode(), map2.hashCode()); } - @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); 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 index f394e71bea..04392d7d7a 100644 --- 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 @@ -43,7 +43,7 @@ class ThreadContextMapTest { props.setProperty("log4j2.isThreadContextMapInheritable", "true"); final PropertiesUtil util = new PropertiesUtil(props); return Stream.of( - new DefaultThreadContextMap(true, util), + new DefaultThreadContextMap(util), new CopyOnWriteSortedArrayThreadContextMap(util), new GarbageFreeSortedArrayThreadContextMap(util)); } diff --git a/log4j-api/pom.xml b/log4j-api/pom.xml index af22fee165..5423c4f99f 100644 --- a/log4j-api/pom.xml +++ b/log4j-api/pom.xml @@ -46,7 +46,9 @@ <bnd-module-name>org.apache.logging.log4j</bnd-module-name> <bnd-extra-package-options> <!-- Not exported by most OSGi system bundles, hence we use the system classloader to load `sun.reflect.Reflection` --> - !sun.reflect + !sun.reflect, + <!-- Annotations only --> + org.jspecify.*;resolution:=optional </bnd-extra-package-options> <bnd-extra-module-options> <!-- Used in StringBuilders through reflection --> @@ -57,6 +59,13 @@ </properties> <dependencies> + + <dependency> + <groupId>org.jspecify</groupId> + <artifactId>jspecify</artifactId> + <scope>provided</scope> + </dependency> + <dependency> <groupId>org.osgi</groupId> <artifactId>org.osgi.core</artifactId> 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 ddc36de030..d919b3130a 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.Collections; 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 final class ThreadContext { ((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<String, String> entry : m.entrySet()) { contextMap.put(entry.getKey(), entry.getValue()); @@ -320,8 +317,6 @@ public final class ThreadContext { ((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 608ca77ffb..0000000000 --- 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<Object[]> 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<String, String> 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<String> 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<String, String> 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 <V> void forEach(final BiConsumer<String, ? super V> action) { - final Object[] state = threadLocalMapState.get(); - if (state == null) { - return; - } - final UnmodifiableArrayBackedMap map = UnmodifiableArrayBackedMap.getInstance(state); - map.forEach(action); - } - - @Override - public <V, S> void forEach(final TriConsumer<String, ? super V, S> 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> V getValue(final String key) { - return (V) get(key); - } - - @Override - public Map<String, String> getCopy() { - final Object[] state = threadLocalMapState.get(); - if (state == null) { - return new HashMap<>(0); - } - return new HashMap<>(UnmodifiableArrayBackedMap.getInstance(state)); - } - - @Override - public Map<String, String> 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<String, String> map = UnmodifiableArrayBackedMap.getInstance(this.threadLocalMapState.get()); - final Map<String, String> 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 fb11083338..f5aa892729 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 @@ import org.apache.logging.log4j.util.TriConsumer; * </ul> * */ -class UnmodifiableArrayBackedMap extends AbstractMap<String, String> implements Serializable, ReadOnlyStringMap { +public class UnmodifiableArrayBackedMap extends AbstractMap<String, String> 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 @@ class UnmodifiableArrayBackedMap extends AbstractMap<String, String> implements 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 @@ class UnmodifiableArrayBackedMap extends AbstractMap<String, String> implements return false; } - Object[] getBackingArray() { + public Object[] getBackingArray() { return backingArray; } @@ -255,7 +255,7 @@ class UnmodifiableArrayBackedMap extends AbstractMap<String, String> implements * @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 @@ class UnmodifiableArrayBackedMap extends AbstractMap<String, String> implements * @param value * @return */ - UnmodifiableArrayBackedMap copyAndPutAll(Map<String, String> entriesToAdd) { + public UnmodifiableArrayBackedMap copyAndPutAll(Map<String, String> entriesToAdd) { // create a new array that can hold the maximum output size UnmodifiableArrayBackedMap newMap = new UnmodifiableArrayBackedMap(numEntries + entriesToAdd.size()); @@ -309,7 +309,7 @@ class UnmodifiableArrayBackedMap extends AbstractMap<String, String> implements * @param value * @return */ - UnmodifiableArrayBackedMap copyAndRemove(String key) { + public UnmodifiableArrayBackedMap copyAndRemove(String key) { int indexToRemove = -1; for (int oldIndex = 0; oldIndex < numEntries; oldIndex++) { if (backingArray[getArrayIndexForKey(oldIndex)].hashCode() == key.hashCode() @@ -356,7 +356,7 @@ class UnmodifiableArrayBackedMap extends AbstractMap<String, String> implements * @param value * @return */ - UnmodifiableArrayBackedMap copyAndRemoveAll(Iterable<String> keysToRemoveIterable) { + public UnmodifiableArrayBackedMap copyAndRemoveAll(Iterable<String> 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/DefaultThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java index a07992d371..31e46c86d6 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,13 +27,19 @@ import org.apache.logging.log4j.util.ReadOnlyStringMap; 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} + * <p> + * 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. + * </p> + * <p> + * 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. + * </p> */ 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 @@ -40,88 +47,66 @@ public class DefaultThreadContextMap implements ThreadContextMap, ReadOnlyString */ public static final String INHERITABLE_MAP = "isThreadContextMapInheritable"; - private final boolean useMap; - private final ThreadLocal<Map<String, String>> localMap; + private ThreadLocal<Object[]> 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 - 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<Map<String, String>>() { + DefaultThreadContextMap(final PropertiesUtil properties) { + localState = properties.getBooleanProperty(INHERITABLE_MAP) + ? new InheritableThreadLocal<Object[]>() { @Override - protected Map<String, String> childValue(final Map<String, String> parentValue) { - return parentValue != null && useMap - ? Collections.unmodifiableMap(new HashMap<>(parentValue)) - : null; + protected Object[] childValue(final Object[] parentValue) { + return parentValue; } } - : new ThreadLocal<Map<String, String>>(); + : new ThreadLocal<>(); } @Override public void put(final String key, final String value) { - if (!useMap) { - return; - } - Map<String, String> 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<String, String> m) { - if (!useMap) { - return; - } - Map<String, String> map = localMap.get(); - map = map == null ? new HashMap<>(m.size()) : new HashMap<>(map); - for (final Map.Entry<String, String> 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<String, String> 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<String, String> map = localMap.get(); - if (map != null) { - final Map<String, String> 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<String> keys) { - final Map<String, String> map = localMap.get(); - if (map != null) { - final Map<String, String> 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 class DefaultThreadContextMap implements ThreadContextMap, ReadOnlyString @Override public boolean containsKey(final String key) { - final Map<String, String> map = localMap.get(); - return map != null && map.containsKey(key); + final Object[] state = localState.get(); + return state != null && getMap(state).containsKey(key); } @Override public <V> void forEach(final BiConsumer<String, ? super V> action) { - final Map<String, String> map = localMap.get(); - if (map == null) { + final Object[] state = localState.get(); + if (state == null) { return; } - for (final Map.Entry<String, String> 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 <V, S> void forEach(final TriConsumer<String, ? super V, S> action, final S state) { - final Map<String, String> map = localMap.get(); - if (map == null) { + final Object[] localState = this.localState.get(); + if (localState == null) { return; } - for (final Map.Entry<String, String> 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> V getValue(final String key) { - final Map<String, String> map = localMap.get(); - return (V) (map == null ? null : map.get(key)); + return (V) get(key); } @Override public Map<String, String> getCopy() { - final Map<String, String> 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<String, String> getImmutableMapOrNull() { - return localMap.get(); + final Object[] state = localState.get(); + return (state == null ? null : getMap(state)); } @Override public boolean isEmpty() { - final Map<String, String> map = localMap.get(); - return map == null || map.isEmpty(); + return size() == 0; } @Override public int size() { - final Map<String, String> map = localMap.get(); - return map == null ? 0 : map.size(); + final Object[] state = localState.get(); + return getMap(state).size(); } @Override public String toString() { - final Map<String, String> 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<String, String> 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 class DefaultThreadContextMap implements ThreadContextMap, ReadOnlyString 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<String, String> map = this.localMap.get(); + final Map<String, String> map = getMap(localState.get()); final Map<String, String> 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 e79626f25a..6de97d6235 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 @@ package org.apache.logging.log4j.spi; 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 class NoOpThreadContextMap implements ThreadContextMap { } @Override - public String get(final String key) { + public @Nullable String get(final String key) { return null; } @@ -48,7 +54,7 @@ public class NoOpThreadContextMap implements ThreadContextMap { } @Override - public Map<String, String> getImmutableMapOrNull() { + public @Nullable Map<String, String> 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 1a56596035..9a79024f70 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 java.util.Properties; 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 @@ import org.apache.logging.log4j.util.PropertiesUtil; * be dropped in a future version. * </p> */ +@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}. - * <p> - * <strong>Warning:</strong> the value of this constant does not point to a concrete class name. - * </p> - * @see #getThreadContextMap - */ - protected static final String NO_OP_CONTEXT_MAP = "NoOp"; - - /** - * Constant used to select a web application-safe implementation of {@link ThreadContextMap}. - * <p> - * This implementation only binds JRE classes to {@link ThreadLocal} variables. - * </p> - * <p> - * <strong>Warning:</strong> the value of this constant does not point to a concrete class name. - * </p> - * @see #getThreadContextMap - */ - protected static final String WEB_APP_CONTEXT_MAP = "WebApp"; - - /** - * Constant used to select a copy-on-write implementation of {@link ThreadContextMap}. - * <p> - * <strong>Warning:</strong> the value of this constant does not point to a concrete class name. - * </p> - * @see #getThreadContextMap - */ - protected static final String COPY_ON_WRITE_CONTEXT_MAP = "CopyOnWrite"; - - /** - * Constant used to select a garbage-free implementation of {@link ThreadContextMap}. - * <p> - * 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. - * </p> - * <p> - * <strong>Warning:</strong> the value of this constant does not point to a concrete class name. - * </p> - * @see #getThreadContextMap - */ - protected static final String GARBAGE_FREE_CONTEXT_MAP = "GarbageFree"; - - // Property keys relevant for context map selection private static final String DISABLE_CONTEXT_MAP = "log4j2.disableThreadContextMap"; private static final String DISABLE_THREAD_CONTEXT = "log4j2.disableThreadContext"; - private static final String THREAD_CONTEXT_MAP_PROPERTY = "log4j2.threadContextMap"; - private static final String GC_FREE_THREAD_CONTEXT_PROPERTY = "log4j2.garbagefree.threadContextMap"; - private static final Integer DEFAULT_PRIORITY = -1; + private static final int DEFAULT_PRIORITY = -1; private static final Logger LOGGER = StatusLogger.getLogger(); - private final Integer priority; + private final int priority; // LoggerContextFactory @Deprecated - private final String className; + private final @Nullable String className; - private final Class<? extends LoggerContextFactory> loggerContextFactoryClass; - private final Lazy<LoggerContextFactory> loggerContextFactoryLazy = Lazy.lazy(this::createLoggerContextFactory); + private final @Nullable Class<? extends LoggerContextFactory> loggerContextFactoryClass; // ThreadContextMap @Deprecated - private final String threadContextMap; + private final @Nullable String threadContextMap; - private final Class<? extends ThreadContextMap> threadContextMapClass; - private final Lazy<ThreadContextMap> threadContextMapLazy = Lazy.lazy(this::createThreadContextMap); - private final String versions; + private final @Nullable Class<? extends ThreadContextMap> threadContextMapClass; + private final @Nullable String versions; @Deprecated - private final URL url; + private final @Nullable URL url; @Deprecated private final WeakReference<ClassLoader> classLoader; @@ -154,7 +107,7 @@ public class Provider { 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 class Provider { * @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 class 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. */ public Provider( - final Integer priority, + final @Nullable Integer priority, final String versions, - final Class<? extends LoggerContextFactory> loggerContextFactoryClass) { + final @Nullable Class<? extends LoggerContextFactory> loggerContextFactoryClass) { this(priority, versions, loggerContextFactoryClass, null); } @@ -188,15 +141,15 @@ public class Provider { * @param priority A positive number specifying the provider's priority or {@code null} if default, * @param versions Minimal API version required, should be set to {@link #CURRENT_VERSION}, * @param loggerContextFactoryClass A public exported implementation of {@link LoggerContextFactory} or {@code - * null} if {@link #createLoggerContextFactory()} is also implemented, + * null} if {@link #getLoggerContextFactory()} is also implemented, * @param threadContextMapClass A public exported implementation of {@link ThreadContextMap} or {@code null} if - * {@link #createThreadContextMap()} is implemented. + * {@link #getThreadContextMapInstance()} is implemented. */ public Provider( - final Integer priority, + final @Nullable Integer priority, final String versions, - final Class<? extends LoggerContextFactory> loggerContextFactoryClass, - final Class<? extends ThreadContextMap> threadContextMapClass) { + final @Nullable Class<? extends LoggerContextFactory> loggerContextFactoryClass, + final @Nullable Class<? extends ThreadContextMap> threadContextMapClass) { this.priority = priority != null ? priority : DEFAULT_PRIORITY; this.versions = versions; this.loggerContextFactoryClass = loggerContextFactoryClass; @@ -213,7 +166,7 @@ public class Provider { * @return A String containing the Log4j versions supported. */ public String getVersions() { - return versions; + return versions != null ? versions : ""; } /** @@ -233,7 +186,7 @@ public class Provider { * @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 class Provider { * Loads the {@link LoggerContextFactory} class specified by this Provider. * * @return the LoggerContextFactory implementation class or {@code null} if unspecified or a loader error occurred. - * @see #createLoggerContextFactory() */ - public Class<? extends LoggerContextFactory> loadLoggerContextFactory() { + public @Nullable Class<? extends LoggerContextFactory> loadLoggerContextFactory() { if (loggerContextFactoryClass != null) { return loggerContextFactoryClass; } @@ -271,65 +223,30 @@ public class Provider { return null; } - private LoggerContextFactory createLoggerContextFactory() { - final Class<? extends LoggerContextFactory> factoryClass = loadLoggerContextFactory(); - if (factoryClass != null) { - try { - return LoaderUtil.newInstanceOf(factoryClass); - } catch (final Exception e) { - LOGGER.error( - "Unable to create instance of class {} specified in {}", factoryClass.getName(), getUrl(), e); - } - } - LOGGER.warn("Falling back to {}", SimpleLoggerContextFactory.INSTANCE); - return SimpleLoggerContextFactory.INSTANCE; - } - /** * @return The logger context factory to be used by {@link org.apache.logging.log4j.LogManager}. * @since 2.24.0 */ public LoggerContextFactory getLoggerContextFactory() { - return loggerContextFactoryLazy.get(); + final Class<?> implementation = loadLoggerContextFactory(); + if (implementation != null) { + try { + return LoaderUtil.newInstanceOf(implementation.asSubclass(LoggerContextFactory.class)); + } catch (final ReflectiveOperationException e) { + LOGGER.error("Failed to instantiate logger context factory {}.", implementation.getName(), e); + } + } + LOGGER.error("Falling back to simple logger context factory: {}", SimpleLoggerContextFactory.class.getName()); + return SimpleLoggerContextFactory.INSTANCE; } /** - * Gets the class name of the {@link ThreadContextMap} implementation of this Provider. - * <p> - * This method should return one of the internal implementations: - * <ol> - * <li>{@code null} if {@link #loadThreadContextMap} is implemented,</li> - * <li>{@link #NO_OP_CONTEXT_MAP},</li> - * <li>{@link #WEB_APP_CONTEXT_MAP},</li> - * <li>{@link #COPY_ON_WRITE_CONTEXT_MAP},</li> - * <li>{@link #GARBAGE_FREE_CONTEXT_MAP}.</li> - * </ol> - * </p> + * Gets the class name of the {@link org.apache.logging.log4j.spi.ThreadContextMap} implementation of this Provider. + * * @return the class name of a ThreadContextMap implementation - * @see #loadThreadContextMap() */ - public String getThreadContextMap() { - if (threadContextMapClass != null) { - return threadContextMapClass.getName(); - } - // Field value - if (threadContextMap != null) { - return threadContextMap; - } - // Properties - final PropertiesUtil props = PropertiesUtil.getProperties(); - if (props.getBooleanProperty(DISABLE_CONTEXT_MAP) || props.getBooleanProperty(DISABLE_THREAD_CONTEXT)) { - return NO_OP_CONTEXT_MAP; - } - final String threadContextMapClass = props.getStringProperty(THREAD_CONTEXT_MAP_PROPERTY); - if (threadContextMapClass != null) { - return threadContextMapClass; - } - // Default based on properties - if (props.getBooleanProperty(GC_FREE_THREAD_CONTEXT_PROPERTY)) { - return GARBAGE_FREE_CONTEXT_MAP; - } - return Constants.ENABLE_THREADLOCALS ? COPY_ON_WRITE_CONTEXT_MAP : WEB_APP_CONTEXT_MAP; + public @Nullable String getThreadContextMap() { + return threadContextMapClass != null ? threadContextMapClass.getName() : threadContextMap; } /** @@ -337,17 +254,18 @@ public class Provider { * * @return the {@code ThreadContextMap} implementation class or {@code null} if unspecified or a loading error * occurred. - * @see #createThreadContextMap() */ - public Class<? extends ThreadContextMap> loadThreadContextMap() { + public @Nullable Class<? extends ThreadContextMap> loadThreadContextMap() { if (threadContextMapClass != null) { return threadContextMapClass; } - final String threadContextMap = getThreadContextMap(); + if (threadContextMap == null) { + return null; + } final ClassLoader loader = classLoader.get(); // Support for deprecated {@code META-INF/log4j-provider.properties} format. // In the remaining cases {@code loader == null}. - if (loader == null || threadContextMap == null) { + if (loader == null) { return null; } try { @@ -358,78 +276,34 @@ public class Provider { LOGGER.error( "Class {} specified in {} does not extend {}", threadContextMap, - getUrl(), + url, ThreadContextMap.class.getName()); } } catch (final Exception e) { - LOGGER.error("Unable to load class {} specified in {}", threadContextMap, url.toString(), e); + LOGGER.error("Unable to load class {} specified in {}", threadContextMap, url, e); } return null; } /** - * Creates a {@link ThreadContextMap} using the legacy {@link #loadThreadContextMap()} and - * {@link #getThreadContextMap()} methods: - * <ol> - * <li>calls {@link #loadThreadContextMap},</li> - * <li>if the previous call returns {@code null}, it calls {@link #getThreadContextMap} to instantiate one of - * the internal implementations,</li> - * <li>it returns a no-op map otherwise.</li> - * </ol> + * @return The thread context map to be used by {@link org.apache.logging.log4j.ThreadContext}. + * @since 2.24.0 */ - @SuppressWarnings("deprecation") - ThreadContextMap createThreadContextMap() { - final Class<? extends ThreadContextMap> threadContextMapClass = loadThreadContextMap(); - if (threadContextMapClass != null) { - try { - return LoaderUtil.newInstanceOf(threadContextMapClass); - } catch (final Exception e) { - LOGGER.error( - "Unable to create instance of class {} specified in {}", - threadContextMapClass.getName(), - getUrl(), - e); - } + public ThreadContextMap getThreadContextMapInstance() { + final PropertiesUtil props = PropertiesUtil.getProperties(); + if (props.getBooleanProperty(DISABLE_CONTEXT_MAP) || props.getBooleanProperty(DISABLE_THREAD_CONTEXT)) { + return NoOpThreadContextMap.INSTANCE; } - // 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(); + return new DefaultThreadContextMap(); } /** @@ -447,7 +321,7 @@ public class Provider { * @deprecated since 2.24.0, without replacement. */ @Deprecated - public URL getUrl() { + public @Nullable URL getUrl() { return url; } @@ -455,7 +329,7 @@ public class Provider { public String toString() { final StringBuilder result = new StringBuilder("Provider '").append(getClass().getName()).append("'"); - if (!DEFAULT_PRIORITY.equals(priority)) { + if (priority != DEFAULT_PRIORITY) { result.append("\n\tpriority = ").append(priority); } final String threadContextMap = getThreadContextMap(); @@ -485,24 +359,18 @@ public class Provider { 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 b00bd97985..418beb4583 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 a98bce9fdc..20f6791a2f 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.nio.file.Path; 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 @@ public abstract class AbstractAsyncThreadContextTestBase { } protected enum ContextImpl { - WEBAPP, - GARBAGE_FREE, - COPY_ON_WRITE; + WEBAPP("WebApp", "org.apache.logging.log4j.spi.DefaultThreadContextMap"), + GARBAGE_FREE("GarbageFree", "org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap"), + COPY_ON_WRITE("CopyOnWrite", "org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap"); + + 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 @@ public abstract class AbstractAsyncThreadContextTestBase { 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 @@ public abstract class AbstractAsyncThreadContextTestBase { 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/impl/ThreadContextDataInjectorTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java index 9465460823..c94e5e1143 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 @@ package org.apache.logging.log4j.core.impl; import static java.util.Arrays.asList; import static java.util.concurrent.Executors.newSingleThreadExecutor; -import static org.apache.logging.log4j.ThreadContext.getThreadContextMap; import static org.apache.logging.log4j.core.impl.ContextDataInjectorFactory.createInjector; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.allOf; @@ -33,9 +32,9 @@ import java.util.Collection; import java.util.concurrent.ExecutionException; import org.apache.logging.log4j.ThreadContext; import org.apache.logging.log4j.core.ContextDataInjector; -import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap; -import org.apache.logging.log4j.test.ThreadContextUtilityClass; +import org.apache.logging.log4j.spi.ThreadContextMap; import org.apache.logging.log4j.util.PropertiesUtil; +import org.apache.logging.log4j.util.ProviderUtil; import org.apache.logging.log4j.util.SortedArrayStringMap; import org.apache.logging.log4j.util.StringMap; import org.junit.After; @@ -51,26 +50,24 @@ public class ThreadContextDataInjectorTest { @Parameters(name = "{0}") public static Collection<String[]> 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.spi.CopyOnWriteSortedArrayThreadContextMap"}, + {"org.apache.logging.log4j.spi.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); } @@ -83,13 +80,11 @@ public class ThreadContextDataInjectorTest { } private void testContextDataInjector() { - final ReadOnlyThreadContextMap readOnlythreadContextMap = getThreadContextMap(); + final ThreadContextMap threadContextMap = ProviderUtil.getProvider().getThreadContextMapInstance(); assertThat( "thread context map class name", - (readOnlythreadContextMap == null) - ? null - : readOnlythreadContextMap.getClass().getName(), - is(equalTo(readOnlythreadContextMapClassName))); + threadContextMap.getClass().getName(), + is(equalTo(threadContextMapClassName))); final ContextDataInjector contextDataInjector = createInjector(true); final StringMap stringMap = contextDataInjector.injectContextData(null, new SortedArrayStringMap()); @@ -122,9 +117,8 @@ public class ThreadContextDataInjectorTest { private void prepareThreadContext(final boolean isThreadContextMapInheritable) { System.setProperty("log4j2.isThreadContextMapInheritable", Boolean.toString(isThreadContextMapInheritable)); - PropertiesUtil.getProperties().reload(); - ThreadContextUtilityClass.reset(); - ThreadContext.remove("baz"); + resetThreadContextMap(); + ThreadContext.clearMap(); ThreadContext.put("foo", "bar"); } diff --git a/log4j-core-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 ecf3e9f6c6..661023a46f 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; /** * <p> @@ -29,6 +32,8 @@ public final class ThreadContextTestAccess { } 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 05a6de655d..1d376c994b 100644 --- a/log4j-core/pom.xml +++ b/log4j-core/pom.xml @@ -53,6 +53,8 @@ --> <bnd-multi-release>true</bnd-multi-release> <bnd-extra-package-options> + <!-- Annotations only --> + org.jspecify.*;resolution:=optional, <!-- External optional dependencies --> com.conversantmedia.util.concurrent;resolution:=optional; com.fasterxml.jackson.*;resolution:=optional, @@ -122,6 +124,11 @@ <scope>provided</scope> <optional>true</optional> </dependency> + <dependency> + <groupId>org.jspecify</groupId> + <artifactId>jspecify</artifactId> + <scope>provided</scope> + </dependency> <!-- Used for OSGi bundle support --> <dependency> <groupId>org.osgi</groupId> 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 31d71cb5f4..222eafc858 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 @@ -19,16 +19,183 @@ package org.apache.logging.log4j.core.impl; import aQute.bnd.annotation.Resolution; import aQute.bnd.annotation.spi.ServiceConsumer; import aQute.bnd.annotation.spi.ServiceProvider; +import org.apache.logging.log4j.Logger; +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.ScopedContextProvider; +import org.apache.logging.log4j.spi.ThreadContextMap; +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; /** * Binding for the Log4j API. */ @ServiceProvider(value = Provider.class, resolution = Resolution.OPTIONAL) @ServiceConsumer(value = ScopedContextProvider.class, resolution = Resolution.OPTIONAL) +@NullMarked public class Log4jProvider extends Provider { + + /** + * Constant used to disable the {@link ThreadContextMap}. + * <p> + * <strong>Warning:</strong> the value of this constant does not point to a concrete class name. + * </p> + * @see #getThreadContextMap + */ + private static final String NO_OP_CONTEXT_MAP = "NoOp"; + + /** + * Constant used to select a web application-safe implementation of {@link ThreadContextMap}. + * <p> + * This implementation only binds JRE classes to {@link ThreadLocal} variables. + * </p> + * <p> + * <strong>Warning:</strong> the value of this constant does not point to a concrete class name. + * </p> + * @see #getThreadContextMap + */ + private static final String WEB_APP_CONTEXT_MAP = "WebApp"; + + /** + * Constant used to select a copy-on-write implementation of {@link ThreadContextMap}. + * <p> + * <strong>Warning:</strong> the value of this constant does not point to a concrete class name. + * </p> + * @see #getThreadContextMap + */ + private static final String COPY_ON_WRITE_CONTEXT_MAP = "CopyOnWrite"; + + /** + * Constant used to select a garbage-free implementation of {@link ThreadContextMap}. + * <p> + * 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. + * </p> + * <p> + * <strong>Warning:</strong> the value of this constant does not point to a concrete class name. + * </p> + * @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.spi.GarbageFreeSortedArrayThreadContextMap"; + private static final String COPY_ON_WRITE_CLASS_NAME = + "org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap"; + + private static final Logger LOGGER = StatusLogger.getLogger(); + + private final Lazy<LoggerContextFactory> loggerContextFactoryLazy = Lazy.lazy(Log4jContextFactory::new); + private final Lazy<ThreadContextMap> 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) { + if (props.getBooleanProperty(GC_FREE_THREAD_CONTEXT_PROPERTY)) { + threadContextMapClass = GARBAGE_FREE_CONTEXT_MAP; + } else if (Constants.ENABLE_THREADLOCALS) { + threadContextMapClass = COPY_ON_WRITE_CONTEXT_MAP; + } else { + threadContextMapClass = 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(); + case GARBAGE_FREE_CONTEXT_MAP: + case "org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap": + case COPY_ON_WRITE_CONTEXT_MAP: + case "org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap": + return super.getThreadContextMapInstance(); + 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; + } + + @Override + public @Nullable String getThreadContextMap() { + // Properties + final PropertiesUtil props = PropertiesUtil.getProperties(); + if (props.getBooleanProperty(DISABLE_CONTEXT_MAP) || props.getBooleanProperty(DISABLE_THREAD_CONTEXT)) { + return NoOpThreadContextMap.class.getName(); + } + String threadContextMapClass = props.getStringProperty(THREAD_CONTEXT_MAP_PROPERTY); + // Default based on properties + if (threadContextMapClass == null) { + if (props.getBooleanProperty(GC_FREE_THREAD_CONTEXT_PROPERTY)) { + threadContextMapClass = GARBAGE_FREE_CONTEXT_MAP; + } else if (Constants.ENABLE_THREADLOCALS) { + threadContextMapClass = COPY_ON_WRITE_CONTEXT_MAP; + } else { + threadContextMapClass = WEB_APP_CONTEXT_MAP; + } + } + switch (threadContextMapClass) { + case NO_OP_CONTEXT_MAP: + return NoOpThreadContextMap.class.getName(); + case WEB_APP_CONTEXT_MAP: + return DefaultThreadContextMap.class.getName(); + case GARBAGE_FREE_CONTEXT_MAP: + return GARBAGE_FREE_CLASS_NAME; + case COPY_ON_WRITE_CONTEXT_MAP: + return COPY_ON_WRITE_CLASS_NAME; + default: + return threadContextMapClass; + } + } + + // Used in tests + void resetThreadContextMap() { + threadContextMapLazy.set(null); + } } diff --git a/log4j-osgi-test/pom.xml b/log4j-osgi-test/pom.xml index d8e241ce79..60930eb317 100644 --- a/log4j-osgi-test/pom.xml +++ b/log4j-osgi-test/pom.xml @@ -203,7 +203,7 @@ <artifactId>maven-surefire-plugin</artifactId> <configuration> <classpathDependencyExcludes> - <exclude>org.osgi:org.osgi.framework</exclude> + <exclude>org.osgi:org.osgi.core</exclude> </classpathDependencyExcludes> <systemPropertyVariables> <!-- PAX logging has a copy of Log4j2 API--> diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextVsScopedContextBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextVsScopedContextBenchmark.java index c510e4f10c..76ac3d4d82 100644 --- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextVsScopedContextBenchmark.java +++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextVsScopedContextBenchmark.java @@ -30,7 +30,6 @@ 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.internal.map.StringArrayThreadContextMap; import org.apache.logging.log4j.perf.appender.StringAppender; import org.apache.logging.log4j.spi.CopyOnWriteOpenHashMapThreadContextMap; import org.apache.logging.log4j.spi.DefaultThreadContextMap; @@ -92,7 +91,6 @@ public class ThreadContextVsScopedContextBenchmark { private static final Logger LOGGER = LogManager.getLogger(ThreadContextVsScopedContextBenchmark.class); private static final String DEFAULT_CONTEXT_MAP = "Default"; - private static final String STRING_ARRAY_MAP = "StringArray"; 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"; @@ -101,7 +99,6 @@ public class ThreadContextVsScopedContextBenchmark { static { IMPLEMENTATIONS.put(DEFAULT_CONTEXT_MAP, DefaultThreadContextMap.class); - IMPLEMENTATIONS.put(STRING_ARRAY_MAP, StringArrayThreadContextMap.class); IMPLEMENTATIONS.put(COPY_OPENHASH_MAP, CopyOnWriteOpenHashMapThreadContextMap.class); IMPLEMENTATIONS.put( COPY_ARRAY_MAP, diff --git a/log4j-to-jul/pom.xml b/log4j-to-jul/pom.xml index 0c4babfe54..25caed1639 100644 --- a/log4j-to-jul/pom.xml +++ b/log4j-to-jul/pom.xml @@ -28,7 +28,20 @@ <name>Apache Log4j to JUL Bridge</name> <description>The Apache Log4j binding between Log4j 2 API and java.util.logging (JUL).</description> <inceptionYear>2022</inceptionYear> + + <properties> + <bnd-extra-package-options> + <!-- Annotations only --> + org.jspecify.*;resolution:=optional + </bnd-extra-package-options> + </properties> + <dependencies> + <dependency> + <groupId>org.jspecify</groupId> + <artifactId>jspecify</artifactId> + <scope>provided</scope> + </dependency> <dependency> <groupId>org.osgi</groupId> <artifactId>org.osgi.core</artifactId> 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 0f371f43ef..7497e484f8 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 @@ package org.apache.logging.log4j.tojul; 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 <a href="http://www.vorburger.ch">Michael Vorburger.ch</a> 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 class JULProvider extends Provider { } @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 fdcb4888a9..a983debe93 100644 --- a/log4j-to-slf4j/pom.xml +++ b/log4j-to-slf4j/pom.xml @@ -40,6 +40,8 @@ --> <slf4j.support.range>[1.7,3)</slf4j.support.range> <bnd-extra-package-options> + <!-- Annotations only --> + org.jspecify.*;resolution:=optional, <!-- This bridge also support SLF4J 2.x --> org.slf4j.*;version="${slf4j.support.range}" </bnd-extra-package-options> @@ -63,6 +65,11 @@ <artifactId>org.osgi.core</artifactId> <scope>provided</scope> </dependency> + <dependency> + <groupId>org.jspecify</groupId> + <artifactId>jspecify</artifactId> + <scope>provided</scope> + </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> 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 318d7937f6..52a11a5560 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 aQute.bnd.annotation.spi.ServiceProvider; 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 34c1bd653d..f3076419d0 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"> <issue id="2330" link="https://github.com/apache/logging-log4j2/pull/2330"/> - <description format="asciidoc">Add a faster `ThreadContextMap` for web app users: `org.apache.logging.log4j.internal.map.StringArrayThreadContextMap`.</description> + <description format="asciidoc">Add a faster `DefaultThreadContextMap` implementation.</description> </entry>
