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

pkarwasz pushed a commit to branch fix/property-environment
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 6f4050761010e9c727e29272ff4ea3266bb51125
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Wed Feb 21 13:07:11 2024 +0100

    Add `PropertyEnvironment` to `log4j-sdk`
    
    This PR creates a simplified version of `PropertyEnvironment` in
    `log4j-sdk` that supports Spring-like property classes.
    
    It provides aggregation features to allow the conversion of multiple
    properties at once in a typesafe way.
---
 log4j-sdk/pom.xml                                  |   6 +
 .../sdk/env/ConfigurablePropertyEnvironment.java   |  35 +++
 .../logging/log4j/sdk/env/Log4jProperty.java       |  51 +++++
 .../logging/log4j/sdk/env/PropertyEnvironment.java | 200 ++++++++++++++++
 .../logging/log4j/sdk/env/PropertySource.java      |  48 ++++
 .../internal/ContextEnvironmentPropertySource.java |  67 ++++++
 .../internal/ContextPropertiesPropertySource.java  |  66 ++++++
 .../PropertiesUtilPropertyEnvironment.java         |  48 ++++
 .../log4j/sdk/env/internal/package-info.java       |  24 ++
 .../apache/logging/log4j/sdk/env/package-info.java |  24 ++
 .../sdk/env/support/BasicPropertyEnvironment.java  | 254 +++++++++++++++++++++
 .../support/ClassloaderPropertyEnvironment.java    |  37 +++
 .../support/PropertySourcePropertyEnvironment.java |  94 ++++++++
 .../log4j/sdk/env/support/package-info.java        |  24 ++
 .../logging/log4j/sdk/logger/AbstractLogger.java   |   2 +
 .../env/support/BasicPropertyEnvironmentTest.java  | 157 +++++++++++++
 .../log4j/sdk/logger/AbstractLoggerTest.java       |   2 +-
 .../logging/log4j/sdk/logger/TestListLogger.java   |  73 ++++++
 18 files changed, 1211 insertions(+), 1 deletion(-)

diff --git a/log4j-sdk/pom.xml b/log4j-sdk/pom.xml
index 595f381ff1..9451799cc7 100644
--- a/log4j-sdk/pom.xml
+++ b/log4j-sdk/pom.xml
@@ -60,6 +60,12 @@
       <scope>test</scope>
     </dependency>
 
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-params</artifactId>
+      <scope>test</scope>
+    </dependency>
+
     <dependency>
       <groupId>org.javassist</groupId>
       <artifactId>javassist</artifactId>
diff --git 
a/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/ConfigurablePropertyEnvironment.java
 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/ConfigurablePropertyEnvironment.java
new file mode 100644
index 0000000000..0d5f0f5fb9
--- /dev/null
+++ 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/ConfigurablePropertyEnvironment.java
@@ -0,0 +1,35 @@
+/*
+ * 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.sdk.env;
+
+/**
+ * Provides methods to modify the set of {@link PropertySource}s used by a 
{@link PropertyEnvironment}.
+ */
+public interface ConfigurablePropertyEnvironment extends PropertyEnvironment {
+
+    /**
+     * Adds a property source to the environment.
+     * @param source A property source.
+     */
+    void addPropertySource(PropertySource source);
+
+    /**
+     * Removes a property source from the environment.
+     * @param source A property source.
+     */
+    void removePropertySource(PropertySource source);
+}
diff --git 
a/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/Log4jProperty.java 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/Log4jProperty.java
new file mode 100644
index 0000000000..081980ee7a
--- /dev/null
+++ 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/Log4jProperty.java
@@ -0,0 +1,51 @@
+/*
+ * 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.sdk.env;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotates a class or parameter that stores Log4j API configuration 
properties.
+ * <p>
+ *     This annotation is required for root property classes.
+ * </p>
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD})
+public @interface Log4jProperty {
+
+    /**
+     * Provides a name for the configuration property.
+     * <p>
+     *
+     * </p>
+     */
+    String name() default "";
+
+    /**
+     * Provides the default value of the property.
+     * <p>
+     *     This only applies to scalar values.
+     * </p>
+     */
+    String defaultValue() default "";
+}
diff --git 
a/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/PropertyEnvironment.java
 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/PropertyEnvironment.java
new file mode 100644
index 0000000000..e153887c31
--- /dev/null
+++ 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/PropertyEnvironment.java
@@ -0,0 +1,200 @@
+/*
+ * 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.sdk.env;
+
+import java.nio.charset.Charset;
+import java.time.Duration;
+import 
org.apache.logging.log4j.sdk.env.internal.PropertiesUtilPropertyEnvironment;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Represents the main access point to Log4j properties.
+ * <p>
+ *     It provides as typesafe way to access properties stored in multiple 
{@link PropertySource}s, type conversion
+ *     methods and property aggregation methods (cf. {@link 
#getProperty(Class)}).
+ * </p>
+ */
+public interface PropertyEnvironment {
+
+    static PropertyEnvironment getGlobal() {
+        return PropertiesUtilPropertyEnvironment.INSTANCE;
+    }
+
+    /**
+     * 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(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.
+     */
+    @Nullable
+    Boolean getBooleanProperty(String name, @Nullable Boolean defaultValue);
+
+    /**
+     * 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(final String name) {
+        return getCharsetProperty(name, Charset.defaultCharset());
+    }
+
+    /**
+     * Gets the named property as a Charset 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 Charset value of the property or {@code defaultValue} if 
undefined.
+     */
+    @Nullable
+    Charset getCharsetProperty(String name, @Nullable Charset defaultValue);
+
+    /**
+     * Gets the named property as a Class value.
+     *
+     * @param name         the name of the property to look up
+     * @return the Class value of the property or {@code null} if it can not 
be loaded.
+     */
+    default @Nullable Class<?> getClassProperty(final String name) {
+        return getClassProperty(name, null);
+    }
+
+    /**
+     * Gets the named property as a Class 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 Class value of the property or {@code defaultValue} if it 
can not be loaded.
+     */
+    default @Nullable Class<?> getClassProperty(final String name, final 
@Nullable Class<?> defaultValue) {
+        return getClassProperty(name, defaultValue, Object.class);
+    }
+
+    /**
+     * Gets the named property as a subclass of {@code upperBound}.
+     *
+     * @param name         the name of the property to look up
+     * @param defaultValue the default value to use if the property is 
undefined
+     * @return the Class value of the property or {@code defaultValue} if it 
can not be loaded.
+     */
+    <T> @Nullable Class<? extends T> getClassProperty(
+            String name, @Nullable Class<? extends T> defaultValue, Class<T> 
upperBound);
+
+    /**
+     * Gets the named property as {@link Duration}.
+     *
+     * @param name The property name.
+     * @return The value of the String as a Duration or {@link Duration#ZERO} 
if it was undefined or could not be parsed.
+     */
+    default Duration getDurationProperty(final String name) {
+        return getDurationProperty(name, Duration.ZERO);
+    }
+
+    /**
+     * Gets the named property as {@link Duration}.
+     *
+     * @param name The property name.
+     * @param defaultValue The default value.
+     * @return The value of the String as a Duration or {@code defaultValue} 
if it was undefined or could not be parsed.
+     */
+    Duration getDurationProperty(String name, Duration defaultValue);
+
+    /**
+     * Gets the named property as an integer.
+     *
+     * @param name         the name of the property to look up
+     * @return the parsed integer value of the property or {@code 0} if it was 
undefined or could not be
+     * parsed.
+     */
+    default int getIntegerProperty(final String name) {
+        return getIntegerProperty(name, 0);
+    }
+
+    /**
+     * 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.
+     */
+    Integer getIntegerProperty(String name, Integer defaultValue);
+
+    /**
+     * Gets the named property as a long.
+     *
+     * @param name         the name of the property to look up
+     * @return the parsed long value of the property or {@code 0} if it was 
undefined or could not be
+     * parsed.
+     */
+    default long getLongProperty(final String name) {
+        return getLongProperty(name, 0L);
+    }
+
+    /**
+     * 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.
+     */
+    Long getLongProperty(String name, Long defaultValue);
+
+    /**
+     * 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.
+     */
+    @Nullable
+    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(final String name, final String 
defaultValue) {
+        final String prop = getStringProperty(name);
+        return (prop == null) ? defaultValue : prop;
+    }
+
+    /**
+     * Binds properties to class {@code T}.
+     * <p>
+     *     The implementation should at least support binding Java records 
with a single public constructor and enums.
+     * </p>
+     * @param propertyClass a class annotated by {@link Log4jProperty}.
+     * @return an instance of T with all JavaBean properties bound.
+     */
+    <T> T getProperty(final Class<T> propertyClass);
+}
diff --git 
a/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/PropertySource.java 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/PropertySource.java
new file mode 100644
index 0000000000..0318364db3
--- /dev/null
+++ 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/PropertySource.java
@@ -0,0 +1,48 @@
+/*
+ * 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.sdk.env;
+
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Basic interface to retrieve property values.
+ * <p>
+ *     We can not reuse the property sources from 2.x, since those required 
some sort of {@code log4j} prefix to be
+ *     included. In 3.x we want to use keys without a prefix.
+ * </p>
+ */
+public interface PropertySource {
+    /**
+     * Provides the priority of the property source.
+     * <p>
+     *     Property sources are ordered according to the natural ordering of 
their priority. Sources with lower
+     *     numerical value take precedence over those with higher numerical 
value.
+     * </p>
+     *
+     * @return priority value
+     */
+    int getPriority();
+
+    /**
+     * 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.
+     */
+    @Nullable
+    String getProperty(String name);
+}
diff --git 
a/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/internal/ContextEnvironmentPropertySource.java
 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/internal/ContextEnvironmentPropertySource.java
new file mode 100644
index 0000000000..5761d48b85
--- /dev/null
+++ 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/internal/ContextEnvironmentPropertySource.java
@@ -0,0 +1,67 @@
+/*
+ * 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.sdk.env.internal;
+
+import java.util.Locale;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.PropertySource;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * PropertySource backed by the current environment variables.
+ * <p>
+ *     Should haves a slightly lower priority than global environment 
variables.
+ * </p>
+ */
+public class ContextEnvironmentPropertySource implements PropertySource {
+
+    private static final int DEFAULT_PRIORITY = 100;
+
+    private final String prefix;
+    private final int priority;
+
+    public ContextEnvironmentPropertySource(final String contextName, final 
int priorityOffset) {
+        this.prefix = "log4j2." + contextName + ".";
+        this.priority = DEFAULT_PRIORITY + priorityOffset;
+    }
+
+    @Override
+    public int getPriority() {
+        return priority;
+    }
+
+    @Override
+    public @Nullable String getProperty(final String key) {
+        final String actualKey = key.replace('.', 
'_').toUpperCase(Locale.ROOT);
+        try {
+            return System.getenv(prefix + actualKey);
+        } catch (final SecurityException e) {
+            StatusLogger.getLogger()
+                    .warn(
+                            "{} lacks permissions to access system property 
{}.",
+                            getClass().getName(),
+                            actualKey,
+                            e);
+        }
+        return null;
+    }
+
+    @Override
+    public boolean containsProperty(final String key) {
+        return getProperty(key) != null;
+    }
+}
diff --git 
a/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/internal/ContextPropertiesPropertySource.java
 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/internal/ContextPropertiesPropertySource.java
new file mode 100644
index 0000000000..21447c02f0
--- /dev/null
+++ 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/internal/ContextPropertiesPropertySource.java
@@ -0,0 +1,66 @@
+/*
+ * 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.sdk.env.internal;
+
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.PropertySource;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * PropertySource backed by the current system properties.
+ * <p>
+ *     Should have a slightly lower priority than global system properties.
+ * </p>
+ */
+public class ContextPropertiesPropertySource implements PropertySource {
+
+    private static final int DEFAULT_PRIORITY = 0;
+
+    private final String prefix;
+    private final int priority;
+
+    public ContextPropertiesPropertySource(final String contextName, final int 
priorityOffset) {
+        this.prefix = "log4j2." + contextName + ".";
+        this.priority = DEFAULT_PRIORITY + priorityOffset;
+    }
+
+    @Override
+    public int getPriority() {
+        return priority;
+    }
+
+    @Override
+    public @Nullable String getProperty(final String key) {
+        final String actualKey = prefix + key;
+        try {
+            return System.getProperty(actualKey);
+        } catch (final SecurityException e) {
+            StatusLogger.getLogger()
+                    .warn(
+                            "{} lacks permissions to access system property 
{}.",
+                            getClass().getName(),
+                            actualKey,
+                            e);
+        }
+        return null;
+    }
+
+    @Override
+    public boolean containsProperty(final String key) {
+        return getProperty(key) != null;
+    }
+}
diff --git 
a/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/internal/PropertiesUtilPropertyEnvironment.java
 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/internal/PropertiesUtilPropertyEnvironment.java
new file mode 100644
index 0000000000..28f9836720
--- /dev/null
+++ 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/internal/PropertiesUtilPropertyEnvironment.java
@@ -0,0 +1,48 @@
+/*
+ * 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.sdk.env.internal;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.sdk.env.PropertyEnvironment;
+import org.apache.logging.log4j.sdk.env.support.BasicPropertyEnvironment;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.PropertiesUtil;
+
+/**
+ * An adapter of the {@link PropertiesUtil} from Log4j API 2.x.
+ *
+ * @implNote Since {@link PropertiesUtil} requires all properties to start 
with {@code log4j2.}, we must add the prefix
+ * before querying for the property.
+ */
+public class PropertiesUtilPropertyEnvironment extends 
BasicPropertyEnvironment {
+
+    private static final String PREFIX = "log4j2.";
+    public static final PropertyEnvironment INSTANCE =
+            new 
PropertiesUtilPropertyEnvironment(PropertiesUtil.getProperties(), 
StatusLogger.getLogger());
+
+    private final PropertiesUtil propsUtil;
+
+    public PropertiesUtilPropertyEnvironment(final PropertiesUtil propsUtil, 
final Logger statusLogger) {
+        super(statusLogger);
+        this.propsUtil = propsUtil;
+    }
+
+    @Override
+    public String getStringProperty(final String name) {
+        return propsUtil.getStringProperty(PREFIX + name);
+    }
+}
diff --git 
a/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/internal/package-info.java
 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/internal/package-info.java
new file mode 100644
index 0000000000..f4a5436ba2
--- /dev/null
+++ 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/internal/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+@Export
+@NullMarked
+@Version("3.0.0")
+package org.apache.logging.log4j.sdk.env.internal;
+
+import org.jspecify.annotations.NullMarked;
+import org.osgi.annotation.bundle.Export;
+import org.osgi.annotation.versioning.Version;
diff --git 
a/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/package-info.java 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/package-info.java
new file mode 100644
index 0000000000..8c549819cc
--- /dev/null
+++ b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+@Export
+@NullMarked
+@Version("3.0.0")
+package org.apache.logging.log4j.sdk.env;
+
+import org.jspecify.annotations.NullMarked;
+import org.osgi.annotation.bundle.Export;
+import org.osgi.annotation.versioning.Version;
diff --git 
a/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/support/BasicPropertyEnvironment.java
 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/support/BasicPropertyEnvironment.java
new file mode 100644
index 0000000000..0228a2d03b
--- /dev/null
+++ 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/support/BasicPropertyEnvironment.java
@@ -0,0 +1,254 @@
+/*
+ * 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.sdk.env.support;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Parameter;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
+import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
+import java.time.Duration;
+import java.time.format.DateTimeParseException;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.sdk.env.Log4jProperty;
+import org.apache.logging.log4j.sdk.env.PropertyEnvironment;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * An implementation of {@link PropertyEnvironment} that only uses basic Java 
functions.
+ * <p>
+ * Conversion problems are logged using a status logger.
+ * </p>
+ */
+public abstract class BasicPropertyEnvironment implements PropertyEnvironment {
+
+    private final Logger statusLogger;
+
+    protected BasicPropertyEnvironment(final Logger statusLogger) {
+        this.statusLogger = statusLogger;
+    }
+
+    @Override
+    public @Nullable Boolean getBooleanProperty(final String name, final 
@Nullable Boolean defaultValue) {
+        final String prop = getStringProperty(name);
+        return prop == null ? defaultValue : Boolean.parseBoolean(prop);
+    }
+
+    @Override
+    public @Nullable Charset getCharsetProperty(final String name, final 
@Nullable Charset defaultValue) {
+        final String charsetName = getStringProperty(name);
+        if (charsetName == null) {
+            return defaultValue;
+        }
+        try {
+            return Charset.forName(charsetName);
+        } catch (final IllegalCharsetNameException | 
UnsupportedOperationException e) {
+            statusLogger.warn(
+                    "Unable to get Charset '{}' for property '{}', using 
default '{}'.",
+                    charsetName,
+                    name,
+                    defaultValue,
+                    e);
+        }
+        return defaultValue;
+    }
+
+    @Override
+    public <T> @Nullable Class<? extends T> getClassProperty(
+            final String name, final @Nullable Class<? extends T> 
defaultValue, final Class<T> upperBound) {
+        final String className = getStringProperty(name);
+        if (className == null) {
+            return defaultValue;
+        }
+        try {
+            final Class<?> clazz = getClassForName(className);
+            if (upperBound.isAssignableFrom(clazz)) {
+                return (Class<? extends T>) clazz;
+            }
+            statusLogger.warn(
+                    "Unable to get Class '{}' for property '{}': class does 
not extend {}.",
+                    className,
+                    name,
+                    upperBound.getName());
+        } catch (final ReflectiveOperationException e) {
+            statusLogger.warn(
+                    "Unable to get Class '{}' for property '{}', using default 
'{}'.",
+                    className,
+                    name,
+                    defaultValue,
+                    e);
+        }
+        return defaultValue;
+    }
+
+    protected Class<?> getClassForName(final String className) throws 
ReflectiveOperationException {
+        return Class.forName(className);
+    }
+
+    @Override
+    public @Nullable Duration getDurationProperty(final String name, final 
@Nullable Duration defaultValue) {
+        final String prop = getStringProperty(name);
+        if (prop != null) {
+            try {
+                return Duration.parse(prop);
+            } catch (final DateTimeParseException ignored) {
+                statusLogger.warn(
+                        "Invalid Duration value '{}' for property '{}', using 
default '{}'.", prop, name, defaultValue);
+            }
+        }
+        return defaultValue;
+    }
+
+    @Override
+    public @Nullable Integer getIntegerProperty(final String name, final 
@Nullable Integer defaultValue) {
+        final String prop = getStringProperty(name);
+        if (prop != null) {
+            try {
+                return Integer.parseInt(prop);
+            } catch (final Exception ignored) {
+                statusLogger.warn(
+                        "Invalid integer value '{}' for property '{}', using 
default '{}'.", prop, name, defaultValue);
+            }
+        }
+        return defaultValue;
+    }
+
+    @Override
+    public @Nullable Long getLongProperty(final String name, final @Nullable 
Long defaultValue) {
+        final String prop = getStringProperty(name);
+        if (prop != null) {
+            try {
+                return Long.parseLong(prop);
+            } catch (final Exception ignored) {
+                statusLogger.warn(
+                        "Invalid long value '{}' for property '{}', using 
default '{}'.", prop, name, defaultValue);
+            }
+        }
+        return defaultValue;
+    }
+
+    @Override
+    public abstract @Nullable String getStringProperty(String name);
+
+    @Override
+    public <T> T getProperty(final Class<T> propertyClass) {
+        if (!propertyClass.isRecord()) {
+            throw new IllegalArgumentException("Unsupported configuration 
properties class '" + propertyClass.getName()
+                    + "': class is not a record.");
+        }
+        if (propertyClass.getAnnotation(Log4jProperty.class) == null) {
+            throw new IllegalArgumentException("Unsupported configuration 
properties class '" + propertyClass.getName()
+                    + "': missing '@Log4jProperty' annotation.");
+        }
+        return getProperty(null, propertyClass);
+    }
+
+    private <T> T getProperty(final @Nullable String parentPrefix, final 
Class<T> propertyClass) {
+        final Log4jProperty annotation = 
propertyClass.getAnnotation(Log4jProperty.class);
+        final String prefix = parentPrefix != null
+                ? parentPrefix
+                : annotation != null && annotation.name().isEmpty() ? 
propertyClass.getSimpleName() : annotation.name();
+
+        @SuppressWarnings("unchecked")
+        final Constructor<T>[] constructors = (Constructor<T>[]) 
propertyClass.getDeclaredConstructors();
+        if (constructors.length == 0) {
+            throw new IllegalArgumentException("Unsupported configuration 
properties class '" + propertyClass.getName()
+                    + "': missing public constructor.");
+        } else if (constructors.length > 1) {
+            throw new IllegalArgumentException("Unsupported configuration 
properties class '" + propertyClass.getName()
+                    + "': more than one constructor found.");
+        }
+        final Constructor<T> constructor = constructors[0];
+
+        final Parameter[] parameters = constructor.getParameters();
+        final Object[] initArgs = new Object[parameters.length];
+        for (int i = 0; i < initArgs.length; i++) {
+            initArgs[i] = getProperty(prefix, parameters[i]);
+        }
+        try {
+            return constructor.newInstance(initArgs);
+        } catch (final ReflectiveOperationException e) {
+            statusLogger.warn("Unable to parse configuration properties class 
{}.", propertyClass.getName(), e);
+            return null;
+        }
+    }
+
+    private Object getProperty(final String parentPrefix, final Parameter 
parameter) {
+        if (!parameter.isNamePresent()) {
+            statusLogger.warn("Missing parameter name on configuration 
parameter {}.", parameter);
+            return null;
+        }
+        final String key = parentPrefix + "." + parameter.getName();
+        final Class<?> type = parameter.getType();
+        if (boolean.class.equals(type)) {
+            return getBooleanProperty(key);
+        }
+        if (Class.class.equals(type)) {
+            return getClassProperty(key, 
parameter.getAnnotatedType().getType());
+        }
+        if (Charset.class.equals(type)) {
+            return getCharsetProperty(key);
+        }
+        if (Duration.class.equals(type)) {
+            return getDurationProperty(key);
+        }
+        if (Enum.class.isAssignableFrom(type)) {
+            final String prop = getStringProperty(key);
+            if (prop != null) {
+                try {
+                    return Enum.valueOf((Class<? extends Enum>) type, prop);
+                } catch (final IllegalArgumentException e) {
+                    statusLogger.warn("Invalid {} value '{}' for property 
'{}'.", type.getSimpleName(), prop, key);
+                }
+            }
+            return null;
+        }
+        if (int.class.equals(type)) {
+            return getIntegerProperty(key);
+        }
+        if (long.class.equals(type)) {
+            return getLongProperty(key);
+        }
+        return String.class.equals(type) ? getStringProperty(key) : 
getProperty(key, type);
+    }
+
+    private Object getClassProperty(final String key, final Type type) {
+        Class<?> upperBound = Object.class;
+        if (type instanceof final ParameterizedType parameterizedType) {
+            final Type[] arguments = 
parameterizedType.getActualTypeArguments();
+            if (arguments.length > 0) {
+                upperBound = findUpperBound(arguments[0]);
+            }
+        }
+        return getClassProperty(key, null, upperBound);
+    }
+
+    private Class<?> findUpperBound(final Type type) {
+        final Type[] bounds;
+        if (type instanceof final TypeVariable<?> typeVariable) {
+            bounds = typeVariable.getBounds();
+        } else if (type instanceof final WildcardType wildcardType) {
+            bounds = wildcardType.getUpperBounds();
+        } else {
+            bounds = new Type[0];
+        }
+        return bounds.length > 0 && bounds[0] instanceof final Class<?> clazz 
? clazz : Object.class;
+    }
+}
diff --git 
a/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/support/ClassloaderPropertyEnvironment.java
 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/support/ClassloaderPropertyEnvironment.java
new file mode 100644
index 0000000000..b5f5717fb6
--- /dev/null
+++ 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/support/ClassloaderPropertyEnvironment.java
@@ -0,0 +1,37 @@
+/*
+ * 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.sdk.env.support;
+
+import org.apache.logging.log4j.Logger;
+
+/**
+ * An environment implementation that uses a specific classloader to load 
classes.
+ */
+public abstract class ClassloaderPropertyEnvironment extends 
BasicPropertyEnvironment {
+
+    private final ClassLoader loader;
+
+    public ClassloaderPropertyEnvironment(final ClassLoader loader, final 
Logger statusLogger) {
+        super(statusLogger);
+        this.loader = loader;
+    }
+
+    @Override
+    protected Class<?> getClassForName(final String className) throws 
ReflectiveOperationException {
+        return Class.forName(className, true, loader);
+    }
+}
diff --git 
a/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/support/PropertySourcePropertyEnvironment.java
 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/support/PropertySourcePropertyEnvironment.java
new file mode 100644
index 0000000000..6d2b608f86
--- /dev/null
+++ 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/support/PropertySourcePropertyEnvironment.java
@@ -0,0 +1,94 @@
+/*
+ * 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.sdk.env.support;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.internal.CopyOnWriteNavigableSet;
+import org.apache.logging.log4j.sdk.env.ConfigurablePropertyEnvironment;
+import org.apache.logging.log4j.sdk.env.PropertyEnvironment;
+import org.apache.logging.log4j.sdk.env.PropertySource;
+import org.jspecify.annotations.Nullable;
+
+public class PropertySourcePropertyEnvironment extends 
ClassloaderPropertyEnvironment
+        implements ConfigurablePropertyEnvironment {
+
+    private final Collection<PropertySource> sources =
+            new 
CopyOnWriteNavigableSet<>(Comparator.comparing(PropertySource::getPriority));
+    private final Map<String, Optional<String>> stringCache = new 
ConcurrentHashMap<>();
+    private final Map<Class<?>, Object> classCache = new ConcurrentHashMap<>();
+
+    public PropertySourcePropertyEnvironment(
+            final @Nullable PropertyEnvironment parentEnvironment,
+            final Collection<? extends PropertySource> sources,
+            final ClassLoader loader,
+            final Logger statusLogger) {
+        super(loader, statusLogger);
+        this.sources.addAll(sources);
+        if (parentEnvironment != null) {
+            this.sources.add(new 
ParentEnvironmentPropertySource(parentEnvironment));
+        }
+    }
+
+    @Override
+    public @Nullable String getStringProperty(final String name) {
+        return stringCache
+                .computeIfAbsent(name, key -> sources.stream()
+                        .map(source -> source.getProperty(key))
+                        .filter(Objects::nonNull)
+                        .findFirst())
+                .orElse(null);
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T> T getProperty(final Class<T> propertyClass) {
+        return (T) classCache.computeIfAbsent(propertyClass, 
super::getProperty);
+    }
+
+    @Override
+    public void addPropertySource(final PropertySource source) {
+        sources.add(Objects.requireNonNull(source));
+        stringCache.clear();
+        classCache.clear();
+    }
+
+    @Override
+    public void removePropertySource(final PropertySource source) {
+        sources.remove(Objects.requireNonNull(source));
+        stringCache.clear();
+        classCache.clear();
+    }
+
+    private record ParentEnvironmentPropertySource(PropertyEnvironment 
parentEnvironment) implements PropertySource {
+
+        @Override
+        public int getPriority() {
+            return Integer.MAX_VALUE;
+        }
+
+        @Override
+        public @Nullable String getProperty(final String name) {
+            return parentEnvironment.getStringProperty(name);
+        }
+    }
+}
diff --git 
a/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/support/package-info.java
 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/support/package-info.java
new file mode 100644
index 0000000000..79c7e9d5a0
--- /dev/null
+++ 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/env/support/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+@Export
+@NullMarked
+@Version("3.0.0")
+package org.apache.logging.log4j.sdk.env.support;
+
+import org.jspecify.annotations.NullMarked;
+import org.osgi.annotation.bundle.Export;
+import org.osgi.annotation.versioning.Version;
diff --git 
a/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/logger/AbstractLogger.java
 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/logger/AbstractLogger.java
index 03cb039e0b..4b1c690142 100644
--- 
a/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/logger/AbstractLogger.java
+++ 
b/log4j-sdk/src/main/java/org/apache/logging/log4j/sdk/logger/AbstractLogger.java
@@ -1114,6 +1114,7 @@ public abstract class AbstractLogger implements 
ExtendedLogger {
             final @Nullable Throwable throwable) {
         // This method does NOT check the level
         logMessageSafely(fqcn, null, level, marker, message, throwable);
+        logMessageSafely(FQCN, null, level, marker, message, throwable);
     }
 
     @Override
@@ -1127,6 +1128,7 @@ public abstract class AbstractLogger implements 
ExtendedLogger {
             final @Nullable Throwable throwable) {
         // This method does NOT check the level
         logMessageSafely(fqcn, location, level, marker, message, throwable);
+        logMessageSafely(FQCN, location, level, marker, message, throwable);
     }
     // </editor-fold>
 
diff --git 
a/log4j-sdk/src/test/java/org/apache/logging/log4j/sdk/env/support/BasicPropertyEnvironmentTest.java
 
b/log4j-sdk/src/test/java/org/apache/logging/log4j/sdk/env/support/BasicPropertyEnvironmentTest.java
new file mode 100644
index 0000000000..18ba1ab412
--- /dev/null
+++ 
b/log4j-sdk/src/test/java/org/apache/logging/log4j/sdk/env/support/BasicPropertyEnvironmentTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.sdk.env.support;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.sdk.env.Log4jProperty;
+import org.apache.logging.log4j.sdk.env.PropertyEnvironment;
+import org.apache.logging.log4j.sdk.logger.TestListLogger;
+import org.apache.logging.log4j.spi.StandardLevel;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class BasicPropertyEnvironmentTest {
+
+    private static final Map<String, String> TEST_PROPS = Map.of(
+            "ComponentProperties.boolAttr",
+            "true",
+            "ComponentProperties.intAttr",
+            "123",
+            "ComponentProperties.longAttr",
+            "123456",
+            "ComponentProperties.charsetAttr",
+            "UTF-8",
+            "ComponentProperties.durationAttr",
+            "PT8H",
+            "ComponentProperties.stringAttr",
+            "Hello child!",
+            "ComponentProperties.classAttr",
+            "org.apache.logging.log4j.sdk.env.PropertyEnvironment",
+            "ComponentProperties.level",
+            "INFO",
+            "ComponentProperties.subComponent.subProperty",
+            "Hello parent!");
+
+    @Test
+    void get_property_should_support_records() {
+        final TestListLogger logger = new 
TestListLogger(BasicPropertyEnvironmentTest.class.getName());
+        final PropertyEnvironment env = new 
TestPropertyEnvironment(TEST_PROPS, logger);
+        final ComponentProperties expected = new ComponentProperties(
+                true,
+                123,
+                123456L,
+                StandardCharsets.UTF_8,
+                Duration.ofHours(8),
+                "Hello child!",
+                PropertyEnvironment.class,
+                StandardLevel.INFO,
+                new SubComponentProperties("Hello parent!"));
+        
assertThat(env.getProperty(ComponentProperties.class)).isEqualTo(expected);
+        assertThat(logger.getMessages()).isEmpty();
+    }
+
+    static Stream<Arguments> get_property_should_check_bounds() {
+        return Stream.of(
+                Arguments.of(
+                        "BoundedClass.className",
+                        "java.lang.String",
+                        BoundedClass.class,
+                        new BoundedClass(null),
+                        List.of("Unable to get Class 'java.lang.String' for 
property 'BoundedClass.className': "
+                                + "class does not extend java.lang.Number.")),
+                Arguments.of(
+                        "BoundedClassParam.className",
+                        "java.lang.String",
+                        BoundedClassParam.class,
+                        new BoundedClassParam(null),
+                        List.of("Unable to get Class 'java.lang.String' for 
property 'BoundedClassParam.className': "
+                                + "class does not extend java.lang.Number.")),
+                Arguments.of(
+                        "BoundedClass.className",
+                        "java.lang.Integer",
+                        BoundedClass.class,
+                        new BoundedClass(Integer.class),
+                        Collections.emptyList()),
+                Arguments.of(
+                        "BoundedClassParam.className",
+                        "java.lang.Integer",
+                        BoundedClassParam.class,
+                        new BoundedClassParam(Integer.class),
+                        Collections.emptyList()));
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void get_property_should_check_bounds(
+            final String key,
+            final String value,
+            final Class<?> clazz,
+            final Object expected,
+            final Iterable<? extends String> expectedMessages) {
+        final TestListLogger logger = new 
TestListLogger(BasicPropertyEnvironmentTest.class.getName());
+        final PropertyEnvironment env = new 
TestPropertyEnvironment(Map.of(key, value), logger);
+        assertThat(env.getProperty(clazz)).isEqualTo(expected);
+        
Assertions.<String>assertThat(logger.getMessages()).containsExactlyElementsOf(expectedMessages);
+    }
+
+    @Log4jProperty
+    record ComponentProperties(
+            boolean boolAttr,
+            int intAttr,
+            long longAttr,
+            Charset charsetAttr,
+            Duration durationAttr,
+            String stringAttr,
+            Class<?> classAttr,
+            StandardLevel level,
+            SubComponentProperties subComponent) {}
+
+    record SubComponentProperties(String subProperty) {}
+
+    @Log4jProperty
+    record BoundedClass(Class<? extends Number> className) {}
+
+    @Log4jProperty
+    record BoundedClassParam<T extends Number>(Class<T> className) {}
+
+    private static class TestPropertyEnvironment extends 
BasicPropertyEnvironment {
+
+        private final Map<String, String> props;
+
+        public TestPropertyEnvironment(final Map<String, String> props, final 
Logger logger) {
+            super(logger);
+            this.props = props;
+        }
+
+        @Override
+        public String getStringProperty(final String name) {
+            return props.get(name);
+        }
+    }
+}
diff --git 
a/log4j-sdk/src/test/java/org/apache/logging/log4j/sdk/logger/AbstractLoggerTest.java
 
b/log4j-sdk/src/test/java/org/apache/logging/log4j/sdk/logger/AbstractLoggerTest.java
index 22e8078a67..9662c15ed0 100644
--- 
a/log4j-sdk/src/test/java/org/apache/logging/log4j/sdk/logger/AbstractLoggerTest.java
+++ 
b/log4j-sdk/src/test/java/org/apache/logging/log4j/sdk/logger/AbstractLoggerTest.java
@@ -26,7 +26,7 @@ import javassist.bytecode.CodeAttribute;
 import javassist.bytecode.MethodInfo;
 import org.junit.jupiter.api.Test;
 
-public class AbstractLoggerTest {
+class AbstractLoggerTest {
 
     private static final int MAX_INLINE_SIZE = 35;
     /**
diff --git 
a/log4j-sdk/src/test/java/org/apache/logging/log4j/sdk/logger/TestListLogger.java
 
b/log4j-sdk/src/test/java/org/apache/logging/log4j/sdk/logger/TestListLogger.java
new file mode 100644
index 0000000000..c3c8e95522
--- /dev/null
+++ 
b/log4j-sdk/src/test/java/org/apache/logging/log4j/sdk/logger/TestListLogger.java
@@ -0,0 +1,73 @@
+/*
+ * 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.sdk.logger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.Marker;
+import org.apache.logging.log4j.internal.recycler.DummyRecyclerFactoryProvider;
+import org.apache.logging.log4j.message.DefaultFlowMessageFactory;
+import org.apache.logging.log4j.message.FlowMessageFactory;
+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.spi.recycler.RecyclerFactory;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+@NullMarked
+public class TestListLogger extends AbstractLogger {
+
+    private static final MessageFactory MESSAGE_FACTORY = 
ParameterizedNoReferenceMessageFactory.INSTANCE;
+    private static final FlowMessageFactory FLOW_MESSAGE_FACTORY = new 
DefaultFlowMessageFactory();
+    private static final RecyclerFactory RECYCLER_FACTORY =
+            new DummyRecyclerFactoryProvider().createForEnvironment(null);
+
+    private final List<String> messages = new ArrayList<>();
+
+    public TestListLogger(final String name) {
+        super(name, MESSAGE_FACTORY, FLOW_MESSAGE_FACTORY, RECYCLER_FACTORY, 
StatusLogger.getLogger());
+    }
+
+    @Override
+    public Level getLevel() {
+        return Level.DEBUG;
+    }
+
+    @Override
+    public boolean isEnabled(final Level level, @Nullable final Marker marker) 
{
+        return Level.DEBUG.isLessSpecificThan(level);
+    }
+
+    @Override
+    protected void doLog(
+            final String fqcn,
+            final @Nullable StackTraceElement location,
+            final Level level,
+            final @Nullable Marker marker,
+            final @Nullable Message message,
+            final @Nullable Throwable throwable) {
+        messages.add(message != null ? message.getFormattedMessage() : "");
+    }
+
+    public List<? extends String> getMessages() {
+        return Collections.unmodifiableList(messages);
+    }
+}

Reply via email to