From 2c40cad4b367c23a702878f129b90d913dd37765 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Sat, 12 Jul 2025 19:49:48 +0200 Subject: [PATCH 1/2] Improve Log4j Core Configuration File Detection for Version 3 Log4j Core 3 has undergone significant modularization and no longer uses optional parser dependencies. This change requires updates to Spring Boot's configuration file detection logic to properly support both Log4j Core 2 and 3. * **Updated configuration file detection** Spring Boot now detects configuration formats based on the presence of `ConfigurationFactory` implementations, instead of relying on optional parser dependencies (as was the case in Log4j Core 2). * **Improved classloader usage for reflection** Reflection logic now uses the classloader that loaded Log4j Core, rather than the one associated with the Spring Boot context, ensuring greater compatibility in modular environments. * **Adjusted configuration file lookup order** The lookup now prioritizes configuration files specified via properties over automatically discovered ones, improving consistency with Log4j Core. * **Support for contextual configuration files** Files named in the form `log4j2.` are now also supported. These changes ensure compatibility with Log4j Core 3 while preserving support for Log4j Core 2, improving Spring Boot's flexibility in detecting and loading user-defined logging configurations. > [!NOTE] > The configuration file detection logic introduced here could potentially be moved into a future version of Log4j Core itself. For more context, see apache/logging-log4j2#3775. Signed-off-by: Piotr P. Karwasz --- .../logging/log4j2/Log4J2LoggingSystem.java | 104 ++++++++++++---- .../log4j2/Log4J2LoggingSystemTests.java | 113 ++++++++++++------ .../log4j2/TestLog4J2LoggingSystem.java | 2 +- 3 files changed, 160 insertions(+), 59 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java index e697f50b4ae0..2a9fbd5de523 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java @@ -26,7 +26,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Properties; import java.util.Set; import java.util.logging.ConsoleHandler; import java.util.logging.Handler; @@ -83,6 +82,7 @@ * @author Alexander Heusingfeld * @author Ben Hale * @author Ralph Goers + * @author Piotr P. Karwasz * @since 1.2.0 */ public class Log4J2LoggingSystem extends AbstractLoggingSystem { @@ -93,6 +93,41 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { private static final String LOG4J_LOG_MANAGER = "org.apache.logging.log4j.jul.LogManager"; + /** + * JSON tree parser used by Log4j 2 (optional dependency). + */ + private static final String JSON_TREE_PARSER_V2 = "com.fasterxml.jackson.databind.ObjectMapper"; + + /** + * JSON tree parser embedded in Log4j 3. + */ + private static final String JSON_TREE_PARSER_V3 = "org.apache.logging.log4j.kit.json.JsonReader"; + + /** + * Configuration factory for properties files (Log4j 2). + */ + private static final String PROPS_CONFIGURATION_FACTORY_V2 = "org.apache.logging.log4j.core.config.properties.PropertiesConfigurationFactory"; + + /** + * Configuration factory for properties files (Log4j 3, optional dependency). + */ + private static final String PROPS_CONFIGURATION_FACTORY_V3 = "org.apache.logging.log4j.config.properties.JavaPropsConfigurationFactory"; + + /** + * YAML tree parser used by Log4j 2 (optional dependency). + */ + private static final String YAML_TREE_PARSER_V2 = "com.fasterxml.jackson.dataformat.yaml.YAMLMapper"; + + /** + * Configuration factory for YAML files (Log4j 2, embedded). + */ + private static final String YAML_CONFIGURATION_FACTORY_V2 = "org.apache.logging.log4j.core.config.yaml.YamlConfigurationFactory"; + + /** + * Configuration factory for YAML files (Log4j 3, optional dependency). + */ + private static final String YAML_CONFIGURATION_FACTORY_V3 = "org.apache.logging.log4j.config.yaml.YamlConfigurationFactory"; + private static final SpringEnvironmentPropertySource propertySource = new SpringEnvironmentPropertySource(); static final String ENVIRONMENT_KEY = Conventions.getQualifiedAttributeName(Log4J2LoggingSystem.class, @@ -122,32 +157,61 @@ public Log4J2LoggingSystem(ClassLoader classLoader) { @Override protected String[] getStandardConfigLocations() { List locations = new ArrayList<>(); - locations.add("log4j2-test.properties"); - if (isClassAvailable("com.fasterxml.jackson.dataformat.yaml.YAMLParser")) { - Collections.addAll(locations, "log4j2-test.yaml", "log4j2-test.yml"); + // The `log4j2.configurationFile` and `log4j.configuration.location` properties + // should be checked first, as they can be set to a custom location. + for (String property : new String[] { "log4j2.configurationFile", "log4j.configuration.location" }) { + String propertyDefinedLocation = PropertiesUtil.getProperties().getStringProperty(property); + if (propertyDefinedLocation != null) { + locations.add(propertyDefinedLocation); + } } - if (isClassAvailable("com.fasterxml.jackson.databind.ObjectMapper")) { - Collections.addAll(locations, "log4j2-test.json", "log4j2-test.jsn"); + + // If no custom location is defined, we use the standard locations. + LoggerContext loggerContext = getLoggerContext(); + String contextName = loggerContext.getName(); + List extensions = getStandardConfigExtensions(); + extensions.forEach((e) -> locations.add("log4j2-test" + contextName + e)); + extensions.forEach((e) -> locations.add("log4j2-test" + e)); + extensions.forEach((e) -> locations.add("log4j2" + contextName + e)); + extensions.forEach((e) -> locations.add("log4j2" + e)); + + return StringUtils.toStringArray(locations); + } + + private List getStandardConfigExtensions() { + List extensions = new ArrayList<>(); + // These classes need to be visible by the classloader that loads Log4j Core. + ClassLoader classLoader = LoggerContext.class.getClassLoader(); + // The order of the extensions corresponds to the order + // in which Log4j Core 2 and 3 will try to load them, + // in decreasing value of `@Order`. + if (isClassAvailable(classLoader, PROPS_CONFIGURATION_FACTORY_V2) + || isClassAvailable(classLoader, PROPS_CONFIGURATION_FACTORY_V3)) { + extensions.add(".properties"); } - locations.add("log4j2-test.xml"); - locations.add("log4j2.properties"); - if (isClassAvailable("com.fasterxml.jackson.dataformat.yaml.YAMLParser")) { - Collections.addAll(locations, "log4j2.yaml", "log4j2.yml"); + if (areClassesAvailable(classLoader, YAML_CONFIGURATION_FACTORY_V2, YAML_TREE_PARSER_V2) + || isClassAvailable(classLoader, YAML_CONFIGURATION_FACTORY_V3)) { + Collections.addAll(extensions, ".yaml", ".yml"); } - if (isClassAvailable("com.fasterxml.jackson.databind.ObjectMapper")) { - Collections.addAll(locations, "log4j2.json", "log4j2.jsn"); + if (isClassAvailable(classLoader, JSON_TREE_PARSER_V2) || isClassAvailable(classLoader, JSON_TREE_PARSER_V3)) { + Collections.addAll(extensions, ".json", ".jsn"); } - locations.add("log4j2.xml"); - String propertyDefinedLocation = new PropertiesUtil(new Properties()) - .getStringProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY); - if (propertyDefinedLocation != null) { - locations.add(propertyDefinedLocation); + // We assume the `java.xml` module is always available. + extensions.add(".xml"); + return extensions; + } + + private boolean areClassesAvailable(ClassLoader classLoader, String... classNames) { + for (String className : classNames) { + if (!isClassAvailable(classLoader, className)) { + return false; + } } - return StringUtils.toStringArray(locations); + return true; } - protected boolean isClassAvailable(String className) { - return ClassUtils.isPresent(className, getClassLoader()); + protected boolean isClassAvailable(ClassLoader classLoader, String className) { + return ClassUtils.isPresent(className, classLoader); } @Override diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java index 996db9c223a6..d2711173cb56 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java @@ -24,12 +24,14 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.net.ProtocolException; +import java.util.ArrayList; import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.logging.Handler; import java.util.logging.Level; +import java.util.stream.Stream; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.logging.Log; @@ -38,12 +40,15 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configuration; -import org.apache.logging.log4j.core.config.ConfigurationFactory; import org.apache.logging.log4j.core.config.LoggerConfig; import org.apache.logging.log4j.core.config.Reconfigurable; import org.apache.logging.log4j.core.config.composite.CompositeConfiguration; +import org.apache.logging.log4j.core.config.json.JsonConfigurationFactory; import org.apache.logging.log4j.core.config.plugins.util.PluginRegistry; +import org.apache.logging.log4j.core.config.properties.PropertiesConfigurationBuilder; +import org.apache.logging.log4j.core.config.properties.PropertiesConfigurationFactory; import org.apache.logging.log4j.core.config.xml.XmlConfiguration; +import org.apache.logging.log4j.core.config.yaml.YamlConfigurationFactory; import org.apache.logging.log4j.core.util.ShutdownCallbackRegistry; import org.apache.logging.log4j.jul.Log4jBridgeHandler; import org.apache.logging.log4j.status.StatusListener; @@ -53,6 +58,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.slf4j.MDC; import org.springframework.boot.logging.AbstractLoggingSystemTests; @@ -89,6 +97,7 @@ * @author Andy Wilkinson * @author Ben Hale * @author Madhura Bhave + * @author Piotr P. Karwasz */ @ExtendWith(OutputCaptureExtension.class) @ClassPathExclusions("logback-*.jar") @@ -105,6 +114,8 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { private Configuration configuration; + private String contextName; + @BeforeEach void setup() { PluginRegistry.getInstance().clear(); @@ -115,6 +126,7 @@ void setup() { this.configuration = loggerContext.getConfiguration(); this.loggingSystem.cleanUp(); this.logger = LogManager.getLogger(getClass()); + this.contextName = loggerContext.getName(); } @AfterEach @@ -293,54 +305,79 @@ void loggingThatUsesJulIsCaptured(CapturedOutput output) { assertThat(output).contains("Hello world"); } - @Test - void configLocationsWithNoExtraDependencies() { - assertThat(this.loggingSystem.getStandardConfigLocations()).contains("log4j2-test.properties", - "log4j2-test.xml", "log4j2.properties", "log4j2.xml"); - } - - @Test - void configLocationsWithJacksonDatabind() { - this.loggingSystem.availableClasses(ObjectMapper.class.getName()); - assertThat(this.loggingSystem.getStandardConfigLocations()).containsExactly("log4j2-test.properties", - "log4j2-test.json", "log4j2-test.jsn", "log4j2-test.xml", "log4j2.properties", "log4j2.json", - "log4j2.jsn", "log4j2.xml"); - } - - @Test - void configLocationsWithJacksonDataformatYaml() { - this.loggingSystem.availableClasses("com.fasterxml.jackson.dataformat.yaml.YAMLParser"); - assertThat(this.loggingSystem.getStandardConfigLocations()).containsExactly("log4j2-test.properties", - "log4j2-test.yaml", "log4j2-test.yml", "log4j2-test.xml", "log4j2.properties", "log4j2.yaml", - "log4j2.yml", "log4j2.xml"); - } - - @Test - void configLocationsWithJacksonDatabindAndDataformatYaml() { - this.loggingSystem.availableClasses("com.fasterxml.jackson.dataformat.yaml.YAMLParser", - ObjectMapper.class.getName()); - assertThat(this.loggingSystem.getStandardConfigLocations()).containsExactly("log4j2-test.properties", - "log4j2-test.yaml", "log4j2-test.yml", "log4j2-test.json", "log4j2-test.jsn", "log4j2-test.xml", - "log4j2.properties", "log4j2.yaml", "log4j2.yml", "log4j2.json", "log4j2.jsn", "log4j2.xml"); + static Stream configLocationsWithConfigurationFileSystemProperty() { + return Stream.of("log4j2.configurationFile", "log4j.configuration.location"); } - @Test - void configLocationsWithConfigurationFileSystemProperty() { - System.setProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY, "custom-log4j2.properties"); + @ParameterizedTest + @MethodSource + void configLocationsWithConfigurationFileSystemProperty(String propertyName) { + System.setProperty(propertyName, "custom-log4j2.properties"); try { - assertThat(this.loggingSystem.getStandardConfigLocations()).containsExactly("log4j2-test.properties", - "log4j2-test.xml", "log4j2.properties", "log4j2.xml", "custom-log4j2.properties"); + assertThat(this.loggingSystem.getStandardConfigLocations()).containsExactly("custom-log4j2.properties", + "log4j2-test" + this.contextName + ".xml", "log4j2-test.xml", "log4j2" + this.contextName + ".xml", + "log4j2.xml"); } finally { - System.clearProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY); + System.clearProperty(propertyName); } } + static Stream standardConfigLocations() { + // For each configuration file format we make "available" to the + // Log4j2LoggingSystem: + // - The Log4j Core `ConfigurationFactory` class + // - The tree parser used internally by that configuration factory + return Stream.of( + // No classes, only XML + Arguments.of(List.of(), List.of(".xml")), + // Log4j Core 2 + Arguments.of(List.of(JsonConfigurationFactory.class.getName(), ObjectMapper.class.getName()), + List.of(".json", ".jsn", ".xml")), + Arguments.of(List.of(PropertiesConfigurationFactory.class.getName(), + PropertiesConfigurationBuilder.class.getName()), List.of(".properties", ".xml")), + Arguments.of(List.of(YamlConfigurationFactory.class.getName(), + "com.fasterxml.jackson.dataformat.yaml.YAMLMapper"), List.of(".yaml", ".yml", ".xml")), + Arguments.of(List.of(JsonConfigurationFactory.class.getName(), ObjectMapper.class.getName(), + PropertiesConfigurationFactory.class.getName(), PropertiesConfigurationBuilder.class.getName(), + YamlConfigurationFactory.class.getName(), "com.fasterxml.jackson.dataformat.yaml.YAMLMapper"), + List.of(".properties", ".yaml", ".yml", ".json", ".jsn", ".xml")), + // Log4j Core 3 + Arguments.of(List.of(JsonConfigurationFactory.class.getName(), + "org.apache.logging.log4j.kit.json.JsonReader"), List.of(".json", ".jsn", ".xml")), + Arguments.of(List.of("org.apache.logging.log4j.config.properties.JavaPropsConfigurationFactory", + "tools.jackson.dataformat.javaprop.JavaPropsMapper"), List.of(".properties", ".xml")), + Arguments.of(List.of("org.apache.logging.log4j.config.yaml.YamlConfigurationFactory", + "tools.jackson.dataformat.yaml.YAMLMapper"), List.of(".yaml", ".yml", ".xml")), + Arguments.of( + List.of(JsonConfigurationFactory.class.getName(), + "org.apache.logging.log4j.kit.json.JsonReader", + "org.apache.logging.log4j.config.properties.JavaPropsConfigurationFactory", + "tools.jackson.dataformat.javaprop.JavaPropsMapper", + "org.apache.logging.log4j.config.yaml.YamlConfigurationFactory", + "tools.jackson.dataformat.yaml.YAMLMapper"), + List.of(".properties", ".yaml", ".yml", ".json", ".jsn", ".xml"))); + } + + @ParameterizedTest + @MethodSource + void standardConfigLocations(List availableClasses, List expectedSuffixes) { + this.loggingSystem.availableClasses(availableClasses.toArray(new String[0])); + String[] locations = this.loggingSystem.getStandardConfigLocations(); + assertThat(locations).hasSize(4 * expectedSuffixes.size()); + List expected = new ArrayList<>(); + expectedSuffixes.forEach(s -> expected.add("log4j2-test" + this.contextName + s)); + expectedSuffixes.forEach(s -> expected.add("log4j2-test" + s)); + expectedSuffixes.forEach(s -> expected.add("log4j2" + this.contextName + s)); + expectedSuffixes.forEach(s -> expected.add("log4j2" + s)); + assertThat(locations).containsExactlyElementsOf(expected); + } + @Test void springConfigLocations() { String[] locations = getSpringConfigLocations(this.loggingSystem); - assertThat(locations).containsExactly("log4j2-test-spring.properties", "log4j2-test-spring.xml", - "log4j2-spring.properties", "log4j2-spring.xml"); + assertThat(locations).containsExactly("log4j2-test" + contextName + "-spring.xml", "log4j2-test-spring.xml", + "log4j2" + contextName + "-spring.xml", "log4j2-spring.xml"); } @Test diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/TestLog4J2LoggingSystem.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/TestLog4J2LoggingSystem.java index 5bf39b37a531..9ada125d4d16 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/TestLog4J2LoggingSystem.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/TestLog4J2LoggingSystem.java @@ -43,7 +43,7 @@ private LoggerContext getLoggerContext() { } @Override - protected boolean isClassAvailable(String className) { + protected boolean isClassAvailable(ClassLoader classLoader, String className) { return this.availableClasses.contains(className); } From 1b412dcfc717c16d4f8307bfba01fed0d274e828 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Sat, 12 Jul 2025 20:38:37 +0200 Subject: [PATCH 2/2] fix: Check style failures Signed-off-by: Piotr P. Karwasz --- .../logging/log4j2/Log4J2LoggingSystemTests.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java index d2711173cb56..cd6770f1757b 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java @@ -25,6 +25,7 @@ import java.lang.annotation.Target; import java.net.ProtocolException; import java.util.ArrayList; +import java.util.Collections; import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.List; @@ -330,7 +331,7 @@ static Stream standardConfigLocations() { // - The tree parser used internally by that configuration factory return Stream.of( // No classes, only XML - Arguments.of(List.of(), List.of(".xml")), + Arguments.of(Collections.emptyList(), List.of(".xml")), // Log4j Core 2 Arguments.of(List.of(JsonConfigurationFactory.class.getName(), ObjectMapper.class.getName()), List.of(".json", ".jsn", ".xml")), @@ -366,18 +367,18 @@ void standardConfigLocations(List availableClasses, List expecte String[] locations = this.loggingSystem.getStandardConfigLocations(); assertThat(locations).hasSize(4 * expectedSuffixes.size()); List expected = new ArrayList<>(); - expectedSuffixes.forEach(s -> expected.add("log4j2-test" + this.contextName + s)); - expectedSuffixes.forEach(s -> expected.add("log4j2-test" + s)); - expectedSuffixes.forEach(s -> expected.add("log4j2" + this.contextName + s)); - expectedSuffixes.forEach(s -> expected.add("log4j2" + s)); + expectedSuffixes.forEach((s) -> expected.add("log4j2-test" + this.contextName + s)); + expectedSuffixes.forEach((s) -> expected.add("log4j2-test" + s)); + expectedSuffixes.forEach((s) -> expected.add("log4j2" + this.contextName + s)); + expectedSuffixes.forEach((s) -> expected.add("log4j2" + s)); assertThat(locations).containsExactlyElementsOf(expected); } @Test void springConfigLocations() { String[] locations = getSpringConfigLocations(this.loggingSystem); - assertThat(locations).containsExactly("log4j2-test" + contextName + "-spring.xml", "log4j2-test-spring.xml", - "log4j2" + contextName + "-spring.xml", "log4j2-spring.xml"); + assertThat(locations).containsExactly("log4j2-test" + this.contextName + "-spring.xml", + "log4j2-test-spring.xml", "log4j2" + this.contextName + "-spring.xml", "log4j2-spring.xml"); } @Test