You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by ma...@apache.org on 2022/10/16 23:09:44 UTC

[logging-log4j2] 01/02: Extract PropertyEnvironment interface from PropertiesUtil

This is an automated email from the ASF dual-hosted git repository.

mattsicker pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 7bb33a00dab31e0c531493e1f375765caa9c461e
Author: Matt Sicker <ma...@apache.org>
AuthorDate: Sun Oct 16 13:47:41 2022 -0500

    Extract PropertyEnvironment interface from PropertiesUtil
    
    Related to https://cwiki.apache.org/confluence/display/LOGGING/Properties+Enhancement
    
    Signed-off-by: Matt Sicker <ma...@apache.org>
---
 .../java/org/apache/logging/log4j/LogManager.java  |   3 +-
 .../org/apache/logging/log4j/ThreadContext.java    |  23 +-
 .../apache/logging/log4j/simple/SimpleLogger.java  |  16 +-
 .../logging/log4j/simple/SimpleLoggerContext.java  |  13 +-
 .../CopyOnWriteSortedArrayThreadContextMap.java    |   3 +-
 .../logging/log4j/spi/DefaultThreadContextMap.java |   3 +-
 .../GarbageFreeSortedArrayThreadContextMap.java    |   3 +-
 .../logging/log4j/spi/ThreadContextMapFactory.java |   5 +-
 .../apache/logging/log4j/status/StatusLogger.java  |  27 +-
 .../apache/logging/log4j/util/PropertiesUtil.java  | 282 ++------------------
 .../logging/log4j/util/PropertyEnvironment.java    | 291 +++++++++++++++++++++
 .../async/AsyncQueueFullPolicyFactoryTest.java     |   4 +-
 .../log4j/core/async/AsyncThreadContextTest.java   |   2 +-
 .../core/impl/ThreadContextDataInjectorTest.java   |  30 +--
 .../log4j/core/appender/ConsoleAppender.java       |   5 +-
 .../core/async/AsyncQueueFullPolicyFactory.java    |   5 +-
 .../log4j/core/config/ConfigurationFactory.java    |   4 +-
 .../core/config/DefaultConfigurationFactory.java   |   3 +-
 .../logging/log4j/core/impl/DefaultBundle.java     |   5 +-
 .../logging/log4j/core/layout/PatternLayout.java   |   7 +-
 .../log4j/core/net/UrlConnectionFactory.java       |  21 +-
 .../core/net/ssl/SslConfigurationFactory.java      |   5 +-
 .../core/util/BasicAuthorizationProvider.java      |  10 +-
 .../apache/logging/log4j/docker/DockerLookup.java  |   3 +-
 .../jpa/appender/AbstractJpaAppenderTest.java      |  20 +-
 .../template/json/JsonTemplateLayoutDefaults.java  |   3 +-
 .../plugins/condition/OnPropertyConditionTest.java |   6 +-
 27 files changed, 431 insertions(+), 371 deletions(-)

diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/LogManager.java b/log4j-api/src/main/java/org/apache/logging/log4j/LogManager.java
index aaf1ba84c8..b3fcb51f18 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/LogManager.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/LogManager.java
@@ -28,6 +28,7 @@ import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.Lazy;
 import org.apache.logging.log4j.util.LoaderUtil;
 import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
 import org.apache.logging.log4j.util.ProviderUtil;
 import org.apache.logging.log4j.util.StackLocatorUtil;
 import org.apache.logging.log4j.util.Strings;
@@ -70,7 +71,7 @@ public class LogManager {
      */
     private static final Lazy<LoggerContextFactory> PROVIDER = Lazy.lazy(() -> {
         // Shortcut binding to force a specific logging implementation.
-        final PropertiesUtil managerProps = PropertiesUtil.getProperties();
+        final PropertyEnvironment managerProps = PropertiesUtil.getProperties();
         final String factoryClassName = managerProps.getStringProperty(FACTORY_PROPERTY_NAME);
         if (factoryClassName != null) {
             try {
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 6f6e8b0748..c61ca0701c 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
@@ -17,26 +17,27 @@
 
 package org.apache.logging.log4j;
 
-import java.io.Serializable;
-import java.util.AbstractCollection;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.NoSuchElementException;
-
 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.NoOpThreadContextMap;
 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.CleanableThreadContextMap;
 import org.apache.logging.log4j.spi.ThreadContextMapFactory;
 import org.apache.logging.log4j.spi.ThreadContextStack;
 import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
+
+import java.io.Serializable;
+import java.util.AbstractCollection;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
 
 /**
  * The ThreadContext allows applications to store information either in a Map or a Stack.
@@ -212,7 +213,7 @@ public final class ThreadContext {
     public static void init() {
         ThreadContextMapFactory.init();
         contextMap = null;
-        final PropertiesUtil managerProps = PropertiesUtil.getProperties();
+        final PropertyEnvironment managerProps = PropertiesUtil.getProperties();
         final boolean disableAll = managerProps.getBooleanProperty(DISABLE_ALL);
         useStack = !(managerProps.getBooleanProperty(DISABLE_STACK) || disableAll);
         final boolean useMap = !(managerProps.getBooleanProperty(DISABLE_MAP) || disableAll);
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLogger.java b/log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLogger.java
index fe21b3a162..53d53473a2 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLogger.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLogger.java
@@ -16,21 +16,21 @@
  */
 package org.apache.logging.log4j.simple;
 
-import java.io.PrintStream;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Map;
-
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.MessageFactory;
 import org.apache.logging.log4j.spi.AbstractLogger;
-import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
 import org.apache.logging.log4j.util.Strings;
 
+import java.io.PrintStream;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Map;
+
 /**
  * This is the default logger that is used when no suitable logging implementation is available.
  */
@@ -60,7 +60,7 @@ public class SimpleLogger extends AbstractLogger {
 
     public SimpleLogger(final String name, final Level defaultLevel, final boolean showLogName,
             final boolean showShortLogName, final boolean showDateTime, final boolean showContextMap,
-            final String dateTimeFormat, final MessageFactory messageFactory, final PropertiesUtil props,
+            final String dateTimeFormat, final MessageFactory messageFactory, final PropertyEnvironment props,
             final PrintStream stream) {
         super(name, messageFactory);
         final String lvl = props.getStringProperty(SimpleLoggerContext.SYSTEM_PREFIX + name + ".level");
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLoggerContext.java b/log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLoggerContext.java
index ac14f37682..b83ed480a2 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLoggerContext.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLoggerContext.java
@@ -16,10 +16,6 @@
  */
 package org.apache.logging.log4j.simple;
 
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.PrintStream;
-
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.message.MessageFactory;
 import org.apache.logging.log4j.spi.AbstractLogger;
@@ -27,6 +23,11 @@ import org.apache.logging.log4j.spi.ExtendedLogger;
 import org.apache.logging.log4j.spi.LoggerContext;
 import org.apache.logging.log4j.spi.LoggerRegistry;
 import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
+
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.PrintStream;
 
 /**
  * A simple {@link LoggerContext} implementation.
@@ -46,7 +47,7 @@ public class SimpleLoggerContext implements LoggerContext {
     /** All system properties used by <code>SimpleLog</code> start with this */
     protected static final String SYSTEM_PREFIX = "org.apache.logging.log4j.simplelog.";
 
-    private final PropertiesUtil props;
+    private final PropertyEnvironment props;
 
     /** Include the instance name in the log message? */
     private final boolean showLogName;
@@ -76,7 +77,7 @@ public class SimpleLoggerContext implements LoggerContext {
      * Constructs a new initialized instance.
      */
     public SimpleLoggerContext() {
-        props = new PropertiesUtil("log4j2.simplelog.properties");
+        props = PropertiesUtil.getProperties("simplelog");
 
         showContextMap = props.getBooleanProperty(SYSTEM_PREFIX + "showContextMap", false);
         showLogName = props.getBooleanProperty(SYSTEM_PREFIX + "showlogname", false);
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
index b3fa93918c..4c291d830e 100644
--- 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
@@ -19,6 +19,7 @@ package org.apache.logging.log4j.spi;
 import org.apache.logging.log4j.util.LazyBoolean;
 import org.apache.logging.log4j.util.LazyInt;
 import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
 import org.apache.logging.log4j.util.ReadOnlyStringMap;
 import org.apache.logging.log4j.util.SortedArrayStringMap;
 import org.apache.logging.log4j.util.StringMap;
@@ -66,7 +67,7 @@ class CopyOnWriteSortedArrayThreadContextMap implements ReadOnlyThreadContextMap
      * Initializes static variables based on system properties. Normally called when this class is initialized by the VM
      * and when Log4j is reconfigured.
      */
-    static void init(final PropertiesUtil properties) {
+    static void init(final PropertyEnvironment properties) {
         initialCapacity.setAsInt(properties.getIntegerProperty(PROPERTY_NAME_INITIAL_CAPACITY, DEFAULT_INITIAL_CAPACITY));
         inheritableMap.setAsBoolean(properties.getBooleanProperty(INHERITABLE_MAP));
     }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java
index 314db45c4f..7865fef75a 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
@@ -19,6 +19,7 @@ package org.apache.logging.log4j.spi;
 import org.apache.logging.log4j.util.BiConsumer;
 import org.apache.logging.log4j.util.LazyBoolean;
 import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
 import org.apache.logging.log4j.util.ReadOnlyStringMap;
 import org.apache.logging.log4j.util.TriConsumer;
 
@@ -65,7 +66,7 @@ public class DefaultThreadContextMap implements ThreadContextMap, ReadOnlyString
         return new ThreadLocal<>();
     }
 
-    static void init(final PropertiesUtil properties) {
+    static void init(final PropertyEnvironment properties) {
         inheritableMap.setAsBoolean(properties.getBooleanProperty(INHERITABLE_MAP));
     }
     
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/GarbageFreeSortedArrayThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/GarbageFreeSortedArrayThreadContextMap.java
index 28562682dc..108a12a8c6 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/GarbageFreeSortedArrayThreadContextMap.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/GarbageFreeSortedArrayThreadContextMap.java
@@ -19,6 +19,7 @@ package org.apache.logging.log4j.spi;
 import org.apache.logging.log4j.util.LazyBoolean;
 import org.apache.logging.log4j.util.LazyInt;
 import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
 import org.apache.logging.log4j.util.ReadOnlyStringMap;
 import org.apache.logging.log4j.util.SortedArrayStringMap;
 import org.apache.logging.log4j.util.StringMap;
@@ -66,7 +67,7 @@ class GarbageFreeSortedArrayThreadContextMap implements ReadOnlyThreadContextMap
      * Initializes static variables based on system properties. Normally called when this class is initialized by the VM
      * and when Log4j is reconfigured.
      */
-    static void init(final PropertiesUtil properties) {
+    static void init(final PropertyEnvironment properties) {
         initialCapacity.setAsInt(properties.getIntegerProperty(PROPERTY_NAME_INITIAL_CAPACITY, DEFAULT_INITIAL_CAPACITY));
         inheritableMap.setAsBoolean(properties.getBooleanProperty(INHERITABLE_MAP));
     }
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 60d69b35b8..a6f07cc5e0 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
@@ -22,6 +22,7 @@ import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.Constants;
 import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
 import org.apache.logging.log4j.util.ProviderUtil;
 import org.apache.logging.log4j.util.ReflectionUtil;
 
@@ -63,7 +64,7 @@ public final class ThreadContextMapFactory {
      * and when Log4j is reconfigured.
      */
     public static void init() {
-        final PropertiesUtil properties = PropertiesUtil.getProperties();
+        final PropertyEnvironment properties = PropertiesUtil.getProperties();
         CopyOnWriteSortedArrayThreadContextMap.init(properties);
         GarbageFreeSortedArrayThreadContextMap.init(properties);
         DefaultThreadContextMap.init(properties);
@@ -74,7 +75,7 @@ public final class ThreadContextMapFactory {
      * Initializes static variables based on system properties. Normally called when this class is initialized by the VM
      * and when Log4j is reconfigured.
      */
-    private static void initPrivate(final PropertiesUtil properties) {
+    private static void initPrivate(final PropertyEnvironment properties) {
         ThreadContextMapName = properties.getStringProperty(THREAD_CONTEXT_KEY);
         GcFreeThreadContextKey = properties.getBooleanProperty(GC_FREE_THREAD_CONTEXT_KEY);
     }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java b/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java
index 86eee2acf3..92e94f9d80 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java
@@ -16,6 +16,19 @@
  */
 package org.apache.logging.log4j.status;
 
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.Marker;
+import org.apache.logging.log4j.message.Message;
+import org.apache.logging.log4j.message.MessageFactory;
+import org.apache.logging.log4j.message.ParameterizedNoReferenceMessageFactory;
+import org.apache.logging.log4j.simple.SimpleLogger;
+import org.apache.logging.log4j.simple.SimpleLoggerContext;
+import org.apache.logging.log4j.spi.AbstractLogger;
+import org.apache.logging.log4j.util.Constants;
+import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
+import org.apache.logging.log4j.util.Strings;
+
 import java.io.Closeable;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -29,18 +42,6 @@ import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 
-import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.Marker;
-import org.apache.logging.log4j.message.Message;
-import org.apache.logging.log4j.message.MessageFactory;
-import org.apache.logging.log4j.message.ParameterizedNoReferenceMessageFactory;
-import org.apache.logging.log4j.simple.SimpleLogger;
-import org.apache.logging.log4j.simple.SimpleLoggerContext;
-import org.apache.logging.log4j.spi.AbstractLogger;
-import org.apache.logging.log4j.util.Constants;
-import org.apache.logging.log4j.util.PropertiesUtil;
-import org.apache.logging.log4j.util.Strings;
-
 /**
  * Records events that occur in the logging system. By default, only error messages are logged to {@link System#err}.
  * Normally, the Log4j StatusLogger is configured via the root {@code <Configuration status="LEVEL"/>} node in a Log4j
@@ -75,7 +76,7 @@ public final class StatusLogger extends AbstractLogger {
 
     private static final String NOT_AVAIL = "?";
 
-    private static final PropertiesUtil PROPS = new PropertiesUtil("log4j2.StatusLogger.properties");
+    private static final PropertyEnvironment PROPS = PropertiesUtil.getProperties("StatusLogger");
 
     private static final int MAX_ENTRIES = PROPS.getIntegerProperty(MAX_STATUS_ENTRIES, 200);
 
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertiesUtil.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertiesUtil.java
index 1f215de209..354edfbffe 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertiesUtil.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertiesUtil.java
@@ -19,7 +19,6 @@ package org.apache.logging.log4j.util;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.invoke.MethodHandles;
-import java.nio.charset.Charset;
 import java.time.Duration;
 import java.time.temporal.ChronoUnit;
 import java.time.temporal.TemporalUnit;
@@ -48,7 +47,7 @@ import java.util.concurrent.ConcurrentSkipListSet;
  *
  * @see PropertySource
  */
-public final class PropertiesUtil {
+public final class PropertiesUtil implements PropertyEnvironment {
 
     private static final String LOG4J_PROPERTIES_FILE_NAME = "log4j2.component.properties";
     private static final String LOG4J_SYSTEM_PROPERTIES_FILE_NAME = "log4j2.system.properties";
@@ -118,14 +117,15 @@ public final class PropertiesUtil {
      *
      * @return the main Log4j PropertiesUtil instance.
      */
-    public static PropertiesUtil getProperties() {
+    public static PropertyEnvironment getProperties() {
         return COMPONENT_PROPERTIES.value();
     }
 
-    /**
-     * Allows a PropertySource to be added after PropertiesUtil has been created.
-     * @param propertySource the PropertySource to add.
-     */
+    public static PropertyEnvironment getProperties(final String namespace) {
+        return new Environment(new PropertyFilePropertySource(String.format("log4j2.%s.properties", namespace)));
+    }
+
+    @Override
     public void addPropertySource(PropertySource propertySource) {
         if (environment != null) {
             environment.addPropertySource(propertySource);
@@ -138,103 +138,9 @@ public final class PropertiesUtil {
      * @param name the name of the property to verify
      * @return {@code true} if the specified property is defined, regardless of its value
      */
+    @Override
     public boolean hasProperty(final String name) {
-        return environment.containsKey(name);
-    }
-
-    /**
-     * Gets the named property as a boolean value. If the property matches the string {@code "true"} (case-insensitive),
-     * then it is returned as the boolean value {@code true}. Any other non-{@code null} text in the property is
-     * considered {@code false}.
-     *
-     * @param name the name of the property to look up
-     * @return the boolean value of the property or {@code false} if undefined.
-     */
-    public boolean getBooleanProperty(final String name) {
-        return getBooleanProperty(name, false);
-    }
-
-    /**
-     * Gets the named property as a boolean value.
-     *
-     * @param name         the name of the property to look up
-     * @param defaultValue the default value to use if the property is undefined
-     * @return the boolean value of the property or {@code defaultValue} if undefined.
-     */
-    public boolean getBooleanProperty(final String name, final boolean defaultValue) {
-        final String prop = getStringProperty(name);
-        return prop == null ? defaultValue : "true".equalsIgnoreCase(prop);
-    }
-
-    /**
-     * Gets the named property as a boolean value.
-     *
-     * @param name                  the name of the property to look up
-     * @param defaultValueIfAbsent  the default value to use if the property is undefined
-     * @param defaultValueIfPresent the default value to use if the property is defined but not assigned
-     * @return the boolean value of the property or {@code defaultValue} if undefined.
-     */
-    public boolean getBooleanProperty(final String name, final boolean defaultValueIfAbsent,
-                                      final boolean defaultValueIfPresent) {
-        final String prop = getStringProperty(name);
-        return prop == null ? defaultValueIfAbsent
-            : prop.isEmpty() ? defaultValueIfPresent : "true".equalsIgnoreCase(prop);
-    }
-
-    /**
-     * Retrieves a property that may be prefixed by more than one string.
-     * @param prefixes The array of prefixes.
-     * @param key The key to locate.
-     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
-     * if no property is found.
-     * @return The value or null if it is not found.
-     * @since 2.13.0
-     */
-    public Boolean getBooleanProperty(final String[] prefixes, final String key, final Supplier<Boolean> supplier) {
-        for (final String prefix : prefixes) {
-            if (hasProperty(prefix + key)) {
-                return getBooleanProperty(prefix + key);
-            }
-        }
-        return supplier != null ? supplier.get() : null;
-    }
-
-    /**
-     * Gets the named property as a Charset value.
-     *
-     * @param name the name of the property to look up
-     * @return the Charset value of the property or {@link Charset#defaultCharset()} if undefined.
-     */
-    public Charset getCharsetProperty(final String name) {
-        return getCharsetProperty(name, Charset.defaultCharset());
-    }
-
-    /**
-     * Gets the named property as a Charset value. If we cannot find the named Charset, see if it is mapped in
-     * file {@code Log4j-charsets.properties} on the class path.
-     *
-     * @param name         the name of the property to look up
-     * @param defaultValue the default value to use if the property is undefined
-     * @return the Charset value of the property or {@code defaultValue} if undefined.
-     */
-    public Charset getCharsetProperty(final String name, final Charset defaultValue) {
-        final String charsetName = getStringProperty(name);
-        if (charsetName == null) {
-            return defaultValue;
-        }
-        if (Charset.isSupported(charsetName)) {
-            return Charset.forName(charsetName);
-        }
-        final ResourceBundle bundle = getCharsetsResourceBundle();
-        if (bundle.containsKey(name)) {
-            final String mapped = bundle.getString(name);
-            if (Charset.isSupported(mapped)) {
-                return Charset.forName(mapped);
-            }
-        }
-        LowLevelLogUtil.log("Unable to get Charset '" + charsetName + "' for property '" + name + "', using default "
-            + defaultValue + " and continuing.");
-        return defaultValue;
+        return environment.hasProperty(name);
     }
 
     /**
@@ -256,153 +162,15 @@ public final class PropertiesUtil {
         return defaultValue;
     }
 
-    /**
-     * Gets the named property as an integer.
-     *
-     * @param name         the name of the property to look up
-     * @param defaultValue the default value to use if the property is undefined
-     * @return the parsed integer value of the property or {@code defaultValue} if it was undefined or could not be
-     * parsed.
-     */
-    public int getIntegerProperty(final String name, final int defaultValue) {
-        final String prop = getStringProperty(name);
-        if (prop != null) {
-            try {
-                return Integer.parseInt(prop);
-            } catch (final Exception ignored) {
-                return defaultValue;
-            }
-        }
-        return defaultValue;
-    }
-
-    /**
-     * Retrieves a property that may be prefixed by more than one string.
-     * @param prefixes The array of prefixes.
-     * @param key The key to locate.
-     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
-     * if no property is found.
-     * @return The value or null if it is not found.
-     * @since 2.13.0
-     */
-    public Integer getIntegerProperty(final String[] prefixes, final String key, final Supplier<Integer> supplier) {
-        for (final String prefix : prefixes) {
-            if (hasProperty(prefix + key)) {
-                return getIntegerProperty(prefix + key, 0);
-            }
-        }
-        return supplier != null ? supplier.get() : null;
-    }
-
-    /**
-     * Gets the named property as a long.
-     *
-     * @param name         the name of the property to look up
-     * @param defaultValue the default value to use if the property is undefined
-     * @return the parsed long value of the property or {@code defaultValue} if it was undefined or could not be parsed.
-     */
-    public long getLongProperty(final String name, final long defaultValue) {
-        final String prop = getStringProperty(name);
-        if (prop != null) {
-            try {
-                return Long.parseLong(prop);
-            } catch (final Exception ignored) {
-                return defaultValue;
-            }
-        }
-        return defaultValue;
-    }
-    /**
-     * Retrieves a property that may be prefixed by more than one string.
-     * @param prefixes The array of prefixes.
-     * @param key The key to locate.
-     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
-     * if no property is found.
-     * @return The value or null if it is not found.
-     * @since 2.13.0
-     */
-    public Long getLongProperty(final String[] prefixes, final String key, final Supplier<Long> supplier) {
-        for (final String prefix : prefixes) {
-            if (hasProperty(prefix + key)) {
-                return getLongProperty(prefix + key, 0);
-            }
-        }
-        return supplier != null ? supplier.get() : null;
-    }
-
-    /**
-     * Retrieves a Duration where the String is of the format nnn[unit] where nnn represents an integer value
-     * and unit represents a time unit.
-     * @param name The property name.
-     * @param defaultValue The default value.
-     * @return The value of the String as a Duration or the default value, which may be null.
-     * @since 2.13.0
-     */
-    public Duration getDurationProperty(final String name, final Duration defaultValue) {
-        final String prop = getStringProperty(name);
-        if (prop != null) {
-            return TimeUnit.getDuration(prop);
-        }
-        return defaultValue;
-    }
-
-    /**
-     * Retrieves a property that may be prefixed by more than one string.
-     * @param prefixes The array of prefixes.
-     * @param key The key to locate.
-     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
-     * if no property is found.
-     * @return The value or null if it is not found.
-     * @since 2.13.0
-     */
-    public Duration getDurationProperty(final String[] prefixes, final String key, final Supplier<Duration> supplier) {
-        for (final String prefix : prefixes) {
-            if (hasProperty(prefix + key)) {
-                return getDurationProperty(prefix + key, null);
-            }
-        }
-        return supplier != null ? supplier.get() : null;
-    }
-
-    /**
-     * Retrieves a property that may be prefixed by more than one string.
-     * @param prefixes The array of prefixes.
-     * @param key The key to locate.
-     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
-     * if no property is found.
-     * @return The value or null if it is not found.
-     * @since 2.13.0
-     */
-    public String getStringProperty(final String[] prefixes, final String key, final Supplier<String> supplier) {
-        for (final String prefix : prefixes) {
-            final String result = getStringProperty(prefix + key);
-            if (result != null) {
-                return result;
-            }
-        }
-        return supplier != null ? supplier.get() : null;
-    }
-
     /**
      * Gets the named property as a String.
      *
      * @param name the name of the property to look up
      * @return the String value of the property or {@code null} if undefined.
      */
+    @Override
     public String getStringProperty(final String name) {
-        return environment.get(name);
-    }
-
-    /**
-     * Gets the named property as a String.
-     *
-     * @param name         the name of the property to look up
-     * @param defaultValue the default value to use if the property is undefined
-     * @return the String value of the property or {@code defaultValue} if undefined.
-     */
-    public String getStringProperty(final String name, final String defaultValue) {
-        final String prop = getStringProperty(name);
-        return (prop == null) ? defaultValue : prop;
+        return environment.getStringProperty(name);
     }
 
     /**
@@ -442,7 +210,7 @@ public final class PropertiesUtil {
      *
      * @since 2.10.0
      */
-    private static class Environment {
+    private static class Environment implements PropertyEnvironment {
 
         private final Set<PropertySource> sources = new ConcurrentSkipListSet<>(new PropertySource.Comparator());
         /**
@@ -473,11 +241,8 @@ public final class PropertiesUtil {
             reload();
         }
 
-        /**
-         * Allow a PropertySource to be added.
-         * @param propertySource The PropertySource to add.
-         */
-        public void addPropertySource(PropertySource propertySource) {
+        @Override
+        public void addPropertySource(final PropertySource propertySource) {
             sources.add(propertySource);
         }
 
@@ -489,7 +254,7 @@ public final class PropertiesUtil {
             final Set<String> keys = new HashSet<>();
             sources.stream()
                    .map(PropertySource::getPropertyNames)
-                   .forEach(sourceKeys -> keys.addAll(sourceKeys));
+                   .forEach(keys::addAll);
             // 2. Fills the property caches. Sources with higher priority values don't override the previous ones.
             keys.stream()
                 .filter(Objects::nonNull)
@@ -514,7 +279,8 @@ public final class PropertiesUtil {
                 });
         }
 
-        private String get(final String key) {
+        @Override
+        public String getStringProperty(final String key) {
             if (normalized.containsKey(key)) {
                 return normalized.get(key);
             }
@@ -537,7 +303,8 @@ public final class PropertiesUtil {
             return tokenized.get(tokens);
         }
 
-        private boolean containsKey(final String key) {
+        @Override
+        public boolean hasProperty(final String key) {
             List<CharSequence> tokens = PropertySource.Util.tokenize(key);
             return normalized.containsKey(key) ||
                    literal.containsKey(key) ||
@@ -629,16 +396,7 @@ public final class PropertiesUtil {
         return parts;
     }
 
-    /**
-     * Returns true if system properties tell us we are running on Windows.
-     *
-     * @return true if system properties tell us we are running on Windows.
-     */
-    public boolean isOsWindows() {
-        return getStringProperty("os.name", "").startsWith("Windows");
-    }
-
-    private enum TimeUnit {
+    enum TimeUnit {
         NANOS("ns,nano,nanos,nanosecond,nanoseconds", ChronoUnit.NANOS),
         MICROS("us,micro,micros,microsecond,microseconds", ChronoUnit.MICROS),
         MILLIS("ms,milli,millis,millsecond,milliseconds", ChronoUnit.MILLIS),
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertyEnvironment.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertyEnvironment.java
new file mode 100644
index 0000000000..910906fde9
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertyEnvironment.java
@@ -0,0 +1,291 @@
+/*
+ * 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.util;
+
+import java.nio.charset.Charset;
+import java.time.Duration;
+import java.util.ResourceBundle;
+
+public interface PropertyEnvironment {
+
+    /**
+     * Allows a PropertySource to be added after PropertiesUtil has been created.
+     * @param propertySource the PropertySource to add.
+     */
+    void addPropertySource(PropertySource propertySource);
+
+    /**
+     * Returns {@code true} if the specified property is defined, regardless of its value (it may not have a value).
+     *
+     * @param name the name of the property to verify
+     * @return {@code true} if the specified property is defined, regardless of its value
+     */
+    boolean hasProperty(String name);
+
+    /**
+     * Gets the named property as a boolean value. If the property matches the string {@code "true"} (case-insensitive),
+     * then it is returned as the boolean value {@code true}. Any other non-{@code null} text in the property is
+     * considered {@code false}.
+     *
+     * @param name the name of the property to look up
+     * @return the boolean value of the property or {@code false} if undefined.
+     */
+    default boolean getBooleanProperty(String name) {
+        return getBooleanProperty(name, false);
+    }
+
+    /**
+     * Gets the named property as a boolean value.
+     *
+     * @param name         the name of the property to look up
+     * @param defaultValue the default value to use if the property is undefined
+     * @return the boolean value of the property or {@code defaultValue} if undefined.
+     */
+    default boolean getBooleanProperty(String name, boolean defaultValue) {
+        final String prop = getStringProperty(name);
+        return prop == null ? defaultValue : "true".equalsIgnoreCase(prop);
+    }
+
+    /**
+     * Gets the named property as a boolean value.
+     *
+     * @param name                  the name of the property to look up
+     * @param defaultValueIfAbsent  the default value to use if the property is undefined
+     * @param defaultValueIfPresent the default value to use if the property is defined but not assigned
+     * @return the boolean value of the property or {@code defaultValue} if undefined.
+     */
+    default boolean getBooleanProperty(String name, boolean defaultValueIfAbsent,
+                                       boolean defaultValueIfPresent) {
+        final String prop = getStringProperty(name);
+        return prop == null ? defaultValueIfAbsent
+            : prop.isEmpty() ? defaultValueIfPresent : "true".equalsIgnoreCase(prop);
+    }
+
+    /**
+     * Retrieves a property that may be prefixed by more than one string.
+     * @param prefixes The array of prefixes.
+     * @param key The key to locate.
+     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
+     * if no property is found.
+     * @return The value or null if it is not found.
+     * @since 2.13.0
+     */
+    default Boolean getBooleanProperty(String[] prefixes, String key, Supplier<Boolean> supplier) {
+        for (final String prefix : prefixes) {
+            if (hasProperty(prefix + key)) {
+                return getBooleanProperty(prefix + key);
+            }
+        }
+        return supplier != null ? supplier.get() : null;
+    }
+
+    /**
+     * Gets the named property as a Charset value.
+     *
+     * @param name the name of the property to look up
+     * @return the Charset value of the property or {@link Charset#defaultCharset()} if undefined.
+     */
+    default Charset getCharsetProperty(String name) {
+        return getCharsetProperty(name, Charset.defaultCharset());
+    }
+
+    /**
+     * Gets the named property as a Charset value. If we cannot find the named Charset, see if it is mapped in
+     * file {@code Log4j-charsets.properties} on the class path.
+     *
+     * @param name         the name of the property to look up
+     * @param defaultValue the default value to use if the property is undefined
+     * @return the Charset value of the property or {@code defaultValue} if undefined.
+     */
+    default Charset getCharsetProperty(String name, Charset defaultValue) {
+        final String charsetName = getStringProperty(name);
+        if (charsetName == null) {
+            return defaultValue;
+        }
+        if (Charset.isSupported(charsetName)) {
+            return Charset.forName(charsetName);
+        }
+        final ResourceBundle bundle = PropertiesUtil.getCharsetsResourceBundle();
+        if (bundle.containsKey(name)) {
+            final String mapped = bundle.getString(name);
+            if (Charset.isSupported(mapped)) {
+                return Charset.forName(mapped);
+            }
+        }
+        LowLevelLogUtil.log("Unable to get Charset '" + charsetName + "' for property '" + name + "', using default "
+            + defaultValue + " and continuing.");
+        return defaultValue;
+    }
+
+    /**
+     * Gets the named property as an integer.
+     *
+     * @param name         the name of the property to look up
+     * @param defaultValue the default value to use if the property is undefined
+     * @return the parsed integer value of the property or {@code defaultValue} if it was undefined or could not be
+     * parsed.
+     */
+    default int getIntegerProperty(String name, int defaultValue) {
+        final String prop = getStringProperty(name);
+        if (prop != null) {
+            try {
+                return Integer.parseInt(prop);
+            } catch (final Exception ignored) {
+                return defaultValue;
+            }
+        }
+        return defaultValue;
+    }
+
+    /**
+     * Retrieves a property that may be prefixed by more than one string.
+     * @param prefixes The array of prefixes.
+     * @param key The key to locate.
+     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
+     * if no property is found.
+     * @return The value or null if it is not found.
+     * @since 2.13.0
+     */
+    default Integer getIntegerProperty(String[] prefixes, String key, Supplier<Integer> supplier) {
+        for (final String prefix : prefixes) {
+            if (hasProperty(prefix + key)) {
+                return getIntegerProperty(prefix + key, 0);
+            }
+        }
+        return supplier != null ? supplier.get() : null;
+    }
+
+    /**
+     * Gets the named property as a long.
+     *
+     * @param name         the name of the property to look up
+     * @param defaultValue the default value to use if the property is undefined
+     * @return the parsed long value of the property or {@code defaultValue} if it was undefined or could not be parsed.
+     */
+    default long getLongProperty(String name, long defaultValue) {
+        final String prop = getStringProperty(name);
+        if (prop != null) {
+            try {
+                return Long.parseLong(prop);
+            } catch (final Exception ignored) {
+                return defaultValue;
+            }
+        }
+        return defaultValue;
+    }
+
+    /**
+     * Retrieves a property that may be prefixed by more than one string.
+     * @param prefixes The array of prefixes.
+     * @param key The key to locate.
+     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
+     * if no property is found.
+     * @return The value or null if it is not found.
+     * @since 2.13.0
+     */
+    default Long getLongProperty(String[] prefixes, String key, Supplier<Long> supplier) {
+        for (final String prefix : prefixes) {
+            if (hasProperty(prefix + key)) {
+                return getLongProperty(prefix + key, 0);
+            }
+        }
+        return supplier != null ? supplier.get() : null;
+    }
+
+    /**
+     * Retrieves a Duration where the String is of the format nnn[unit] where nnn represents an integer value
+     * and unit represents a time unit.
+     * @param name The property name.
+     * @param defaultValue The default value.
+     * @return The value of the String as a Duration or the default value, which may be null.
+     * @since 2.13.0
+     */
+    default Duration getDurationProperty(String name, Duration defaultValue) {
+        final String prop = getStringProperty(name);
+        if (prop != null) {
+            return PropertiesUtil.TimeUnit.getDuration(prop);
+        }
+        return defaultValue;
+    }
+
+    /**
+     * Retrieves a property that may be prefixed by more than one string.
+     * @param prefixes The array of prefixes.
+     * @param key The key to locate.
+     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
+     * if no property is found.
+     * @return The value or null if it is not found.
+     * @since 2.13.0
+     */
+    default Duration getDurationProperty(String[] prefixes, String key, Supplier<Duration> supplier) {
+        for (final String prefix : prefixes) {
+            if (hasProperty(prefix + key)) {
+                return getDurationProperty(prefix + key, null);
+            }
+        }
+        return supplier != null ? supplier.get() : null;
+    }
+
+    /**
+     * Retrieves a property that may be prefixed by more than one string.
+     * @param prefixes The array of prefixes.
+     * @param key The key to locate.
+     * @param supplier The method to call to derive the default value. If the value is null, null will be returned
+     * if no property is found.
+     * @return The value or null if it is not found.
+     * @since 2.13.0
+     */
+    default String getStringProperty(String[] prefixes, String key, Supplier<String> supplier) {
+        for (final String prefix : prefixes) {
+            final String result = getStringProperty(prefix + key);
+            if (result != null) {
+                return result;
+            }
+        }
+        return supplier != null ? supplier.get() : null;
+    }
+
+    /**
+     * Gets the named property as a String.
+     *
+     * @param name the name of the property to look up
+     * @return the String value of the property or {@code null} if undefined.
+     */
+    String getStringProperty(String name);
+
+    /**
+     * Gets the named property as a String.
+     *
+     * @param name         the name of the property to look up
+     * @param defaultValue the default value to use if the property is undefined
+     * @return the String value of the property or {@code defaultValue} if undefined.
+     */
+    default String getStringProperty(String name, String defaultValue) {
+        final String prop = getStringProperty(name);
+        return (prop == null) ? defaultValue : prop;
+    }
+
+    /**
+     * Returns true if system properties tell us we are running on Windows.
+     *
+     * @return true if system properties tell us we are running on Windows.
+     */
+    default boolean isOsWindows() {
+        return getStringProperty("os.name", "").startsWith("Windows");
+    }
+}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AsyncQueueFullPolicyFactoryTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AsyncQueueFullPolicyFactoryTest.java
index 62db6cb23b..9589a01a9f 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AsyncQueueFullPolicyFactoryTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AsyncQueueFullPolicyFactoryTest.java
@@ -26,7 +26,7 @@ import org.junit.experimental.categories.Category;
 
 import java.util.Locale;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
 
 /**
  * Tests the AsyncQueueFullPolicyFactory class.
@@ -39,7 +39,7 @@ public class AsyncQueueFullPolicyFactoryTest {
     public void resetProperties() throws Exception {
         System.clearProperty(AsyncQueueFullPolicyFactory.PROPERTY_NAME_ASYNC_EVENT_ROUTER);
         System.clearProperty(AsyncQueueFullPolicyFactory.PROPERTY_NAME_DISCARDING_THRESHOLD_LEVEL);
-        PropertiesUtil.getProperties().reload();
+        ((PropertiesUtil) PropertiesUtil.getProperties()).reload();
     }
 
     @Test
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AsyncThreadContextTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AsyncThreadContextTest.java
index 767c0e6924..45201b94ce 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AsyncThreadContextTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AsyncThreadContextTest.java
@@ -94,7 +94,7 @@ public class AsyncThreadContextTest {
             System.clearProperty("log4j2.threadContextMap");
             final String PACKAGE = "org.apache.logging.log4j.spi.";
             System.setProperty("log4j2.threadContextMap", PACKAGE + implClassSimpleName());
-            PropertiesUtil.getProperties().reload();
+            ((PropertiesUtil) PropertiesUtil.getProperties()).reload();
             ThreadContextTestAccess.init();
         }
 
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 365f876593..bebafb261d 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
@@ -16,26 +16,10 @@
  */
 package org.apache.logging.log4j.core.impl;
 
-import static java.util.Arrays.asList;
-import static java.util.concurrent.Executors.newSingleThreadExecutor;
-import static org.apache.logging.log4j.ThreadContext.getThreadContextMap;
-import static org.apache.logging.log4j.core.impl.ContextDataInjectorFactory.createInjector;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.allOf;
-import static org.hamcrest.Matchers.empty;
-import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.hasEntry;
-import static org.hamcrest.Matchers.hasKey;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.not;
-
-import java.util.Collection;
-import java.util.concurrent.ExecutionException;
-
 import org.apache.logging.log4j.ThreadContext;
-import org.apache.logging.log4j.test.ThreadContextUtilityClass;
 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.util.PropertiesUtil;
 import org.apache.logging.log4j.util.SortedArrayStringMap;
 import org.apache.logging.log4j.util.StringMap;
@@ -47,6 +31,16 @@ import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
+import java.util.Collection;
+import java.util.concurrent.ExecutionException;
+
+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.*;
+
 @RunWith(Parameterized.class)
 public class ThreadContextDataInjectorTest {
     @Parameters(name = "{0}")
@@ -108,7 +102,7 @@ public class ThreadContextDataInjectorTest {
 
     private void prepareThreadContext(boolean isThreadContextMapInheritable) {
         System.setProperty("log4j2.isThreadContextMapInheritable", Boolean.toString(isThreadContextMapInheritable));
-        PropertiesUtil.getProperties().reload();
+        ((PropertiesUtil) PropertiesUtil.getProperties()).reload();
         ThreadContextUtilityClass.reset();
         ThreadContext.remove("baz");
         ThreadContext.put("foo", "bar");
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/ConsoleAppender.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/ConsoleAppender.java
index 9346f9c569..08a07418fc 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/ConsoleAppender.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/ConsoleAppender.java
@@ -30,6 +30,7 @@ import org.apache.logging.log4j.plugins.PluginFactory;
 import org.apache.logging.log4j.plugins.validation.constraints.Required;
 import org.apache.logging.log4j.util.Chars;
 import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
 
 import java.io.FileDescriptor;
 import java.io.FileOutputStream;
@@ -186,8 +187,8 @@ public final class ConsoleAppender extends AbstractOutputStreamAppender<OutputSt
         } catch (final UnsupportedEncodingException ex) { // should never happen
             throw new IllegalStateException("Unsupported default encoding " + enc, ex);
         }
-        final PropertiesUtil propsUtil = PropertiesUtil.getProperties();
-        if (!propsUtil.isOsWindows() || propsUtil.getBooleanProperty("log4j.skipJansi", true) || direct) {
+        final PropertyEnvironment properties = PropertiesUtil.getProperties();
+        if (!properties.isOsWindows() || properties.getBooleanProperty("log4j.skipJansi", true) || direct) {
             return outputStream;
         }
         try {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncQueueFullPolicyFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncQueueFullPolicyFactory.java
index c9a59e592c..b73f4034b4 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncQueueFullPolicyFactory.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncQueueFullPolicyFactory.java
@@ -21,6 +21,7 @@ import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.core.util.Loader;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
 
 /**
  * Creates {@link AsyncQueueFullPolicy} instances based on user-specified system properties. The {@code AsyncQueueFullPolicy}
@@ -99,8 +100,8 @@ public class AsyncQueueFullPolicyFactory {
     }
 
     private static AsyncQueueFullPolicy createDiscardingAsyncQueueFullPolicy() {
-        final PropertiesUtil util = PropertiesUtil.getProperties();
-        final String level = util.getStringProperty(PROPERTY_NAME_DISCARDING_THRESHOLD_LEVEL, Level.INFO.name());
+        final PropertyEnvironment properties = PropertiesUtil.getProperties();
+        final String level = properties.getStringProperty(PROPERTY_NAME_DISCARDING_THRESHOLD_LEVEL, Level.INFO.name());
         final Level thresholdLevel = Level.toLevel(level, Level.INFO);
         LOGGER.debug("Creating custom DiscardingAsyncQueueFullPolicy(discardThreshold:{})", thresholdLevel);
         return new DiscardingAsyncQueueFullPolicy(thresholdLevel);
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationFactory.java
index 39a3c2b5ec..d513da1076 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationFactory.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationFactory.java
@@ -29,7 +29,7 @@ import org.apache.logging.log4j.plugins.di.Key;
 import org.apache.logging.log4j.plugins.model.PluginNamespace;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.LoaderUtil;
-import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
 
 import java.net.URI;
 
@@ -121,7 +121,7 @@ public abstract class ConfigurationFactory extends ConfigurationBuilderFactory {
         return LoggerContext.getContext(false).getInjector().getInstance(KEY);
     }
 
-    public static AuthorizationProvider authorizationProvider(final PropertiesUtil props) {
+    public static AuthorizationProvider authorizationProvider(final PropertyEnvironment props) {
         final String authClass = props.getStringProperty(PREFIXES, "authorizationProvider", null);
         AuthorizationProvider provider = null;
         if (authClass != null) {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/DefaultConfigurationFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/DefaultConfigurationFactory.java
index 531cfd63e6..774509482a 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/DefaultConfigurationFactory.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/DefaultConfigurationFactory.java
@@ -27,6 +27,7 @@ import org.apache.logging.log4j.plugins.di.Injector;
 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.PropertyEnvironment;
 import org.apache.logging.log4j.util.Strings;
 
 import java.net.URI;
@@ -63,7 +64,7 @@ public class DefaultConfigurationFactory extends ConfigurationFactory {
     public Configuration getConfiguration(final LoggerContext loggerContext, final String name, final URI configLocation) {
 
         if (configLocation == null) {
-            final PropertiesUtil properties = PropertiesUtil.getProperties();
+            final PropertyEnvironment properties = PropertiesUtil.getProperties();
             final String configLocationStr = substitutor.replace(properties.getStringProperty(CONFIGURATION_FILE_PROPERTY));
             if (configLocationStr != null) {
                 final String[] sources = parseConfigLocations(configLocationStr);
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/DefaultBundle.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/DefaultBundle.java
index 32e2893dbd..3b03b75db1 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/DefaultBundle.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/DefaultBundle.java
@@ -57,6 +57,7 @@ import org.apache.logging.log4j.spi.DefaultThreadContextMap;
 import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
 
 import java.util.Map;
 import java.util.function.Supplier;
@@ -82,10 +83,10 @@ public class DefaultBundle {
     private static final Logger LOGGER = StatusLogger.getLogger();
 
     private final Injector injector;
-    private final PropertiesUtil properties;
+    private final PropertyEnvironment properties;
     private final ClassLoader classLoader;
 
-    public DefaultBundle(final Injector injector, final PropertiesUtil properties, final ClassLoader classLoader) {
+    public DefaultBundle(final Injector injector, final PropertyEnvironment properties, final ClassLoader classLoader) {
         this.injector = injector;
         this.properties = properties;
         this.classLoader = classLoader;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java
index 12fe627430..dfa94b0ce3 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java
@@ -32,6 +32,7 @@ import org.apache.logging.log4j.plugins.PluginBuilderAttribute;
 import org.apache.logging.log4j.plugins.PluginElement;
 import org.apache.logging.log4j.plugins.PluginFactory;
 import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
 import org.apache.logging.log4j.util.Strings;
 
 import java.nio.charset.Charset;
@@ -573,9 +574,9 @@ public final class PatternLayout extends AbstractStringLayout {
         }
 
         private boolean useAnsiEscapeCodes() {
-            final PropertiesUtil propertiesUtil = PropertiesUtil.getProperties();
-            final boolean isPlatformSupportsAnsi = !propertiesUtil.isOsWindows();
-            final boolean isJansiRequested = !propertiesUtil.getBooleanProperty("log4j.skipJansi", true);
+            final PropertyEnvironment properties = PropertiesUtil.getProperties();
+            final boolean isPlatformSupportsAnsi = !properties.isOsWindows();
+            final boolean isJansiRequested = !properties.getBooleanProperty("log4j.skipJansi", true);
             return isPlatformSupportsAnsi || isJansiRequested;
         }
 
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/UrlConnectionFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/UrlConnectionFactory.java
index cd9cb99e54..d60753a920 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/UrlConnectionFactory.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/UrlConnectionFactory.java
@@ -16,6 +16,16 @@
  */
 package org.apache.logging.log4j.core.net;
 
+import org.apache.logging.log4j.core.config.ConfigurationFactory;
+import org.apache.logging.log4j.core.net.ssl.LaxHostnameVerifier;
+import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
+import org.apache.logging.log4j.core.net.ssl.SslConfigurationFactory;
+import org.apache.logging.log4j.core.util.AuthorizationProvider;
+import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
+import org.apache.logging.log4j.util.Strings;
+
+import javax.net.ssl.HttpsURLConnection;
 import java.io.IOException;
 import java.net.HttpURLConnection;
 import java.net.JarURLConnection;
@@ -25,15 +35,6 @@ import java.net.URLConnection;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Locale;
-import javax.net.ssl.HttpsURLConnection;
-
-import org.apache.logging.log4j.core.config.ConfigurationFactory;
-import org.apache.logging.log4j.core.net.ssl.LaxHostnameVerifier;
-import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
-import org.apache.logging.log4j.core.net.ssl.SslConfigurationFactory;
-import org.apache.logging.log4j.core.util.AuthorizationProvider;
-import org.apache.logging.log4j.util.PropertiesUtil;
-import org.apache.logging.log4j.util.Strings;
 
 /**
  * Constructs an HTTPURLConnection. This class should be considered to be internal
@@ -58,7 +59,7 @@ public class UrlConnectionFactory {
     public static <T extends URLConnection> T createConnection(final URL url, final long lastModifiedMillis,
             final SslConfiguration sslConfiguration, final AuthorizationProvider authorizationProvider)
         throws IOException {
-        final PropertiesUtil props = PropertiesUtil.getProperties();
+        final PropertyEnvironment props = PropertiesUtil.getProperties();
         final List<String> allowed = Arrays.asList(Strings.splitList(props
                 .getStringProperty(ALLOWED_PROTOCOLS, DEFAULT_ALLOWED_PROTOCOLS).toLowerCase(Locale.ROOT)));
         if (allowed.size() == 1 && NO_PROTOCOLS.equals(allowed.get(0))) {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/ssl/SslConfigurationFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/ssl/SslConfigurationFactory.java
index 549d2cfc81..6fdbbbc8a7 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/ssl/SslConfigurationFactory.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/ssl/SslConfigurationFactory.java
@@ -20,6 +20,7 @@ import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.Lazy;
 import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
 
 /**
  * Creates an SSL configuration from Log4j properties.
@@ -43,11 +44,11 @@ public class SslConfigurationFactory {
     private static final String verifyHostName = "log4j2.ssl.verifyHostName";
 
     private static final Lazy<SslConfiguration> SSL_CONFIGURATION = Lazy.lazy(() -> {
-        final PropertiesUtil props = PropertiesUtil.getProperties();
+        final PropertyEnvironment props = PropertiesUtil.getProperties();
         return createSslConfiguration(props);
     });
 
-    static final SslConfiguration createSslConfiguration(final PropertiesUtil props) {
+    static final SslConfiguration createSslConfiguration(final PropertyEnvironment props) {
         KeyStoreConfiguration keyStoreConfiguration = null;
         TrustStoreConfiguration trustStoreConfiguration = null;
         String location = props.getStringProperty(trustStorelocation);
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/BasicAuthorizationProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/BasicAuthorizationProvider.java
index 61aa2d5721..793fbc2b87 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/BasicAuthorizationProvider.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/BasicAuthorizationProvider.java
@@ -16,13 +16,13 @@
  */
 package org.apache.logging.log4j.core.util;
 
-import java.net.URLConnection;
-import java.util.Base64;
-
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.LoaderUtil;
-import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
+
+import java.net.URLConnection;
+import java.util.Base64;
 
 /**
  * Provides the Basic Authorization header to a request.
@@ -41,7 +41,7 @@ public class BasicAuthorizationProvider implements AuthorizationProvider {
 
     private String authString = null;
 
-    public BasicAuthorizationProvider(final PropertiesUtil props) {
+    public BasicAuthorizationProvider(final PropertyEnvironment props) {
         final String userName = props.getStringProperty(PREFIXES,AUTH_USER_NAME,
                 () -> props.getStringProperty(CONFIG_USER_NAME));
         String password = props.getStringProperty(PREFIXES, AUTH_PASSWORD,
diff --git a/log4j-docker/src/main/java/org/apache/logging/log4j/docker/DockerLookup.java b/log4j-docker/src/main/java/org/apache/logging/log4j/docker/DockerLookup.java
index 592e3372d4..385100d519 100644
--- a/log4j-docker/src/main/java/org/apache/logging/log4j/docker/DockerLookup.java
+++ b/log4j-docker/src/main/java/org/apache/logging/log4j/docker/DockerLookup.java
@@ -28,6 +28,7 @@ import org.apache.logging.log4j.docker.model.Network;
 import org.apache.logging.log4j.plugins.Plugin;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
 
 import java.io.IOException;
 import java.net.URL;
@@ -52,7 +53,7 @@ public class DockerLookup extends AbstractLookup {
     public DockerLookup() {
         String baseUri = System.getenv(DOCKER_URI);
         if (baseUri == null) {
-            final PropertiesUtil props = PropertiesUtil.getProperties();
+            final PropertyEnvironment props = PropertiesUtil.getProperties();
             baseUri = props.getStringProperty(DOCKER_URI);
         }
         if (baseUri == null) {
diff --git a/log4j-jpa/src/test/java/org/apache/logging/log4j/jpa/appender/AbstractJpaAppenderTest.java b/log4j-jpa/src/test/java/org/apache/logging/log4j/jpa/appender/AbstractJpaAppenderTest.java
index 7e52c050c9..49112475bc 100644
--- a/log4j-jpa/src/test/java/org/apache/logging/log4j/jpa/appender/AbstractJpaAppenderTest.java
+++ b/log4j-jpa/src/test/java/org/apache/logging/log4j/jpa/appender/AbstractJpaAppenderTest.java
@@ -16,25 +16,25 @@
  */
 package org.apache.logging.log4j.jpa.appender;
 
-import java.io.ByteArrayOutputStream;
-import java.io.PrintWriter;
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.core.test.categories.Appenders;
 import org.apache.logging.log4j.core.Appender;
 import org.apache.logging.log4j.core.LoggerContext;
 import org.apache.logging.log4j.core.config.ConfigurationFactory;
 import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.core.test.categories.Appenders;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.PropertiesUtil;
 import org.junit.Test;
 import org.junit.experimental.categories.Category;
 
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
 import static org.junit.Assert.*;
 
 @Category(Appenders.Jpa.class)
@@ -55,7 +55,7 @@ public abstract class AbstractJpaAppenderTest {
         assertNotNull(getClass().getClassLoader().getResource(resource));
         System.setProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY,
                 resource);
-        PropertiesUtil.getProperties().reload();
+        ((PropertiesUtil) PropertiesUtil.getProperties()).reload();
         final LoggerContext context = LoggerContext.getContext(false);
         if (context.getConfiguration() instanceof DefaultConfiguration) {
             context.reconfigure();
@@ -73,7 +73,7 @@ public abstract class AbstractJpaAppenderTest {
             ((JpaAppender) appender).getManager().close();
         } finally {
             System.clearProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY);
-            PropertiesUtil.getProperties().reload();
+            ((PropertiesUtil) PropertiesUtil.getProperties()).reload();
             context.reconfigure();
             StatusLogger.getLogger().reset();
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutDefaults.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutDefaults.java
index bcfdeead7b..e75066ae96 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutDefaults.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutDefaults.java
@@ -19,6 +19,7 @@ package org.apache.logging.log4j.layout.template.json;
 import org.apache.logging.log4j.layout.template.json.util.RecyclerFactories;
 import org.apache.logging.log4j.layout.template.json.util.RecyclerFactory;
 import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
 
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
@@ -29,7 +30,7 @@ public final class JsonTemplateLayoutDefaults {
 
     private JsonTemplateLayoutDefaults() {}
 
-    private static final PropertiesUtil PROPERTIES = PropertiesUtil.getProperties();
+    private static final PropertyEnvironment PROPERTIES = PropertiesUtil.getProperties();
 
     public static Charset getCharset() {
         final String charsetName =
diff --git a/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/condition/OnPropertyConditionTest.java b/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/condition/OnPropertyConditionTest.java
index 2c9c0caf39..1c3d010575 100644
--- a/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/condition/OnPropertyConditionTest.java
+++ b/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/condition/OnPropertyConditionTest.java
@@ -55,7 +55,7 @@ class OnPropertyConditionTest {
     @Test
     @ClearSystemProperty(key = "foo.bar")
     void whenPropertyAbsent() {
-        PropertiesUtil.getProperties().reload();
+        ((PropertiesUtil) PropertiesUtil.getProperties()).reload();
         final String value = DI.createInjector(OnProperty.class).getInstance(String.class);
         assertEquals("goodbye", value);
     }
@@ -63,7 +63,7 @@ class OnPropertyConditionTest {
     @Test
     @SetSystemProperty(key = "foo.bar", value = "whatever")
     void whenPropertyPresent() {
-        PropertiesUtil.getProperties().reload();
+        ((PropertiesUtil) PropertiesUtil.getProperties()).reload();
         final String value = DI.createInjector(OnProperty.class).getInstance(String.class);
         assertEquals("hello", value);
     }
@@ -71,7 +71,7 @@ class OnPropertyConditionTest {
     @Test
     @SetSystemProperty(key = "foo.bar", value = "true")
     void whenPropertyMatches() {
-        PropertiesUtil.getProperties().reload();
+        ((PropertiesUtil) PropertiesUtil.getProperties()).reload();
         final String value = DI.createInjector(OnProperty.class).getInstance(String.class);
         assertEquals("truth", value);
     }