Skip to content

Commit 2c40cad

Browse files
committed
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<contextName>.<extension>` 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 <[email protected]>
1 parent 1758f50 commit 2c40cad

File tree

3 files changed

+160
-59
lines changed

3 files changed

+160
-59
lines changed

core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java

Lines changed: 84 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import java.util.LinkedHashMap;
2727
import java.util.List;
2828
import java.util.Map;
29-
import java.util.Properties;
3029
import java.util.Set;
3130
import java.util.logging.ConsoleHandler;
3231
import java.util.logging.Handler;
@@ -83,6 +82,7 @@
8382
* @author Alexander Heusingfeld
8483
* @author Ben Hale
8584
* @author Ralph Goers
85+
* @author Piotr P. Karwasz
8686
* @since 1.2.0
8787
*/
8888
public class Log4J2LoggingSystem extends AbstractLoggingSystem {
@@ -93,6 +93,41 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem {
9393

9494
private static final String LOG4J_LOG_MANAGER = "org.apache.logging.log4j.jul.LogManager";
9595

96+
/**
97+
* JSON tree parser used by Log4j 2 (optional dependency).
98+
*/
99+
private static final String JSON_TREE_PARSER_V2 = "com.fasterxml.jackson.databind.ObjectMapper";
100+
101+
/**
102+
* JSON tree parser embedded in Log4j 3.
103+
*/
104+
private static final String JSON_TREE_PARSER_V3 = "org.apache.logging.log4j.kit.json.JsonReader";
105+
106+
/**
107+
* Configuration factory for properties files (Log4j 2).
108+
*/
109+
private static final String PROPS_CONFIGURATION_FACTORY_V2 = "org.apache.logging.log4j.core.config.properties.PropertiesConfigurationFactory";
110+
111+
/**
112+
* Configuration factory for properties files (Log4j 3, optional dependency).
113+
*/
114+
private static final String PROPS_CONFIGURATION_FACTORY_V3 = "org.apache.logging.log4j.config.properties.JavaPropsConfigurationFactory";
115+
116+
/**
117+
* YAML tree parser used by Log4j 2 (optional dependency).
118+
*/
119+
private static final String YAML_TREE_PARSER_V2 = "com.fasterxml.jackson.dataformat.yaml.YAMLMapper";
120+
121+
/**
122+
* Configuration factory for YAML files (Log4j 2, embedded).
123+
*/
124+
private static final String YAML_CONFIGURATION_FACTORY_V2 = "org.apache.logging.log4j.core.config.yaml.YamlConfigurationFactory";
125+
126+
/**
127+
* Configuration factory for YAML files (Log4j 3, optional dependency).
128+
*/
129+
private static final String YAML_CONFIGURATION_FACTORY_V3 = "org.apache.logging.log4j.config.yaml.YamlConfigurationFactory";
130+
96131
private static final SpringEnvironmentPropertySource propertySource = new SpringEnvironmentPropertySource();
97132

98133
static final String ENVIRONMENT_KEY = Conventions.getQualifiedAttributeName(Log4J2LoggingSystem.class,
@@ -122,32 +157,61 @@ public Log4J2LoggingSystem(ClassLoader classLoader) {
122157
@Override
123158
protected String[] getStandardConfigLocations() {
124159
List<String> locations = new ArrayList<>();
125-
locations.add("log4j2-test.properties");
126-
if (isClassAvailable("com.fasterxml.jackson.dataformat.yaml.YAMLParser")) {
127-
Collections.addAll(locations, "log4j2-test.yaml", "log4j2-test.yml");
160+
// The `log4j2.configurationFile` and `log4j.configuration.location` properties
161+
// should be checked first, as they can be set to a custom location.
162+
for (String property : new String[] { "log4j2.configurationFile", "log4j.configuration.location" }) {
163+
String propertyDefinedLocation = PropertiesUtil.getProperties().getStringProperty(property);
164+
if (propertyDefinedLocation != null) {
165+
locations.add(propertyDefinedLocation);
166+
}
128167
}
129-
if (isClassAvailable("com.fasterxml.jackson.databind.ObjectMapper")) {
130-
Collections.addAll(locations, "log4j2-test.json", "log4j2-test.jsn");
168+
169+
// If no custom location is defined, we use the standard locations.
170+
LoggerContext loggerContext = getLoggerContext();
171+
String contextName = loggerContext.getName();
172+
List<String> extensions = getStandardConfigExtensions();
173+
extensions.forEach((e) -> locations.add("log4j2-test" + contextName + e));
174+
extensions.forEach((e) -> locations.add("log4j2-test" + e));
175+
extensions.forEach((e) -> locations.add("log4j2" + contextName + e));
176+
extensions.forEach((e) -> locations.add("log4j2" + e));
177+
178+
return StringUtils.toStringArray(locations);
179+
}
180+
181+
private List<String> getStandardConfigExtensions() {
182+
List<String> extensions = new ArrayList<>();
183+
// These classes need to be visible by the classloader that loads Log4j Core.
184+
ClassLoader classLoader = LoggerContext.class.getClassLoader();
185+
// The order of the extensions corresponds to the order
186+
// in which Log4j Core 2 and 3 will try to load them,
187+
// in decreasing value of `@Order`.
188+
if (isClassAvailable(classLoader, PROPS_CONFIGURATION_FACTORY_V2)
189+
|| isClassAvailable(classLoader, PROPS_CONFIGURATION_FACTORY_V3)) {
190+
extensions.add(".properties");
131191
}
132-
locations.add("log4j2-test.xml");
133-
locations.add("log4j2.properties");
134-
if (isClassAvailable("com.fasterxml.jackson.dataformat.yaml.YAMLParser")) {
135-
Collections.addAll(locations, "log4j2.yaml", "log4j2.yml");
192+
if (areClassesAvailable(classLoader, YAML_CONFIGURATION_FACTORY_V2, YAML_TREE_PARSER_V2)
193+
|| isClassAvailable(classLoader, YAML_CONFIGURATION_FACTORY_V3)) {
194+
Collections.addAll(extensions, ".yaml", ".yml");
136195
}
137-
if (isClassAvailable("com.fasterxml.jackson.databind.ObjectMapper")) {
138-
Collections.addAll(locations, "log4j2.json", "log4j2.jsn");
196+
if (isClassAvailable(classLoader, JSON_TREE_PARSER_V2) || isClassAvailable(classLoader, JSON_TREE_PARSER_V3)) {
197+
Collections.addAll(extensions, ".json", ".jsn");
139198
}
140-
locations.add("log4j2.xml");
141-
String propertyDefinedLocation = new PropertiesUtil(new Properties())
142-
.getStringProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY);
143-
if (propertyDefinedLocation != null) {
144-
locations.add(propertyDefinedLocation);
199+
// We assume the `java.xml` module is always available.
200+
extensions.add(".xml");
201+
return extensions;
202+
}
203+
204+
private boolean areClassesAvailable(ClassLoader classLoader, String... classNames) {
205+
for (String className : classNames) {
206+
if (!isClassAvailable(classLoader, className)) {
207+
return false;
208+
}
145209
}
146-
return StringUtils.toStringArray(locations);
210+
return true;
147211
}
148212

149-
protected boolean isClassAvailable(String className) {
150-
return ClassUtils.isPresent(className, getClassLoader());
213+
protected boolean isClassAvailable(ClassLoader classLoader, String className) {
214+
return ClassUtils.isPresent(className, classLoader);
151215
}
152216

153217
@Override

core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java

Lines changed: 75 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@
2424
import java.lang.annotation.RetentionPolicy;
2525
import java.lang.annotation.Target;
2626
import java.net.ProtocolException;
27+
import java.util.ArrayList;
2728
import java.util.EnumSet;
2829
import java.util.LinkedHashMap;
2930
import java.util.List;
3031
import java.util.Map;
3132
import java.util.logging.Handler;
3233
import java.util.logging.Level;
34+
import java.util.stream.Stream;
3335

3436
import com.fasterxml.jackson.databind.ObjectMapper;
3537
import org.apache.commons.logging.Log;
@@ -38,12 +40,15 @@
3840
import org.apache.logging.log4j.Logger;
3941
import org.apache.logging.log4j.core.LoggerContext;
4042
import org.apache.logging.log4j.core.config.Configuration;
41-
import org.apache.logging.log4j.core.config.ConfigurationFactory;
4243
import org.apache.logging.log4j.core.config.LoggerConfig;
4344
import org.apache.logging.log4j.core.config.Reconfigurable;
4445
import org.apache.logging.log4j.core.config.composite.CompositeConfiguration;
46+
import org.apache.logging.log4j.core.config.json.JsonConfigurationFactory;
4547
import org.apache.logging.log4j.core.config.plugins.util.PluginRegistry;
48+
import org.apache.logging.log4j.core.config.properties.PropertiesConfigurationBuilder;
49+
import org.apache.logging.log4j.core.config.properties.PropertiesConfigurationFactory;
4650
import org.apache.logging.log4j.core.config.xml.XmlConfiguration;
51+
import org.apache.logging.log4j.core.config.yaml.YamlConfigurationFactory;
4752
import org.apache.logging.log4j.core.util.ShutdownCallbackRegistry;
4853
import org.apache.logging.log4j.jul.Log4jBridgeHandler;
4954
import org.apache.logging.log4j.status.StatusListener;
@@ -53,6 +58,9 @@
5358
import org.junit.jupiter.api.BeforeEach;
5459
import org.junit.jupiter.api.Test;
5560
import org.junit.jupiter.api.extension.ExtendWith;
61+
import org.junit.jupiter.params.ParameterizedTest;
62+
import org.junit.jupiter.params.provider.Arguments;
63+
import org.junit.jupiter.params.provider.MethodSource;
5664
import org.slf4j.MDC;
5765

5866
import org.springframework.boot.logging.AbstractLoggingSystemTests;
@@ -89,6 +97,7 @@
8997
* @author Andy Wilkinson
9098
* @author Ben Hale
9199
* @author Madhura Bhave
100+
* @author Piotr P. Karwasz
92101
*/
93102
@ExtendWith(OutputCaptureExtension.class)
94103
@ClassPathExclusions("logback-*.jar")
@@ -105,6 +114,8 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests {
105114

106115
private Configuration configuration;
107116

117+
private String contextName;
118+
108119
@BeforeEach
109120
void setup() {
110121
PluginRegistry.getInstance().clear();
@@ -115,6 +126,7 @@ void setup() {
115126
this.configuration = loggerContext.getConfiguration();
116127
this.loggingSystem.cleanUp();
117128
this.logger = LogManager.getLogger(getClass());
129+
this.contextName = loggerContext.getName();
118130
}
119131

120132
@AfterEach
@@ -293,54 +305,79 @@ void loggingThatUsesJulIsCaptured(CapturedOutput output) {
293305
assertThat(output).contains("Hello world");
294306
}
295307

296-
@Test
297-
void configLocationsWithNoExtraDependencies() {
298-
assertThat(this.loggingSystem.getStandardConfigLocations()).contains("log4j2-test.properties",
299-
"log4j2-test.xml", "log4j2.properties", "log4j2.xml");
300-
}
301-
302-
@Test
303-
void configLocationsWithJacksonDatabind() {
304-
this.loggingSystem.availableClasses(ObjectMapper.class.getName());
305-
assertThat(this.loggingSystem.getStandardConfigLocations()).containsExactly("log4j2-test.properties",
306-
"log4j2-test.json", "log4j2-test.jsn", "log4j2-test.xml", "log4j2.properties", "log4j2.json",
307-
"log4j2.jsn", "log4j2.xml");
308-
}
309-
310-
@Test
311-
void configLocationsWithJacksonDataformatYaml() {
312-
this.loggingSystem.availableClasses("com.fasterxml.jackson.dataformat.yaml.YAMLParser");
313-
assertThat(this.loggingSystem.getStandardConfigLocations()).containsExactly("log4j2-test.properties",
314-
"log4j2-test.yaml", "log4j2-test.yml", "log4j2-test.xml", "log4j2.properties", "log4j2.yaml",
315-
"log4j2.yml", "log4j2.xml");
316-
}
317-
318-
@Test
319-
void configLocationsWithJacksonDatabindAndDataformatYaml() {
320-
this.loggingSystem.availableClasses("com.fasterxml.jackson.dataformat.yaml.YAMLParser",
321-
ObjectMapper.class.getName());
322-
assertThat(this.loggingSystem.getStandardConfigLocations()).containsExactly("log4j2-test.properties",
323-
"log4j2-test.yaml", "log4j2-test.yml", "log4j2-test.json", "log4j2-test.jsn", "log4j2-test.xml",
324-
"log4j2.properties", "log4j2.yaml", "log4j2.yml", "log4j2.json", "log4j2.jsn", "log4j2.xml");
308+
static Stream<String> configLocationsWithConfigurationFileSystemProperty() {
309+
return Stream.of("log4j2.configurationFile", "log4j.configuration.location");
325310
}
326311

327-
@Test
328-
void configLocationsWithConfigurationFileSystemProperty() {
329-
System.setProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY, "custom-log4j2.properties");
312+
@ParameterizedTest
313+
@MethodSource
314+
void configLocationsWithConfigurationFileSystemProperty(String propertyName) {
315+
System.setProperty(propertyName, "custom-log4j2.properties");
330316
try {
331-
assertThat(this.loggingSystem.getStandardConfigLocations()).containsExactly("log4j2-test.properties",
332-
"log4j2-test.xml", "log4j2.properties", "log4j2.xml", "custom-log4j2.properties");
317+
assertThat(this.loggingSystem.getStandardConfigLocations()).containsExactly("custom-log4j2.properties",
318+
"log4j2-test" + this.contextName + ".xml", "log4j2-test.xml", "log4j2" + this.contextName + ".xml",
319+
"log4j2.xml");
333320
}
334321
finally {
335-
System.clearProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY);
322+
System.clearProperty(propertyName);
336323
}
337324
}
338325

326+
static Stream<Arguments> standardConfigLocations() {
327+
// For each configuration file format we make "available" to the
328+
// Log4j2LoggingSystem:
329+
// - The Log4j Core `ConfigurationFactory` class
330+
// - The tree parser used internally by that configuration factory
331+
return Stream.of(
332+
// No classes, only XML
333+
Arguments.of(List.of(), List.of(".xml")),
334+
// Log4j Core 2
335+
Arguments.of(List.of(JsonConfigurationFactory.class.getName(), ObjectMapper.class.getName()),
336+
List.of(".json", ".jsn", ".xml")),
337+
Arguments.of(List.of(PropertiesConfigurationFactory.class.getName(),
338+
PropertiesConfigurationBuilder.class.getName()), List.of(".properties", ".xml")),
339+
Arguments.of(List.of(YamlConfigurationFactory.class.getName(),
340+
"com.fasterxml.jackson.dataformat.yaml.YAMLMapper"), List.of(".yaml", ".yml", ".xml")),
341+
Arguments.of(List.of(JsonConfigurationFactory.class.getName(), ObjectMapper.class.getName(),
342+
PropertiesConfigurationFactory.class.getName(), PropertiesConfigurationBuilder.class.getName(),
343+
YamlConfigurationFactory.class.getName(), "com.fasterxml.jackson.dataformat.yaml.YAMLMapper"),
344+
List.of(".properties", ".yaml", ".yml", ".json", ".jsn", ".xml")),
345+
// Log4j Core 3
346+
Arguments.of(List.of(JsonConfigurationFactory.class.getName(),
347+
"org.apache.logging.log4j.kit.json.JsonReader"), List.of(".json", ".jsn", ".xml")),
348+
Arguments.of(List.of("org.apache.logging.log4j.config.properties.JavaPropsConfigurationFactory",
349+
"tools.jackson.dataformat.javaprop.JavaPropsMapper"), List.of(".properties", ".xml")),
350+
Arguments.of(List.of("org.apache.logging.log4j.config.yaml.YamlConfigurationFactory",
351+
"tools.jackson.dataformat.yaml.YAMLMapper"), List.of(".yaml", ".yml", ".xml")),
352+
Arguments.of(
353+
List.of(JsonConfigurationFactory.class.getName(),
354+
"org.apache.logging.log4j.kit.json.JsonReader",
355+
"org.apache.logging.log4j.config.properties.JavaPropsConfigurationFactory",
356+
"tools.jackson.dataformat.javaprop.JavaPropsMapper",
357+
"org.apache.logging.log4j.config.yaml.YamlConfigurationFactory",
358+
"tools.jackson.dataformat.yaml.YAMLMapper"),
359+
List.of(".properties", ".yaml", ".yml", ".json", ".jsn", ".xml")));
360+
}
361+
362+
@ParameterizedTest
363+
@MethodSource
364+
void standardConfigLocations(List<String> availableClasses, List<String> expectedSuffixes) {
365+
this.loggingSystem.availableClasses(availableClasses.toArray(new String[0]));
366+
String[] locations = this.loggingSystem.getStandardConfigLocations();
367+
assertThat(locations).hasSize(4 * expectedSuffixes.size());
368+
List<String> expected = new ArrayList<>();
369+
expectedSuffixes.forEach(s -> expected.add("log4j2-test" + this.contextName + s));
370+
expectedSuffixes.forEach(s -> expected.add("log4j2-test" + s));
371+
expectedSuffixes.forEach(s -> expected.add("log4j2" + this.contextName + s));
372+
expectedSuffixes.forEach(s -> expected.add("log4j2" + s));
373+
assertThat(locations).containsExactlyElementsOf(expected);
374+
}
375+
339376
@Test
340377
void springConfigLocations() {
341378
String[] locations = getSpringConfigLocations(this.loggingSystem);
342-
assertThat(locations).containsExactly("log4j2-test-spring.properties", "log4j2-test-spring.xml",
343-
"log4j2-spring.properties", "log4j2-spring.xml");
379+
assertThat(locations).containsExactly("log4j2-test" + contextName + "-spring.xml", "log4j2-test-spring.xml",
380+
"log4j2" + contextName + "-spring.xml", "log4j2-spring.xml");
344381
}
345382

346383
@Test

core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/TestLog4J2LoggingSystem.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ private LoggerContext getLoggerContext() {
4343
}
4444

4545
@Override
46-
protected boolean isClassAvailable(String className) {
46+
protected boolean isClassAvailable(ClassLoader classLoader, String className) {
4747
return this.availableClasses.contains(className);
4848
}
4949

0 commit comments

Comments
 (0)