diff --git a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/InitializesThreadContext.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/InitializesThreadContext.java deleted file mode 100644 index abbb616dc0e..00000000000 --- a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/InitializesThreadContext.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to you under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.logging.log4j.test.junit; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import org.apache.logging.log4j.ThreadContext; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.parallel.ResourceAccessMode; -import org.junit.jupiter.api.parallel.ResourceLock; - -/** - * Marks a test class that initializes the {@link ThreadContext} class; - */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE, ElementType.METHOD}) -@Documented -@Inherited -@ExtendWith(ThreadContextInitializer.class) -@ResourceLock(value = Log4jStaticResources.THREAD_CONTEXT, mode = ResourceAccessMode.READ_WRITE) -public @interface InitializesThreadContext {} diff --git a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/ThreadContextInitializer.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/ThreadContextInitializer.java deleted file mode 100644 index 8afceef7378..00000000000 --- a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/ThreadContextInitializer.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to you under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.logging.log4j.test.junit; - -import org.apache.logging.log4j.ThreadContext; -import org.apache.logging.log4j.test.ThreadContextUtilityClass; -import org.junit.jupiter.api.extension.BeforeAllCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; -import org.junit.platform.commons.support.AnnotationSupport; - -class ThreadContextInitializer implements BeforeAllCallback, BeforeEachCallback { - - @Override - public void beforeAll(final ExtensionContext context) throws Exception { - if (AnnotationSupport.isAnnotated(context.getRequiredTestClass(), InitializesThreadContext.class)) { - resetThreadContext(context); - } - } - - @Override - public void beforeEach(final ExtensionContext context) throws Exception { - if (AnnotationSupport.isAnnotated(context.getRequiredTestMethod(), InitializesThreadContext.class)) { - resetThreadContext(context); - } - } - - private void resetThreadContext(final ExtensionContext context) { - ThreadContextUtilityClass.reset(); - // We use `CloseableResource` instead of `afterAll` to reset the - // ThreadContextFactory - // *after* the `@SetSystemProperty` extension has restored the properties - ExtensionContextAnchor.setAttribute( - ThreadContext.class, - new CloseableResource() { - @Override - public void close() throws Throwable { - ThreadContextUtilityClass.reset(); - } - }, - context); - } -} diff --git a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/AbstractThreadContextMapTest.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/AbstractThreadContextMapTest.java new file mode 100644 index 00000000000..4c7c9c157be --- /dev/null +++ b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/AbstractThreadContextMapTest.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.log4j.test.spi; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.apache.logging.log4j.spi.ThreadContextMap; + +/** + * Provides a set of utility methods to test implementations of {@link ThreadContextMap}. + */ +public abstract class AbstractThreadContextMapTest { + + private static final String KEY = "key"; + + /** + * Implementations SHOULD not propagate the context to newly created threads by default. + * + * @param contextMap A {@link ThreadContextMap implementation}. + */ + protected static void assertThreadLocalNotInheritable(final ThreadContextMap contextMap) { + contextMap.put(KEY, "threadLocalNotInheritableByDefault"); + verifyThreadContextValueFromANewThread(contextMap, null); + } + + /** + * Implementations MAY offer a configuration that propagates the context to newly created threads. + * + * @param contextMap A {@link ThreadContextMap implementation}. + */ + protected static void assertThreadLocalInheritable(final ThreadContextMap contextMap) { + contextMap.put(KEY, "threadLocalInheritableIfConfigured"); + verifyThreadContextValueFromANewThread(contextMap, "threadLocalInheritableIfConfigured"); + } + + private static void verifyThreadContextValueFromANewThread( + final ThreadContextMap contextMap, final String expected) { + final ExecutorService executorService = Executors.newSingleThreadExecutor(); + try { + assertThat(executorService.submit(() -> contextMap.get(KEY))) + .succeedsWithin(Duration.ofSeconds(1)) + .isEqualTo(expected); + } finally { + executorService.shutdown(); + } + } +} diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextInheritanceTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextInheritanceTest.java index 96783ae600c..c800b396a61 100644 --- a/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextInheritanceTest.java +++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextInheritanceTest.java @@ -23,7 +23,6 @@ import org.apache.logging.log4j.spi.DefaultThreadContextMap; import org.apache.logging.log4j.test.ThreadContextUtilityClass; -import org.apache.logging.log4j.test.junit.InitializesThreadContext; import org.apache.logging.log4j.test.junit.SetTestProperty; import org.apache.logging.log4j.test.junit.UsingThreadContextMap; import org.apache.logging.log4j.test.junit.UsingThreadContextStack; @@ -31,13 +30,11 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; -import org.junitpioneer.jupiter.SetSystemProperty; /** * Tests {@link ThreadContext}. */ @SetTestProperty(key = DefaultThreadContextMap.INHERITABLE_MAP, value = "true") -@InitializesThreadContext @UsingThreadContextMap @UsingThreadContextStack public class ThreadContextInheritanceTest { @@ -63,31 +60,6 @@ public void testPush() { assertEquals(ThreadContext.pop(), "Hello", "Incorrect simple stack value"); } - @Test - @SetSystemProperty(key = DefaultThreadContextMap.INHERITABLE_MAP, value = "true") - @InitializesThreadContext - public void testInheritanceSwitchedOn() throws Exception { - System.setProperty(DefaultThreadContextMap.INHERITABLE_MAP, "true"); - try { - ThreadContext.clearMap(); - ThreadContext.put("Greeting", "Hello"); - StringBuilder sb = new StringBuilder(); - TestThread thread = new TestThread(sb); - thread.start(); - thread.join(); - String str = sb.toString(); - assertEquals("Hello", str, "Unexpected ThreadContext value. Expected Hello. Actual " + str); - sb = new StringBuilder(); - thread = new TestThread(sb); - thread.start(); - thread.join(); - str = sb.toString(); - assertEquals("Hello", str, "Unexpected ThreadContext value. Expected Hello. Actual " + str); - } finally { - System.clearProperty(DefaultThreadContextMap.INHERITABLE_MAP); - } - } - @Test @Tag("performance") public void perfTest() { diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/DefaultThreadContextMapTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/DefaultThreadContextMapTest.java index dedc5be86b8..43c92dbb5ae 100644 --- a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/DefaultThreadContextMapTest.java +++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/DefaultThreadContextMapTest.java @@ -24,31 +24,17 @@ import java.util.HashMap; import java.util.Map; +import java.util.Properties; import org.apache.logging.log4j.test.junit.UsingThreadContextMap; +import org.apache.logging.log4j.test.spi.AbstractThreadContextMapTest; +import org.apache.logging.log4j.util.PropertiesUtil; import org.junit.jupiter.api.Test; /** * Tests the {@code DefaultThreadContextMap} class. */ @UsingThreadContextMap -public class DefaultThreadContextMapTest { - - @Test - public void testEqualsVsSameKind() { - final DefaultThreadContextMap map1 = createMap(); - final DefaultThreadContextMap map2 = createMap(); - assertEquals(map1, map1); - assertEquals(map2, map2); - assertEquals(map1, map2); - assertEquals(map2, map1); - } - - @Test - public void testHashCodeVsSameKind() { - final DefaultThreadContextMap map1 = createMap(); - final DefaultThreadContextMap map2 = createMap(); - assertEquals(map1.hashCode(), map2.hashCode()); - } +class DefaultThreadContextMapTest extends AbstractThreadContextMapTest { @Test public void testDoesNothingIfConstructedWithUseMapIsFalse() { @@ -214,4 +200,17 @@ public void testToStringShowsMapContext() { map.put("key2", "value2"); assertEquals("{key2=value2}", map.toString()); } + + @Test + void threadLocalNotInheritableByDefault() { + assertThreadLocalNotInheritable(new DefaultThreadContextMap()); + } + + @Test + void threadLocalInheritableIfConfigured() { + final Properties props = new Properties(); + props.setProperty("log4j2.isThreadContextMapInheritable", "true"); + final PropertiesUtil util = new PropertiesUtil(props); + assertThreadLocalInheritable(new DefaultThreadContextMap(true, util)); + } } diff --git a/log4j-api/pom.xml b/log4j-api/pom.xml index af22fee1659..5423c4f99fb 100644 --- a/log4j-api/pom.xml +++ b/log4j-api/pom.xml @@ -46,7 +46,9 @@ org.apache.logging.log4j - !sun.reflect + !sun.reflect, + + org.jspecify.*;resolution:=optional @@ -57,6 +59,13 @@ + + + org.jspecify + jspecify + provided + + org.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..50cebbee446 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java @@ -23,15 +23,12 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import org.apache.logging.log4j.internal.map.StringArrayThreadContextMap; import org.apache.logging.log4j.message.ParameterizedMessage; -import org.apache.logging.log4j.spi.CleanableThreadContextMap; import org.apache.logging.log4j.spi.DefaultThreadContextMap; import org.apache.logging.log4j.spi.DefaultThreadContextStack; import org.apache.logging.log4j.spi.MutableThreadContextStack; import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap; import org.apache.logging.log4j.spi.ThreadContextMap; -import org.apache.logging.log4j.spi.ThreadContextMap2; import org.apache.logging.log4j.spi.ThreadContextMapFactory; import org.apache.logging.log4j.spi.ThreadContextStack; import org.apache.logging.log4j.util.PropertiesUtil; @@ -268,21 +265,11 @@ public static void putIfNull(final String key, final String value) { * *

If the current thread does not have a context map it is * created as a side effect.

- * @param m The map. + * @param map The map. * @since 2.7 */ - public static void putAll(final Map m) { - if (contextMap instanceof ThreadContextMap2) { - ((ThreadContextMap2) contextMap).putAll(m); - } else if (contextMap instanceof DefaultThreadContextMap) { - ((DefaultThreadContextMap) contextMap).putAll(m); - } else if (contextMap instanceof StringArrayThreadContextMap) { - ((StringArrayThreadContextMap) contextMap).putAll(m); - } else { - for (final Map.Entry entry : m.entrySet()) { - contextMap.put(entry.getKey(), entry.getValue()); - } - } + public static void putAll(final Map map) { + contextMap.putAll(map); } /** @@ -316,17 +303,7 @@ public static void remove(final String key) { * @since 2.8 */ public static void removeAll(final Iterable keys) { - if (contextMap instanceof CleanableThreadContextMap) { - ((CleanableThreadContextMap) contextMap).removeAll(keys); - } else if (contextMap instanceof DefaultThreadContextMap) { - ((DefaultThreadContextMap) contextMap).removeAll(keys); - } else if (contextMap instanceof StringArrayThreadContextMap) { - ((StringArrayThreadContextMap) contextMap).removeAll(keys); - } else { - for (final String key : keys) { - contextMap.remove(key); - } - } + contextMap.removeAll(keys); } /** diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java deleted file mode 100644 index ca9f501c217..00000000000 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to you under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.logging.log4j.spi; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import org.apache.logging.log4j.util.PropertiesUtil; -import org.apache.logging.log4j.util.ReadOnlyStringMap; -import org.apache.logging.log4j.util.SortedArrayStringMap; -import org.apache.logging.log4j.util.StringMap; - -/** - * {@code SortedArrayStringMap}-based implementation of the {@code ThreadContextMap} interface that creates a copy of - * the data structure on every modification. Any particular instance of the data structure is a snapshot of the - * ThreadContext at some point in time and can safely be passed off to other threads. Since it is - * expected that the Map will be passed to many more log events than the number of keys it contains the performance - * should be much better than if the Map was copied for each event. - * - * @since 2.7 - */ -class CopyOnWriteSortedArrayThreadContextMap implements ReadOnlyThreadContextMap, ObjectThreadContextMap, CopyOnWrite { - - /** - * Property name ({@value} ) for selecting {@code InheritableThreadLocal} (value "true") or plain - * {@code ThreadLocal} (value is not "true") in the implementation. - */ - public static final String INHERITABLE_MAP = "isThreadContextMapInheritable"; - - /** - * The default initial capacity. - */ - protected static final int DEFAULT_INITIAL_CAPACITY = 16; - - /** - * System property name that can be used to control the data structure's initial capacity. - */ - protected static final String PROPERTY_NAME_INITIAL_CAPACITY = "log4j2.ThreadContext.initial.capacity"; - - private static final StringMap EMPTY_CONTEXT_DATA = new SortedArrayStringMap(1); - - static { - EMPTY_CONTEXT_DATA.freeze(); - } - - private final int initialCapacity; - private final ThreadLocal localMap; - - public CopyOnWriteSortedArrayThreadContextMap() { - this(PropertiesUtil.getProperties()); - } - - CopyOnWriteSortedArrayThreadContextMap(final PropertiesUtil properties) { - initialCapacity = properties.getIntegerProperty(PROPERTY_NAME_INITIAL_CAPACITY, DEFAULT_INITIAL_CAPACITY); - localMap = properties.getBooleanProperty(INHERITABLE_MAP) - ? new InheritableThreadLocal() { - @Override - protected StringMap childValue(final StringMap parentValue) { - if (parentValue == null) { - return null; - } - final StringMap stringMap = createStringMap(parentValue); - stringMap.freeze(); - return stringMap; - } - } - : new ThreadLocal(); - } - - /** - * Returns an implementation of the {@code StringMap} used to back this thread context map. - *

- * Subclasses may override. - *

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

- * Subclasses may override. - *

- * @param original the key-value pairs to initialize the returned context data with - * @return an implementation of the {@code StringMap} used to back this thread context map - */ - protected StringMap createStringMap(final ReadOnlyStringMap original) { - return new SortedArrayStringMap(original); - } - - @Override - public void put(final String key, final String value) { - putValue(key, value); - } - - @Override - public void putValue(final String key, final Object value) { - StringMap map = localMap.get(); - map = map == null ? createStringMap() : createStringMap(map); - map.putValue(key, value); - map.freeze(); - localMap.set(map); - } - - @Override - public void putAll(final Map values) { - if (values == null || values.isEmpty()) { - return; - } - StringMap map = localMap.get(); - map = map == null ? createStringMap() : createStringMap(map); - for (final Map.Entry entry : values.entrySet()) { - map.putValue(entry.getKey(), entry.getValue()); - } - map.freeze(); - localMap.set(map); - } - - @Override - public void putAllValues(final Map values) { - if (values == null || values.isEmpty()) { - return; - } - StringMap map = localMap.get(); - map = map == null ? createStringMap() : createStringMap(map); - for (final Map.Entry entry : values.entrySet()) { - map.putValue(entry.getKey(), entry.getValue()); - } - map.freeze(); - localMap.set(map); - } - - @Override - public String get(final String key) { - return (String) getValue(key); - } - - @Override - public V getValue(final String key) { - final StringMap map = localMap.get(); - return map == null ? null : map.getValue(key); - } - - @Override - public void remove(final String key) { - final StringMap map = localMap.get(); - if (map != null) { - final StringMap copy = createStringMap(map); - copy.remove(key); - copy.freeze(); - localMap.set(copy); - } - } - - @Override - public void removeAll(final Iterable keys) { - final StringMap map = localMap.get(); - if (map != null) { - final StringMap copy = createStringMap(map); - for (final String key : keys) { - copy.remove(key); - } - copy.freeze(); - localMap.set(copy); - } - } - - @Override - public void clear() { - localMap.remove(); - } - - @Override - public boolean containsKey(final String key) { - final StringMap map = localMap.get(); - return map != null && map.containsKey(key); - } - - @Override - public Map getCopy() { - final StringMap map = localMap.get(); - return map == null ? new HashMap<>() : map.toMap(); - } - - /** - * {@inheritDoc} - */ - @Override - public StringMap getReadOnlyContextData() { - final StringMap map = localMap.get(); - return map == null ? EMPTY_CONTEXT_DATA : map; - } - - @Override - public Map getImmutableMapOrNull() { - final StringMap map = localMap.get(); - return map == null ? null : Collections.unmodifiableMap(map.toMap()); - } - - @Override - public boolean isEmpty() { - final StringMap map = localMap.get(); - return map == null || map.isEmpty(); - } - - @Override - public String toString() { - final StringMap map = localMap.get(); - return map == null ? "{}" : map.toString(); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - final StringMap map = this.localMap.get(); - result = prime * result + ((map == null) ? 0 : map.hashCode()); - return result; - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof ThreadContextMap)) { - return false; - } - final ThreadContextMap other = (ThreadContextMap) obj; - final Map map = this.getImmutableMapOrNull(); - final Map otherMap = other.getImmutableMapOrNull(); - return Objects.equals(map, otherMap); - } -} diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java index a07992d371f..61495a7172f 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java @@ -24,6 +24,8 @@ import org.apache.logging.log4j.util.PropertiesUtil; import org.apache.logging.log4j.util.ReadOnlyStringMap; import org.apache.logging.log4j.util.TriConsumer; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * The actual ThreadContext Map. A new ThreadContext Map is created each time it is updated and the Map stored is always @@ -31,6 +33,7 @@ * expected that the Map will be passed to many more log events than the number of keys it contains the performance * should be much better than if the Map was copied for each event. */ +@NullMarked public class DefaultThreadContextMap implements ThreadContextMap, ReadOnlyStringMap { private static final long serialVersionUID = 8218007901108944053L; @@ -40,8 +43,13 @@ public class DefaultThreadContextMap implements ThreadContextMap, ReadOnlyString */ public static final String INHERITABLE_MAP = "isThreadContextMapInheritable"; + /** + * @since 2.24.0 + */ + public static final ThreadContextMap INSTANCE = new DefaultThreadContextMap(); + private final boolean useMap; - private final ThreadLocal> localMap; + private final ThreadLocal<@Nullable Map> localMap; public DefaultThreadContextMap() { this(true); @@ -60,7 +68,8 @@ public DefaultThreadContextMap(final boolean useMap) { localMap = properties.getBooleanProperty(INHERITABLE_MAP) ? new InheritableThreadLocal>() { @Override - protected Map childValue(final Map parentValue) { + protected @Nullable Map childValue( + final @Nullable Map parentValue) { return parentValue != null && useMap ? Collections.unmodifiableMap(new HashMap<>(parentValue)) : null; @@ -80,20 +89,19 @@ public void put(final String key, final String value) { localMap.set(Collections.unmodifiableMap(map)); } + @Override 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()); - } + map.putAll(m); localMap.set(Collections.unmodifiableMap(map)); } @Override - public String get(final String key) { + public @Nullable String get(final String key) { final Map map = localMap.get(); return map == null ? null : map.get(key); } @@ -108,13 +116,12 @@ public void remove(final String key) { } } + @Override 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); - } + keys.forEach(copy::remove); localMap.set(Collections.unmodifiableMap(copy)); } } @@ -166,8 +173,7 @@ public void forEach(final TriConsumer action, final @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 @@ -177,7 +183,7 @@ public Map getCopy() { } @Override - public Map getImmutableMapOrNull() { + public @Nullable Map getImmutableMapOrNull() { return localMap.get(); } @@ -203,9 +209,9 @@ public String toString() { public int hashCode() { final int prime = 31; int result = 1; - final Map map = this.localMap.get(); + final Map map = localMap.get(); result = prime * result + ((map == null) ? 0 : map.hashCode()); - result = prime * result + Boolean.valueOf(this.useMap).hashCode(); + result = prime * result + Boolean.valueOf(useMap).hashCode(); return result; } @@ -219,7 +225,7 @@ public boolean equals(final Object obj) { } if (obj instanceof DefaultThreadContextMap) { final DefaultThreadContextMap other = (DefaultThreadContextMap) obj; - if (this.useMap != other.useMap) { + if (useMap != other.useMap) { return false; } } @@ -227,7 +233,7 @@ public boolean equals(final Object obj) { return false; } final ThreadContextMap other = (ThreadContextMap) obj; - final Map map = this.localMap.get(); + final Map map = localMap.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..0acb4c723dd 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; } @@ -60,6 +66,12 @@ public boolean isEmpty() { @Override public void put(final String key, final String value) {} + @Override + public void putAll(final Map map) {} + @Override public void remove(final String key) {} + + @Override + public void removeAll(final Iterable keys) {} } 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 1a565960354..6392cb6ca12 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.asSubclass(LoggerContextFactory.class)); + } catch (final ReflectiveOperationException e) { + LOGGER.error("Failed to instantiate logger context factory {}.", implementation.getName(), e); + } + } + LOGGER.error("Falling back to simple logger context factory: {}", SimpleLoggerContextFactory.class.getName()); + return SimpleLoggerContextFactory.INSTANCE; } /** - * Gets the class name of the {@link ThreadContextMap} implementation of this Provider. - *

- * This method should return one of the internal implementations: - *

    - *
  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,17 +254,18 @@ public String getThreadContextMap() { * * @return the {@code ThreadContextMap} implementation class or {@code null} if unspecified or a loading error * occurred. - * @see #createThreadContextMap() */ - public Class loadThreadContextMap() { + public @Nullable Class loadThreadContextMap() { if (threadContextMapClass != null) { return threadContextMapClass; } - final String threadContextMap = getThreadContextMap(); + if (threadContextMap == null) { + return null; + } final ClassLoader loader = classLoader.get(); // Support for deprecated {@code META-INF/log4j-provider.properties} format. // In the remaining cases {@code loader == null}. - if (loader == null || threadContextMap == null) { + if (loader == null) { return null; } try { @@ -358,78 +276,24 @@ public Class loadThreadContextMap() { LOGGER.error( "Class {} specified in {} does not extend {}", threadContextMap, - getUrl(), + url, ThreadContextMap.class.getName()); } } catch (final Exception e) { - LOGGER.error("Unable to load class {} specified in {}", threadContextMap, url.toString(), e); + LOGGER.error("Unable to load class {} specified in {}", threadContextMap, url, e); } return null; } - /** - * Creates a {@link ThreadContextMap} using the legacy {@link #loadThreadContextMap()} and - * {@link #getThreadContextMap()} methods: - *
    - *
  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 PropertiesUtil props = PropertiesUtil.getProperties(); + return props.getBooleanProperty(DISABLE_CONTEXT_MAP) || props.getBooleanProperty(DISABLE_THREAD_CONTEXT) + ? NoOpThreadContextMap.INSTANCE + : DefaultThreadContextMap.INSTANCE; } /** @@ -447,7 +311,7 @@ public ScopedContextProvider getScopedContextProvider() { * @deprecated since 2.24.0, without replacement. */ @Deprecated - public URL getUrl() { + public @Nullable URL getUrl() { return url; } @@ -455,7 +319,7 @@ public URL getUrl() { public String toString() { final StringBuilder result = new StringBuilder("Provider '").append(getClass().getName()).append("'"); - if (!DEFAULT_PRIORITY.equals(priority)) { + if (priority != DEFAULT_PRIORITY) { result.append("\n\tpriority = ").append(priority); } final String threadContextMap = getThreadContextMap(); @@ -485,24 +349,18 @@ public boolean equals(final Object o) { if (this == o) { return true; } - if (!(o instanceof Provider)) { - return false; + if (o instanceof Provider) { + final Provider provider = (Provider) o; + return Objects.equals(priority, provider.priority) + && Objects.equals(className, provider.className) + && Objects.equals(loggerContextFactoryClass, provider.loggerContextFactoryClass) + && Objects.equals(versions, provider.versions); } - - final Provider provider = (Provider) o; - - return Objects.equals(priority, provider.priority) - && Objects.equals(className, provider.className) - && Objects.equals(loggerContextFactoryClass, provider.loggerContextFactoryClass) - && Objects.equals(versions, provider.versions); + return false; } @Override public int hashCode() { - int result = priority != null ? priority.hashCode() : 0; - result = 31 * result + (className != null ? className.hashCode() : 0); - result = 31 * result + (loggerContextFactoryClass != null ? loggerContextFactoryClass.hashCode() : 0); - result = 31 * result + (versions != null ? versions.hashCode() : 0); - return result; + return Objects.hash(priority, className, loggerContextFactoryClass, versions); } } diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap.java index f0af90ddbb2..26290da51d7 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap.java @@ -18,6 +18,9 @@ import java.util.Map; import org.apache.logging.log4j.ThreadContext; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.osgi.annotation.versioning.ProviderType; /** * Service provider interface to implement custom MDC behavior for {@link org.apache.logging.log4j.ThreadContext}. @@ -26,6 +29,8 @@ * are accessible to applications via the {@link ThreadContext#getThreadContextMap()} method. *

*/ +@ProviderType +@NullMarked public interface ThreadContextMap { /** @@ -35,18 +40,20 @@ public interface ThreadContextMap { /** * Determines if the key is in the context. - * @param key The key to locate. + * @param key The key to locate, not {@code null}. * @return True if the key is in the context, false otherwise. */ boolean containsKey(final String key); /** - * Gets the context identified by the key parameter. - * - *

This method has no side effects.

- * @param key The key to locate. - * @return The value associated with the key or null. + * Gets the {@link String} value for the specified key if it exists or {@code null}. + *

+ * This method has no side effects. + *

+ * @param key The key to locate, not {@code null}. + * @return The value associated with the key or {@code null}. */ + @Nullable String get(final String key); /** @@ -59,6 +66,7 @@ public interface ThreadContextMap { * Returns an immutable view on the context Map or {@code null} if the context map is empty. * @return an immutable context Map or {@code null}. */ + @Nullable Map getImmutableMapOrNull(); /** @@ -68,21 +76,43 @@ public interface ThreadContextMap { boolean isEmpty(); /** - * Puts a context value (the o parameter) as identified - * with the key parameter into the current thread's - * context map. - * - *

If the current thread does not have a context map it is - * created as a side effect.

- * @param key The key name. - * @param value The key value. + * Sets the context value for the given key in the current thread's context map. + *

+ * If the current thread does not have a context map it is created as a side effect. + *

+ * @param key The key to add, not {@code null}. + * @param value The value to add, not {@code null}. */ void put(final String key, final String value); /** - * Removes the context identified by the key - * parameter. - * @param key The key to remove. + * Puts all given context map entries into the current thread's context map. + *

+ * If the current thread does not have a context map it is created as a side effect. + *

+ * @param map The map to add, not {@code null}. + * @since 2.24.0 + */ + default void putAll(final Map map) { + map.forEach(this::put); + } + + /** + * Removes the context entry corresponding to the given key. + * @param key The key to remove, not {@code null}. */ void remove(final String key); + + /** + * Removes all given context map keys from the current thread's context map. + *

+ * If the current thread does not have a context map it is created as a side effect. + *

+ * + * @param keys The keys, {@code null}. + * @since 2.24.0 + */ + default void removeAll(final Iterable keys) { + keys.forEach(this::remove); + } } diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java index b00bd979858..418beb4583c 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java @@ -48,12 +48,12 @@ public final class ThreadContextMapFactory { * and when Log4j is reconfigured. */ public static void init() { - ProviderUtil.getProvider().resetThreadContextMap(); + ProviderUtil.getProvider().getThreadContextMapInstance(); } private ThreadContextMapFactory() {} public static ThreadContextMap createThreadContextMap() { - return ProviderUtil.getProvider().createThreadContextMap(); + return ProviderUtil.getProvider().getThreadContextMapInstance(); } } diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/ThreadContextTestAccess.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/ThreadContextTestAccess.java deleted file mode 100644 index ecf3e9f6c69..00000000000 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/ThreadContextTestAccess.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to you under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.logging.log4j; - -/** - *

- * Utility class to access package protected methods in {@code ThreadContext}. - *

- * - * @see ThreadContext - * @since 2.7 - */ -public final class ThreadContextTestAccess { - private ThreadContextTestAccess() { // prevent instantiation - } - - public static void init() { - ThreadContext.init(); - } -} diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AbstractAsyncThreadContextTestBase.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AbstractAsyncThreadContextTestBase.java index a98bce9fdc0..29ddda9717b 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AbstractAsyncThreadContextTestBase.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AbstractAsyncThreadContextTestBase.java @@ -29,15 +29,16 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.ThreadContext; -import org.apache.logging.log4j.ThreadContextTestAccess; import org.apache.logging.log4j.core.config.ConfigurationFactory; import org.apache.logging.log4j.core.config.LoggerConfig; +import org.apache.logging.log4j.core.context.CopyOnWriteSortedArrayThreadContextMap; +import org.apache.logging.log4j.core.context.GarbageFreeSortedArrayThreadContextMap; +import org.apache.logging.log4j.core.context.StringArrayThreadContextMap; import org.apache.logging.log4j.core.impl.Log4jContextFactory; import org.apache.logging.log4j.core.jmx.RingBufferAdmin; import org.apache.logging.log4j.core.selector.ClassLoaderContextSelector; import org.apache.logging.log4j.core.selector.ContextSelector; import org.apache.logging.log4j.core.test.CoreLoggerContexts; -import org.apache.logging.log4j.spi.DefaultThreadContextMap; import org.apache.logging.log4j.spi.LoggerContext; import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap; import org.apache.logging.log4j.test.TestProperties; @@ -89,26 +90,24 @@ void initConfigFile() { } protected enum ContextImpl { - WEBAPP, - GARBAGE_FREE, - COPY_ON_WRITE; + WEBAPP("WebApp", StringArrayThreadContextMap.class), + GARBAGE_FREE("GarbageFree", GarbageFreeSortedArrayThreadContextMap.class), + COPY_ON_WRITE("CopyOnWrite", CopyOnWriteSortedArrayThreadContextMap.class); + + private final String threadContextMap; + private final Class implClass; + + ContextImpl(final String threadContextMap, final Class implClass) { + this.threadContextMap = threadContextMap; + this.implClass = implClass; + } void init() { - final String PACKAGE = "org.apache.logging.log4j.spi."; - props.setProperty("log4j2.threadContextMap", PACKAGE + implClassSimpleName()); - ThreadContextTestAccess.init(); + props.setProperty("log4j2.threadContextMap", threadContextMap); } - public String implClassSimpleName() { - switch (this) { - case WEBAPP: - return DefaultThreadContextMap.class.getSimpleName(); - case GARBAGE_FREE: - return "GarbageFreeSortedArrayThreadContextMap"; - case COPY_ON_WRITE: - return "CopyOnWriteSortedArrayThreadContextMap"; - } - throw new IllegalStateException("Unknown state " + this); + public String getImplClassSimpleName() { + return implClass.getSimpleName(); } } @@ -123,8 +122,8 @@ private void init(final ContextImpl contextImpl, final Mode asyncMode) { } else { assertThat(ThreadContext.getThreadContextMap()) .isNotNull() - .extracting(o -> o.getClass().getSimpleName()) - .isEqualTo(contextImpl.implClassSimpleName()); + .extracting(Object::getClass) + .isEqualTo(contextImpl.implClass); } } @@ -197,25 +196,25 @@ protected void testAsyncLogWritesToLog(final ContextImpl contextImpl, final Mode private static String contextMap() { final ReadOnlyThreadContextMap impl = ThreadContext.getThreadContextMap(); return impl == null - ? ContextImpl.WEBAPP.implClassSimpleName() + ? ContextImpl.WEBAPP.getImplClassSimpleName() : impl.getClass().getSimpleName(); } private void checkResult(final Path file, final String loggerContextName, final ContextImpl contextImpl) throws IOException { - final String contextDesc = contextImpl + " " + contextImpl.implClassSimpleName() + " " + loggerContextName; + final String contextDesc = contextImpl + " " + contextImpl.getImplClassSimpleName() + " " + loggerContextName; try (final BufferedReader reader = Files.newBufferedReader(file)) { String expect; for (int i = 0; i < LINE_COUNT; i++) { final String line = reader.readLine(); if ((i & 1) == 1) { - expect = - "INFO c.f.Bar mapvalue [stackvalue] {KEY=mapvalue, configProp=configValue, configProp2=configValue2, count=" - + i + "} " + contextDesc + " i=" + i; + expect = "INFO c.f.Bar mapvalue [stackvalue] {KEY=mapvalue, configProp=configValue," + + " configProp2=configValue2, count=" + + i + "} " + contextDesc + " i=" + i; } else { - expect = - "INFO c.f.Bar mapvalue [stackvalue] {KEY=mapvalue, configProp=configValue, configProp2=configValue2} " - + contextDesc + " i=" + i; + expect = "INFO c.f.Bar mapvalue [stackvalue] {KEY=mapvalue, configProp=configValue," + + " configProp2=configValue2} " + + contextDesc + " i=" + i; } assertThat(line).as("Log file '%s'", file.getFileName()).isEqualTo(expect); } diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMapTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/StringArrayThreadContextMapTest.java similarity index 92% rename from log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMapTest.java rename to log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/StringArrayThreadContextMapTest.java index 2f845d8bf73..d71186ff14b 100644 --- a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMapTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/StringArrayThreadContextMapTest.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.logging.log4j.internal.map; +package org.apache.logging.log4j.core.context; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -26,6 +26,7 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import org.apache.logging.log4j.core.context.internal.UnmodifiableArrayBackedMap; import org.apache.logging.log4j.test.junit.UsingThreadContextMap; import org.apache.logging.log4j.util.TriConsumer; import org.junit.jupiter.api.Test; @@ -36,23 +37,6 @@ @UsingThreadContextMap public class StringArrayThreadContextMapTest { - @Test - public void testEqualsVsSameKind() { - final StringArrayThreadContextMap map1 = createMap(); - final StringArrayThreadContextMap map2 = createMap(); - assertEquals(map1, map1); - assertEquals(map2, map2); - assertEquals(map1, map2); - assertEquals(map2, map1); - } - - @Test - public void testHashCodeVsSameKind() { - final StringArrayThreadContextMap map1 = createMap(); - final StringArrayThreadContextMap map2 = createMap(); - assertEquals(map1.hashCode(), map2.hashCode()); - } - @Test public void testGet() { final StringArrayThreadContextMap map1 = createMap(); @@ -99,7 +83,7 @@ public void testPutAll() { /** * Test method for - * {@link org.apache.logging.log4j.internal.map.StringArrayThreadContextMap#remove(java.lang.String)} + * {@link StringArrayThreadContextMap#remove(java.lang.String)} * . */ @Test diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextMapTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/ThreadContextMapTest.java similarity index 62% rename from log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextMapTest.java rename to log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/ThreadContextMapTest.java index f394e71bea0..aff6dd63a42 100644 --- a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextMapTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/ThreadContextMapTest.java @@ -14,26 +14,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.logging.log4j.spi; +package org.apache.logging.log4j.core.context; -import static org.assertj.core.api.Assertions.assertThat; - -import java.time.Duration; import java.util.Properties; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.stream.Stream; +import org.apache.logging.log4j.spi.ThreadContextMap; +import org.apache.logging.log4j.test.spi.AbstractThreadContextMapTest; import org.apache.logging.log4j.util.PropertiesUtil; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -class ThreadContextMapTest { - - private static final String KEY = "key"; +class ThreadContextMapTest extends AbstractThreadContextMapTest { static Stream defaultMaps() { return Stream.of( - new DefaultThreadContextMap(), + new StringArrayThreadContextMap(), new CopyOnWriteSortedArrayThreadContextMap(), new GarbageFreeSortedArrayThreadContextMap()); } @@ -43,7 +38,7 @@ static Stream inheritableMaps() { props.setProperty("log4j2.isThreadContextMapInheritable", "true"); final PropertiesUtil util = new PropertiesUtil(props); return Stream.of( - new DefaultThreadContextMap(true, util), + new StringArrayThreadContextMap(util), new CopyOnWriteSortedArrayThreadContextMap(util), new GarbageFreeSortedArrayThreadContextMap(util)); } @@ -51,26 +46,12 @@ static Stream inheritableMaps() { @ParameterizedTest @MethodSource("defaultMaps") void threadLocalNotInheritableByDefault(final ThreadContextMap contextMap) { - contextMap.put(KEY, "threadLocalNotInheritableByDefault"); - verifyThreadContextValueFromANewThread(contextMap, null); + assertThreadLocalNotInheritable(contextMap); } @ParameterizedTest @MethodSource("inheritableMaps") void threadLocalInheritableIfConfigured(final ThreadContextMap contextMap) { - contextMap.put(KEY, "threadLocalInheritableIfConfigured"); - verifyThreadContextValueFromANewThread(contextMap, "threadLocalInheritableIfConfigured"); - } - - private static void verifyThreadContextValueFromANewThread( - final ThreadContextMap contextMap, final String expected) { - final ExecutorService executorService = Executors.newSingleThreadExecutor(); - try { - assertThat(executorService.submit(() -> contextMap.get(KEY))) - .succeedsWithin(Duration.ofSeconds(1)) - .isEqualTo(expected); - } finally { - executorService.shutdown(); - } + assertThreadLocalInheritable(contextMap); } } diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMapTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/internal/UnmodifiableArrayBackedMapTest.java similarity index 99% rename from log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMapTest.java rename to log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/internal/UnmodifiableArrayBackedMapTest.java index 9652034d70f..9362311306e 100644 --- a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMapTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/context/internal/UnmodifiableArrayBackedMapTest.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.logging.log4j.internal.map; +package org.apache.logging.log4j.core.context.internal; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java index 94654608239..03c3be708f6 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java @@ -18,7 +18,6 @@ import static java.util.Arrays.asList; import static java.util.concurrent.Executors.newSingleThreadExecutor; -import static org.apache.logging.log4j.ThreadContext.getThreadContextMap; import static org.apache.logging.log4j.core.impl.ContextDataInjectorFactory.createInjector; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.allOf; @@ -33,9 +32,9 @@ import java.util.concurrent.ExecutionException; import org.apache.logging.log4j.ThreadContext; import org.apache.logging.log4j.core.ContextDataInjector; -import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap; -import org.apache.logging.log4j.test.ThreadContextUtilityClass; +import org.apache.logging.log4j.spi.ThreadContextMap; import org.apache.logging.log4j.util.PropertiesUtil; +import org.apache.logging.log4j.util.ProviderUtil; import org.apache.logging.log4j.util.SortedArrayStringMap; import org.apache.logging.log4j.util.StringMap; import org.junit.After; @@ -51,26 +50,24 @@ public class ThreadContextDataInjectorTest { @Parameters(name = "{0}") public static Collection threadContextMapClassNames() { return asList(new String[][] { - { - "org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap", - "org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap" - }, - { - "org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap", - "org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap" - }, - {"org.apache.logging.log4j.spi.DefaultThreadContextMap", null} + {"org.apache.logging.log4j.core.context.CopyOnWriteSortedArrayThreadContextMap"}, + {"org.apache.logging.log4j.core.context.GarbageFreeSortedArrayThreadContextMap"}, + {"org.apache.logging.log4j.core.context.StringArrayThreadContextMap"} }); } @Parameter public String threadContextMapClassName; - @Parameter(value = 1) - public String readOnlythreadContextMapClassName; + private static void resetThreadContextMap() { + PropertiesUtil.getProperties().reload(); + final Log4jProvider provider = (Log4jProvider) ProviderUtil.getProvider(); + provider.resetThreadContextMap(); + ThreadContext.init(); + } @Before - public void before() { + public void before() throws ReflectiveOperationException { System.setProperty("log4j2.threadContextMap", threadContextMapClassName); } @@ -83,13 +80,11 @@ public void after() { } private void testContextDataInjector() { - final ReadOnlyThreadContextMap readOnlythreadContextMap = getThreadContextMap(); + final ThreadContextMap threadContextMap = ProviderUtil.getProvider().getThreadContextMapInstance(); assertThat( "thread context map class name", - (readOnlythreadContextMap == null) - ? null - : readOnlythreadContextMap.getClass().getName(), - is(equalTo(readOnlythreadContextMapClassName))); + threadContextMap.getClass().getName(), + is(equalTo(threadContextMapClassName))); final ContextDataInjector contextDataInjector = createInjector(true); final StringMap stringMap = contextDataInjector.injectContextData(null, new SortedArrayStringMap()); @@ -122,9 +117,8 @@ private void testContextDataInjector() { private void prepareThreadContext(final boolean isThreadContextMapInheritable) { System.setProperty("log4j2.isThreadContextMapInheritable", Boolean.toString(isThreadContextMapInheritable)); - PropertiesUtil.getProperties().reload(); - ThreadContextUtilityClass.reset(); - ThreadContext.remove("baz"); + resetThreadContextMap(); + ThreadContext.clearMap(); ThreadContext.put("foo", "bar"); } diff --git a/log4j-core/pom.xml b/log4j-core/pom.xml index 05a6de655d0..1d376c994bc 100644 --- a/log4j-core/pom.xml +++ b/log4j-core/pom.xml @@ -53,6 +53,8 @@ --> true + + org.jspecify.*;resolution:=optional, com.conversantmedia.util.concurrent;resolution:=optional; com.fasterxml.jackson.*;resolution:=optional, @@ -122,6 +124,11 @@ 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/AbstractSortedArrayThreadContextMap.java similarity index 58% rename from log4j-api/src/main/java/org/apache/logging/log4j/spi/GarbageFreeSortedArrayThreadContextMap.java rename to log4j-core/src/main/java/org/apache/logging/log4j/core/context/AbstractSortedArrayThreadContextMap.java index 1622a0c455b..7dcf7d23b1b 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/GarbageFreeSortedArrayThreadContextMap.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/context/AbstractSortedArrayThreadContextMap.java @@ -14,33 +14,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.logging.log4j.spi; +package org.apache.logging.log4j.core.context; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import org.apache.logging.log4j.spi.ObjectThreadContextMap; +import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap; +import org.apache.logging.log4j.spi.ThreadContextMap2; import org.apache.logging.log4j.util.PropertiesUtil; import org.apache.logging.log4j.util.ReadOnlyStringMap; import org.apache.logging.log4j.util.SortedArrayStringMap; import org.apache.logging.log4j.util.StringMap; +import org.jspecify.annotations.Nullable; /** - * {@code SortedArrayStringMap}-based implementation of the {@code ThreadContextMap} interface that attempts not to - * create temporary objects. Adding and removing key-value pairs will not create temporary objects. - *

- * This implementation does not make a copy of its contents on every operation, so this data structure cannot - * be passed to log events. Instead, client code needs to copy the contents when interacting with another thread. - *

- * @since 2.7 + * Commons base class for {@link StringMap}-based implementations of {@code ThreadContextMap}. + * @since 2.24.0 */ -class GarbageFreeSortedArrayThreadContextMap implements ReadOnlyThreadContextMap, ObjectThreadContextMap { +public abstract class AbstractSortedArrayThreadContextMap implements ReadOnlyThreadContextMap, ObjectThreadContextMap { /** * 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 static final String INHERITABLE_MAP = "isThreadContextMapInheritable"; /** * The default initial capacity. @@ -52,25 +51,35 @@ class GarbageFreeSortedArrayThreadContextMap implements ReadOnlyThreadContextMap */ protected static final String PROPERTY_NAME_INITIAL_CAPACITY = "log4j2.ThreadContext.initial.capacity"; - private final int initialCapacity; - protected final ThreadLocal localMap; + private static final StringMap EMPTY_CONTEXT_DATA = new SortedArrayStringMap(1); - public GarbageFreeSortedArrayThreadContextMap() { - this(PropertiesUtil.getProperties()); + static { + EMPTY_CONTEXT_DATA.freeze(); } - GarbageFreeSortedArrayThreadContextMap(final PropertiesUtil properties) { + private final int initialCapacity; + protected final ThreadLocal<@Nullable StringMap> localMap; + + protected AbstractSortedArrayThreadContextMap(final PropertiesUtil properties) { initialCapacity = properties.getIntegerProperty(PROPERTY_NAME_INITIAL_CAPACITY, DEFAULT_INITIAL_CAPACITY); localMap = properties.getBooleanProperty(INHERITABLE_MAP) ? new InheritableThreadLocal() { @Override - protected StringMap childValue(final StringMap parentValue) { - return parentValue != null ? createStringMap(parentValue) : null; + protected @Nullable StringMap childValue(final @Nullable StringMap parentValue) { + return copyStringMap(parentValue); } } : new ThreadLocal<>(); } + /** + * Creates a copy of {@link StringMap} used to back this thread context map. + * + * @param value the context data of the current thread. + * @return the context data for a new thread. + */ + protected abstract @Nullable StringMap copyStringMap(@Nullable StringMap value); + /** * Returns an implementation of the {@code StringMap} used to back this thread context map. *

@@ -95,8 +104,12 @@ protected StringMap createStringMap(final ReadOnlyStringMap original) { return new SortedArrayStringMap(original); } - private StringMap getThreadLocalMap() { - StringMap map = localMap.get(); + protected @Nullable StringMap getMutableLocalMapOrNull() { + return localMap.get(); + } + + protected StringMap getMutableLocalMap() { + StringMap map = getMutableLocalMapOrNull(); if (map == null) { map = createStringMap(); localMap.set(map); @@ -105,51 +118,44 @@ private StringMap getThreadLocalMap() { } @Override - public void put(final String key, final String value) { - getThreadLocalMap().putValue(key, value); + public final void put(final String key, final String value) { + putValue(key, value); } @Override public void putValue(final String key, final Object value) { - getThreadLocalMap().putValue(key, value); + getMutableLocalMap().putValue(key, value); } @Override - public void putAll(final Map values) { - if (values == null || values.isEmpty()) { - return; - } - final StringMap map = getThreadLocalMap(); - for (final Map.Entry entry : values.entrySet()) { - map.putValue(entry.getKey(), entry.getValue()); - } + public final void putAll(final Map values) { + putAllValues(values); } @Override public void putAllValues(final Map values) { - if (values == null || values.isEmpty()) { + if (values.isEmpty()) { return; } - final StringMap map = getThreadLocalMap(); - for (final Map.Entry entry : values.entrySet()) { - map.putValue(entry.getKey(), entry.getValue()); - } + final StringMap map = getMutableLocalMap(); + values.forEach(map::putValue); } @Override - public String get(final String key) { - return (String) getValue(key); + public final @Nullable String get(final String key) { + final Object value = getValue(key); + return value != null ? value.toString() : null; } @Override - public V getValue(final String key) { + public final @Nullable V getValue(final String key) { final StringMap map = localMap.get(); - return map == null ? null : map.getValue(key); + return map == null ? null : (V) map.getValue(key); } @Override public void remove(final String key) { - final StringMap map = localMap.get(); + final StringMap map = getMutableLocalMapOrNull(); if (map != null) { map.remove(key); } @@ -157,19 +163,9 @@ public void remove(final String key) { @Override public void removeAll(final Iterable keys) { - final StringMap map = localMap.get(); + final StringMap map = getMutableLocalMapOrNull(); if (map != null) { - for (final String key : keys) { - map.remove(key); - } - } - } - - @Override - public void clear() { - final StringMap map = localMap.get(); - if (map != null) { - map.clear(); + keys.forEach(map::remove); } } @@ -185,21 +181,14 @@ public Map getCopy() { return map == null ? new HashMap<>() : map.toMap(); } - /** - * {@inheritDoc} - */ @Override public StringMap getReadOnlyContextData() { - StringMap map = localMap.get(); - if (map == null) { - map = createStringMap(); - localMap.set(map); - } - return map; + final StringMap map = localMap.get(); + return map == null ? EMPTY_CONTEXT_DATA : map; } @Override - public Map getImmutableMapOrNull() { + public @Nullable Map getImmutableMapOrNull() { final StringMap map = localMap.get(); return map == null ? null : Collections.unmodifiableMap(map.toMap()); } @@ -218,27 +207,13 @@ public String toString() { @Override public int hashCode() { - final int prime = 31; - int result = 1; - final StringMap map = this.localMap.get(); - result = prime * result + ((map == null) ? 0 : map.hashCode()); - return result; + return Objects.hashCode(localMap.get()); } @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof ThreadContextMap)) { - return false; - } - final ThreadContextMap other = (ThreadContextMap) obj; - final Map map = this.getImmutableMapOrNull(); - final Map otherMap = other.getImmutableMapOrNull(); - return Objects.equals(map, otherMap); + public boolean equals(final @Nullable Object obj) { + return obj == this + || obj instanceof ObjectThreadContextMap + && Objects.equals(getReadOnlyContextData(), ((ThreadContextMap2) obj).getReadOnlyContextData()); } } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/context/CopyOnWriteSortedArrayThreadContextMap.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/context/CopyOnWriteSortedArrayThreadContextMap.java new file mode 100644 index 00000000000..e8a0114fd00 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/context/CopyOnWriteSortedArrayThreadContextMap.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.log4j.core.context; + +import java.util.Map; +import org.apache.logging.log4j.spi.CopyOnWrite; +import org.apache.logging.log4j.util.PropertiesUtil; +import org.apache.logging.log4j.util.StringMap; +import org.jspecify.annotations.Nullable; + +/** + * {@code SortedArrayStringMap}-based implementation of the {@code ThreadContextMap} interface that creates a copy of + * the data structure on every modification. Any particular instance of the data structure is a snapshot of the + * ThreadContext at some point in time and can safely be passed off to other threads. Since it is + * expected that the Map will be passed to many more log events than the number of keys it contains the performance + * should be much better than if the Map was copied for each event. + * + * @since 2.7 + */ +public class CopyOnWriteSortedArrayThreadContextMap extends AbstractSortedArrayThreadContextMap implements CopyOnWrite { + + public CopyOnWriteSortedArrayThreadContextMap() { + this(PropertiesUtil.getProperties()); + } + + CopyOnWriteSortedArrayThreadContextMap(final PropertiesUtil properties) { + super(properties); + } + + @Override + protected @Nullable StringMap copyStringMap(@Nullable StringMap value) { + if (value != null) { + final StringMap stringMap = createStringMap(value); + stringMap.freeze(); + return stringMap; + } + return null; + } + + @Override + protected @Nullable StringMap getMutableLocalMapOrNull() { + final StringMap map = localMap.get(); + if (map != null) { + final StringMap mutableMap = createStringMap(map); + localMap.set(mutableMap); + return mutableMap; + } + return null; + } + + private void freezeLocalMap() { + final StringMap map = localMap.get(); + if (map != null) { + map.freeze(); + } + } + + @Override + public void putValue(final String key, final Object value) { + super.putValue(key, value); + freezeLocalMap(); + } + + @Override + public void putAllValues(final Map values) { + super.putAllValues(values); + freezeLocalMap(); + } + + @Override + public void remove(final String key) { + super.remove(key); + freezeLocalMap(); + } + + @Override + public void removeAll(final Iterable keys) { + super.removeAll(keys); + freezeLocalMap(); + } + + @Override + public void clear() { + localMap.remove(); + } +} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/context/GarbageFreeSortedArrayThreadContextMap.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/context/GarbageFreeSortedArrayThreadContextMap.java new file mode 100644 index 00000000000..0eddbea2fb1 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/context/GarbageFreeSortedArrayThreadContextMap.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.log4j.core.context; + +import org.apache.logging.log4j.util.PropertiesUtil; +import org.apache.logging.log4j.util.StringMap; +import org.jspecify.annotations.Nullable; + +/** + * {@code SortedArrayStringMap}-based implementation of the {@code ThreadContextMap} interface that attempts not to + * create temporary objects. Adding and removing key-value pairs will not create temporary objects. + *

+ * This implementation does not make a copy of its contents on every operation, so this data structure cannot + * be passed to log events. Instead, client code needs to copy the contents when interacting with another thread. + *

+ * @since 2.7 + */ +public class GarbageFreeSortedArrayThreadContextMap extends AbstractSortedArrayThreadContextMap { + + public GarbageFreeSortedArrayThreadContextMap() { + this(PropertiesUtil.getProperties()); + } + + GarbageFreeSortedArrayThreadContextMap(final PropertiesUtil properties) { + super(properties); + } + + @Override + protected @Nullable StringMap copyStringMap(@Nullable StringMap value) { + return value != null ? createStringMap(value) : null; + } + + @Override + public void clear() { + final StringMap map = localMap.get(); + if (map != null) { + map.clear(); + } + } +} diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/context/StringArrayThreadContextMap.java similarity index 71% rename from log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java rename to log4j-core/src/main/java/org/apache/logging/log4j/core/context/StringArrayThreadContextMap.java index 608ca77ffb4..89a0ba7628d 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/context/StringArrayThreadContextMap.java @@ -14,15 +14,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.logging.log4j.internal.map; +package org.apache.logging.log4j.core.context; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import org.apache.logging.log4j.core.context.internal.UnmodifiableArrayBackedMap; import org.apache.logging.log4j.spi.ThreadContextMap; import org.apache.logging.log4j.util.BiConsumer; +import org.apache.logging.log4j.util.PropertiesUtil; import org.apache.logging.log4j.util.ReadOnlyStringMap; import org.apache.logging.log4j.util.TriConsumer; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * An equivalent for DefaultThreadContxtMap, except that it's backed by @@ -34,6 +39,7 @@ * linearly with the current map size, and callers are advised to minimize this * work. */ +@NullMarked public class StringArrayThreadContextMap implements ThreadContextMap, ReadOnlyStringMap { private static final long serialVersionUID = -2635197170958057849L; @@ -41,32 +47,44 @@ public class StringArrayThreadContextMap implements ThreadContextMap, ReadOnlySt * 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 static final String INHERITABLE_MAP = "isThreadContextMapInheritable"; - private ThreadLocal threadLocalMapState; + private ThreadLocal localMap; public StringArrayThreadContextMap() { - threadLocalMapState = new ThreadLocal<>(); + this(PropertiesUtil.getProperties()); + } + + StringArrayThreadContextMap(final PropertiesUtil properties) { + localMap = properties.getBooleanProperty(INHERITABLE_MAP) + ? new InheritableThreadLocal() { + @Override + protected Object @Nullable [] childValue(final Object @Nullable [] parentValue) { + return parentValue != null ? Arrays.copyOf(parentValue, parentValue.length) : null; + } + } + : new ThreadLocal<>(); } @Override public void put(final String key, final String value) { - final Object[] state = threadLocalMapState.get(); + final Object[] state = localMap.get(); final UnmodifiableArrayBackedMap modifiedMap = UnmodifiableArrayBackedMap.getInstance(state).copyAndPut(key, value); - threadLocalMapState.set(modifiedMap.getBackingArray()); + localMap.set(modifiedMap.getBackingArray()); } + @Override public void putAll(final Map m) { - final Object[] state = threadLocalMapState.get(); + final Object[] state = localMap.get(); final UnmodifiableArrayBackedMap modifiedMap = UnmodifiableArrayBackedMap.getInstance(state).copyAndPutAll(m); - threadLocalMapState.set(modifiedMap.getBackingArray()); + localMap.set(modifiedMap.getBackingArray()); } @Override - public String get(final String key) { - final Object[] state = threadLocalMapState.get(); + public @Nullable String get(final String key) { + final Object[] state = localMap.get(); if (state == null) { return null; } @@ -75,26 +93,27 @@ public String get(final String key) { @Override public void remove(final String key) { - final Object[] state = threadLocalMapState.get(); + final Object[] state = localMap.get(); if (state != null) { final UnmodifiableArrayBackedMap modifiedMap = UnmodifiableArrayBackedMap.getInstance(state).copyAndRemove(key); - threadLocalMapState.set(modifiedMap.getBackingArray()); + localMap.set(modifiedMap.getBackingArray()); } } + @Override public void removeAll(final Iterable keys) { - final Object[] state = threadLocalMapState.get(); + final Object[] state = localMap.get(); if (state != null) { final UnmodifiableArrayBackedMap modifiedMap = UnmodifiableArrayBackedMap.getInstance(state).copyAndRemoveAll(keys); - threadLocalMapState.set(modifiedMap.getBackingArray()); + localMap.set(modifiedMap.getBackingArray()); } } @Override public void clear() { - threadLocalMapState.remove(); + localMap.remove(); } @Override @@ -104,13 +123,13 @@ public Map toMap() { @Override public boolean containsKey(final String key) { - final Object[] state = threadLocalMapState.get(); - return (state == null ? false : (UnmodifiableArrayBackedMap.getInstance(state)).containsKey(key)); + final Object @Nullable [] state = localMap.get(); + return (state != null && (UnmodifiableArrayBackedMap.getInstance(state)).containsKey(key)); } @Override public void forEach(final BiConsumer action) { - final Object[] state = threadLocalMapState.get(); + final Object[] state = localMap.get(); if (state == null) { return; } @@ -120,7 +139,7 @@ public void forEach(final BiConsumer action) { @Override public void forEach(final TriConsumer action, final S state) { - final Object[] localState = threadLocalMapState.get(); + final Object[] localState = localMap.get(); if (localState == null) { return; } @@ -130,13 +149,13 @@ public void forEach(final TriConsumer action, final @SuppressWarnings("unchecked") @Override - public V getValue(final String key) { + public @Nullable V getValue(final String key) { return (V) get(key); } @Override public Map getCopy() { - final Object[] state = threadLocalMapState.get(); + final Object[] state = localMap.get(); if (state == null) { return new HashMap<>(0); } @@ -144,8 +163,8 @@ public Map getCopy() { } @Override - public Map getImmutableMapOrNull() { - final Object[] state = threadLocalMapState.get(); + public @Nullable Map getImmutableMapOrNull() { + final Object[] state = localMap.get(); return (state == null ? null : UnmodifiableArrayBackedMap.getInstance(state)); } @@ -156,13 +175,13 @@ public boolean isEmpty() { @Override public int size() { - final Object[] state = threadLocalMapState.get(); + final Object[] state = localMap.get(); return UnmodifiableArrayBackedMap.getInstance(state).size(); } @Override public String toString() { - final Object[] state = threadLocalMapState.get(); + final Object[] state = localMap.get(); return state == null ? "{}" : UnmodifiableArrayBackedMap.getInstance(state).toString(); @@ -172,7 +191,7 @@ public String toString() { public int hashCode() { final int prime = 31; int result = 1; - final Object[] state = threadLocalMapState.get(); + final Object[] state = localMap.get(); result = prime * result + ((state == null) ? 0 @@ -192,7 +211,7 @@ public boolean equals(final Object obj) { return false; } final ThreadContextMap other = (ThreadContextMap) obj; - final Map map = UnmodifiableArrayBackedMap.getInstance(this.threadLocalMapState.get()); + final Map map = UnmodifiableArrayBackedMap.getInstance(this.localMap.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-core/src/main/java/org/apache/logging/log4j/core/context/internal/UnmodifiableArrayBackedMap.java similarity index 96% rename from log4j-api/src/main/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMap.java rename to log4j-core/src/main/java/org/apache/logging/log4j/core/context/internal/UnmodifiableArrayBackedMap.java index 84d2868b911..a356345e706 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMap.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/context/internal/UnmodifiableArrayBackedMap.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.logging.log4j.internal.map; +package org.apache.logging.log4j.core.context.internal; import java.io.Serializable; import java.util.AbstractMap; @@ -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 getInstance(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) { @@ -270,12 +270,8 @@ UnmodifiableArrayBackedMap copyAndPut(String key, String value) { /** * Creates a new instance that contains the same entries as this map, plus the * new entries or updated values passed in the parameters. - * - * @param key - * @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()); @@ -304,12 +300,8 @@ UnmodifiableArrayBackedMap copyAndPutAll(Map entriesToAdd) { /** * Creates a new instance that contains the same entries as this map, minus the * entry with the specified key (if such an entry exists). - * - * @param key - * @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() @@ -351,12 +343,8 @@ UnmodifiableArrayBackedMap copyAndRemove(String key) { /** * Creates a new instance that contains the same entries as this map, minus all * of the keys passed in the arguments. - * - * @param 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-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 2142cf96dbe..5f922ef86fe 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 @@ -20,22 +20,145 @@ import aQute.bnd.annotation.spi.ServiceConsumer; import aQute.bnd.annotation.spi.ServiceProvider; import java.util.ServiceLoader; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.context.CopyOnWriteSortedArrayThreadContextMap; +import org.apache.logging.log4j.core.context.GarbageFreeSortedArrayThreadContextMap; +import org.apache.logging.log4j.core.context.StringArrayThreadContextMap; import org.apache.logging.log4j.core.impl.internal.QueuedScopedContextProvider; +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.apache.logging.log4j.util.ServiceLoaderUtil; +import org.jspecify.annotations.NullMarked; /** * 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}. + *

+ * 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 copy-on-write implementation of {@link ThreadContextMap}. + *

+ * Warning: the value of this constant does not point to a concrete class name. + *

+ * @see #getThreadContextMap + */ + private 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 + */ + 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"; + + private static final Logger LOGGER = StatusLogger.getLogger(); + + private final Lazy loggerContextFactoryLazy = Lazy.lazy(Log4jContextFactory::new); + private final Lazy threadContextMapLazy = Lazy.lazy(Log4jProvider::createThreadContextMap); + public Log4jProvider() { super(10, CURRENT_VERSION, Log4jContextFactory.class); } + @Override + public LoggerContextFactory getLoggerContextFactory() { + return loggerContextFactoryLazy.get(); + } + + @Override + public ThreadContextMap getThreadContextMapInstance() { + return threadContextMapLazy.get(); + } + + private static 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 new NoOpThreadContextMap(); + case WEB_APP_CONTEXT_MAP: + return new StringArrayThreadContextMap(); + 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(); + 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 ScopedContextProvider getScopedContextProvider() { return ServiceLoaderUtil.safeStream( @@ -45,4 +168,9 @@ public ScopedContextProvider getScopedContextProvider() { .findFirst() .orElse(QueuedScopedContextProvider.INSTANCE); } + + // Used in tests + void resetThreadContextMap() { + threadContextMapLazy.set(null); + } } diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/SortedArrayVsHashMapBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/SortedArrayVsHashMapBenchmark.java index 8f1b29d192f..5290d2f279b 100644 --- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/SortedArrayVsHashMapBenchmark.java +++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/SortedArrayVsHashMapBenchmark.java @@ -209,7 +209,7 @@ public int putArrayContextData() { @Benchmark public int putHashContextData() { - openHashMapContextData.put("someKey", "someValue"); + openHashMapContextData.putValue("someKey", "someValue"); return openHashMapContextData.size(); } diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/nogc/OpenHashStringMap.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/nogc/OpenHashStringMap.java index 2e26a3b1523..fa5f430c2b8 100644 --- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/nogc/OpenHashStringMap.java +++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/nogc/OpenHashStringMap.java @@ -20,12 +20,10 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.Arrays; -import java.util.Collections; import java.util.ConcurrentModificationException; 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.StringMap; @@ -59,7 +57,7 @@ * * @since 2.7 */ -public class OpenHashStringMap implements StringMap, ThreadContextMap { +public class OpenHashStringMap implements StringMap { /** The initial default size of a hash table. */ public static final int DEFAULT_INITIAL_SIZE = 16; @@ -382,11 +380,6 @@ public void forEach(final TriConsumer a } } - @Override - public String get(final String key) { - return (String) getObjectValue(key); - } - @SuppressWarnings("unchecked") private V getObjectValue(final Object k) { if (k == null) { @@ -413,16 +406,6 @@ private V getObjectValue(final Object k) { } } - @Override - public Map getCopy() { - return toMap(); - } - - @Override - public Map getImmutableMapOrNull() { - return isEmpty() ? null : Collections.unmodifiableMap(toMap()); - } - @Override public VAL getValue(final String key) { return (VAL) getObjectValue(key); @@ -433,12 +416,6 @@ public boolean isEmpty() { return size == 0; } - @Override - @SuppressWarnings("unchecked") - public void put(final String key, final String value) { - putObjectValue((K) key, (V) value); - } - private int insert(final K k, final V v) { int pos; if (k == null) { @@ -482,7 +459,6 @@ public void putAll(final ReadOnlyStringMap source) { } } - /** {@inheritDoc} */ public void putAll(final Map map) { if (loadFactor <= .5) { // The resulting map will be sized for m.size() elements 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 index f43717494f9..74d9ee195b9 100644 --- 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 @@ -16,6 +16,7 @@ */ package org.apache.logging.log4j.spi; +import org.apache.logging.log4j.core.context.CopyOnWriteSortedArrayThreadContextMap; 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-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..f6341f011cb 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.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/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..fad1fa0abbb 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,7 +19,9 @@ 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; /** * Bind the Log4j API to JUL. @@ -28,7 +30,7 @@ */ @ServiceProvider(value = Provider.class, resolution = Resolution.OPTIONAL) public class JULProvider extends Provider { - private static final LoggerContextFactory CONTEXT_FACTORY = new JULLoggerContextFactory(); + private final LoggerContextFactory CONTEXT_FACTORY = new JULLoggerContextFactory(); public JULProvider() { super(20, CURRENT_VERSION); @@ -40,8 +42,7 @@ public LoggerContextFactory getLoggerContextFactory() { } @Override - public String getThreadContextMap() { - // JUL does not provide an MDC implementation - return NO_OP_CONTEXT_MAP; + public ThreadContextMap getThreadContextMapInstance() { + return NoOpThreadContextMap.INSTANCE; } } 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..f826fffecf6 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 @@ -28,8 +28,8 @@ @ServiceProvider(value = Provider.class, resolution = Resolution.OPTIONAL) public class SLF4JProvider extends Provider { - private static final LoggerContextFactory CONTEXT_FACTORY = new SLF4JLoggerContextFactory(); - private static final ThreadContextMap THREAD_CONTEXT_MAP = new MDCContextMap(); + private final LoggerContextFactory CONTEXT_FACTORY = new SLF4JLoggerContextFactory(); + private final ThreadContextMap THREAD_CONTEXT_MAP = new MDCContextMap(); public SLF4JProvider() { super(15, CURRENT_VERSION); 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..ed966860c25 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 `ThreadContextMap` for web app users: `org.apache.logging.log4j.core.context.StringArrayThreadContextMap`.