Skip to content

Commit 16439ad

Browse files
ppkarwaszmhalbritter
authored andcommitted
Improve Log4j core configuration file detection for Log4j 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. See gh-46409 Signed-off-by: Piotr P. Karwasz <piotr@github.copernik.eu>
1 parent f3082e7 commit 16439ad

File tree

3 files changed

+161
-59
lines changed

3 files changed

+161
-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;
@@ -84,6 +83,7 @@
8483
* @author Alexander Heusingfeld
8584
* @author Ben Hale
8685
* @author Ralph Goers
86+
* @author Piotr P. Karwasz
8787
* @since 1.2.0
8888
*/
8989
public class Log4J2LoggingSystem extends AbstractLoggingSystem {
@@ -94,6 +94,41 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem {
9494

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

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

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

150-
protected boolean isClassAvailable(String className) {
151-
return ClassUtils.isPresent(className, getClassLoader());
214+
protected boolean isClassAvailable(ClassLoader classLoader, String className) {
215+
return ClassUtils.isPresent(className, classLoader);
152216
}
153217

154218
@Override

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

Lines changed: 76 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,15 @@
2424
import java.lang.annotation.RetentionPolicy;
2525
import java.lang.annotation.Target;
2626
import java.net.ProtocolException;
27+
import java.util.ArrayList;
28+
import java.util.Collections;
2729
import java.util.EnumSet;
2830
import java.util.LinkedHashMap;
2931
import java.util.List;
3032
import java.util.Map;
3133
import java.util.logging.Handler;
3234
import java.util.logging.Level;
35+
import java.util.stream.Stream;
3336

3437
import com.fasterxml.jackson.databind.ObjectMapper;
3538
import org.apache.commons.logging.Log;
@@ -38,12 +41,15 @@
3841
import org.apache.logging.log4j.Logger;
3942
import org.apache.logging.log4j.core.LoggerContext;
4043
import org.apache.logging.log4j.core.config.Configuration;
41-
import org.apache.logging.log4j.core.config.ConfigurationFactory;
4244
import org.apache.logging.log4j.core.config.LoggerConfig;
4345
import org.apache.logging.log4j.core.config.Reconfigurable;
4446
import org.apache.logging.log4j.core.config.composite.CompositeConfiguration;
47+
import org.apache.logging.log4j.core.config.json.JsonConfigurationFactory;
4548
import org.apache.logging.log4j.core.config.plugins.util.PluginRegistry;
49+
import org.apache.logging.log4j.core.config.properties.PropertiesConfigurationBuilder;
50+
import org.apache.logging.log4j.core.config.properties.PropertiesConfigurationFactory;
4651
import org.apache.logging.log4j.core.config.xml.XmlConfiguration;
52+
import org.apache.logging.log4j.core.config.yaml.YamlConfigurationFactory;
4753
import org.apache.logging.log4j.core.util.ShutdownCallbackRegistry;
4854
import org.apache.logging.log4j.jul.Log4jBridgeHandler;
4955
import org.apache.logging.log4j.status.StatusListener;
@@ -53,6 +59,9 @@
5359
import org.junit.jupiter.api.BeforeEach;
5460
import org.junit.jupiter.api.Test;
5561
import org.junit.jupiter.api.extension.ExtendWith;
62+
import org.junit.jupiter.params.ParameterizedTest;
63+
import org.junit.jupiter.params.provider.Arguments;
64+
import org.junit.jupiter.params.provider.MethodSource;
5665
import org.slf4j.MDC;
5766

5867
import org.springframework.boot.logging.AbstractLoggingSystemTests;
@@ -89,6 +98,7 @@
8998
* @author Andy Wilkinson
9099
* @author Ben Hale
91100
* @author Madhura Bhave
101+
* @author Piotr P. Karwasz
92102
*/
93103
@ExtendWith(OutputCaptureExtension.class)
94104
@ClassPathExclusions("logback-*.jar")
@@ -105,6 +115,8 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests {
105115

106116
private Configuration configuration;
107117

118+
private String contextName;
119+
108120
@BeforeEach
109121
void setup() {
110122
PluginRegistry.getInstance().clear();
@@ -115,6 +127,7 @@ void setup() {
115127
this.configuration = loggerContext.getConfiguration();
116128
this.loggingSystem.cleanUp();
117129
this.logger = LogManager.getLogger(getClass());
130+
this.contextName = loggerContext.getName();
118131
}
119132

120133
@AfterEach
@@ -293,54 +306,79 @@ void loggingThatUsesJulIsCaptured(CapturedOutput output) {
293306
assertThat(output).contains("Hello world");
294307
}
295308

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");
309+
static Stream<String> configLocationsWithConfigurationFileSystemProperty() {
310+
return Stream.of("log4j2.configurationFile", "log4j.configuration.location");
325311
}
326312

327-
@Test
328-
void configLocationsWithConfigurationFileSystemProperty() {
329-
System.setProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY, "custom-log4j2.properties");
313+
@ParameterizedTest
314+
@MethodSource
315+
void configLocationsWithConfigurationFileSystemProperty(String propertyName) {
316+
System.setProperty(propertyName, "custom-log4j2.properties");
330317
try {
331-
assertThat(this.loggingSystem.getStandardConfigLocations()).containsExactly("log4j2-test.properties",
332-
"log4j2-test.xml", "log4j2.properties", "log4j2.xml", "custom-log4j2.properties");
318+
assertThat(this.loggingSystem.getStandardConfigLocations()).containsExactly("custom-log4j2.properties",
319+
"log4j2-test" + this.contextName + ".xml", "log4j2-test.xml", "log4j2" + this.contextName + ".xml",
320+
"log4j2.xml");
333321
}
334322
finally {
335-
System.clearProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY);
323+
System.clearProperty(propertyName);
336324
}
337325
}
338326

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

346384
@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)