Skip to content

Commit 7d34320

Browse files
ppkarwaszphilwebb
authored andcommitted
Simplify Log4J2LoggingSystem
Streamline and harden `Log4J2LoggingSystem` by delegating more functionality. The commit makes updates in two key areas: Previously, each method fetched the `LoggerContext` directly from `LogManager` and cast it to `....core.LoggerContext`. This approach has several issues: * ClassCastException risks: - When Log4j Core is on the classpath but not the active implementation (e.g. when `log4j-to-slf4j` is used). - During shutdown, when `LogManager` may return a `SimpleLoggerContext` (see #26953). * Unexpected reinitialization: - If the logger context had already been stopped, `Log4J2LoggingSystem` would trigger creation of a new context, even mid-shutdown. Configuration file detection was previously hardcoded in `Log4J2LoggingSystem`, which limited flexibility: * Harder to support additional configuration formats. * Coupled Spring Boot to internal Log4j Core classes such as `AuthorizationProvider`. This change now delegates configuration resolution to Log4j. This reduces reliance on internal APIs and allows Log4j Core to handle configuration formats and factories more naturally. Signed-off-by: Piotr P. Karwasz <[email protected]> See gh-47424
1 parent debbec8 commit 7d34320

File tree

6 files changed

+191
-378
lines changed

6 files changed

+191
-378
lines changed

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

Lines changed: 95 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@
1616

1717
package org.springframework.boot.logging.log4j2;
1818

19-
import java.io.FileNotFoundException;
2019
import java.io.IOException;
21-
import java.io.InputStream;
22-
import java.net.URL;
23-
import java.net.URLConnection;
2420
import java.util.ArrayList;
2521
import java.util.Collections;
2622
import java.util.LinkedHashMap;
@@ -37,15 +33,11 @@
3733
import org.apache.logging.log4j.core.LoggerContext;
3834
import org.apache.logging.log4j.core.config.AbstractConfiguration;
3935
import org.apache.logging.log4j.core.config.Configuration;
36+
import org.apache.logging.log4j.core.config.ConfigurationException;
4037
import org.apache.logging.log4j.core.config.ConfigurationFactory;
41-
import org.apache.logging.log4j.core.config.ConfigurationSource;
4238
import org.apache.logging.log4j.core.config.LoggerConfig;
4339
import org.apache.logging.log4j.core.config.composite.CompositeConfiguration;
4440
import org.apache.logging.log4j.core.filter.DenyAllFilter;
45-
import org.apache.logging.log4j.core.net.UrlConnectionFactory;
46-
import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
47-
import org.apache.logging.log4j.core.net.ssl.SslConfigurationFactory;
48-
import org.apache.logging.log4j.core.util.AuthorizationProvider;
4941
import org.apache.logging.log4j.core.util.NameUtil;
5042
import org.apache.logging.log4j.jul.Log4jBridgeHandler;
5143
import org.apache.logging.log4j.status.StatusConsoleListener;
@@ -72,7 +64,6 @@
7264
import org.springframework.core.io.ResourceLoader;
7365
import org.springframework.util.Assert;
7466
import org.springframework.util.ClassUtils;
75-
import org.springframework.util.CollectionUtils;
7667
import org.springframework.util.StringUtils;
7768

7869
/**
@@ -88,6 +79,8 @@
8879
*/
8980
public class Log4J2LoggingSystem extends AbstractLoggingSystem {
9081

82+
private static final org.apache.logging.log4j.Logger STATUS_LOGGER = StatusLogger.getLogger();
83+
9184
private static final String OPTIONAL_PREFIX = "optional:";
9285

9386
/**
@@ -100,41 +93,6 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem {
10093
*/
10194
static final String LOG4J_LOG_MANAGER = "org.apache.logging.log4j.jul.LogManager";
10295

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

14098
static final String ENVIRONMENT_KEY = Conventions.getQualifiedAttributeName(Log4J2LoggingSystem.class,
@@ -157,73 +115,69 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem {
157115

158116
private static final Filter FILTER = DenyAllFilter.newBuilder().build();
159117

160-
public Log4J2LoggingSystem(ClassLoader classLoader) {
161-
super(classLoader);
162-
}
118+
private final LoggerContext loggerContext;
163119

164-
@Override
165-
protected String[] getStandardConfigLocations() {
166-
List<String> locations = new ArrayList<>();
167-
addLocationsFromProperties(locations);
168-
addStandardLocations(locations);
169-
return StringUtils.toStringArray(locations);
120+
/**
121+
* Create a new {@link Log4J2LoggingSystem} instance.
122+
* @param classLoader the class loader to use.
123+
* @param loggerContext the {@link LoggerContext} to use.
124+
*/
125+
Log4J2LoggingSystem(ClassLoader classLoader, LoggerContext loggerContext) {
126+
super(classLoader);
127+
this.loggerContext = loggerContext;
170128
}
171129

172-
private void addLocationsFromProperties(List<String> locations) {
173-
for (String property : List.of("log4j2.configurationFile", "log4j.configuration.location")) {
174-
String propertyDefinedLocation = PropertiesUtil.getProperties().getStringProperty(property);
175-
if (propertyDefinedLocation != null) {
176-
locations.add(propertyDefinedLocation);
177-
}
130+
/**
131+
* Create a new {@link Log4J2LoggingSystem} instance.
132+
* @param classLoader the class loader to use
133+
* @return a new {@link Log4J2LoggingSystem} instance
134+
* @throws IllegalStateException if Log4j Core is not the active Log4j API provider.
135+
*/
136+
private static Log4J2LoggingSystem createLoggingSystem(ClassLoader classLoader) {
137+
org.apache.logging.log4j.spi.LoggerContext loggerContext = LogManager.getContext(classLoader, false);
138+
if (loggerContext instanceof LoggerContext) {
139+
return new Log4J2LoggingSystem(classLoader, (LoggerContext) loggerContext);
178140
}
141+
throw new IllegalStateException("Log4j Core is not the active Log4j API provider");
179142
}
180143

181-
private void addStandardLocations(List<String> locations) {
182-
LoggerContext loggerContext = getLoggerContext();
183-
String contextName = loggerContext.getName();
184-
List<String> extensions = getStandardConfigExtensions();
185-
addLocation(locations, "log4j2-test" + contextName, extensions);
186-
addLocation(locations, "log4j2-test", extensions);
187-
addLocation(locations, "log4j2" + contextName, extensions);
188-
addLocation(locations, "log4j2", extensions);
189-
}
190-
191-
private List<String> getStandardConfigExtensions() {
192-
List<String> extensions = new ArrayList<>();
193-
// These classes need to be visible by the classloader that loads Log4j Core.
194-
ClassLoader classLoader = LoggerContext.class.getClassLoader();
195-
// The order of the extensions corresponds to the order in which Log4j Core 2 and
196-
// 3 will try to load them, in decreasing value of @Order.
197-
if (isPresent(classLoader, PROPS_CONFIGURATION_FACTORY_V2)
198-
|| isPresent(classLoader, PROPS_CONFIGURATION_FACTORY_V3)) {
199-
extensions.add(".properties");
200-
}
201-
if (isPresent(classLoader, YAML_CONFIGURATION_FACTORY_V2, YAML_TREE_PARSER_V2)
202-
|| isPresent(classLoader, YAML_CONFIGURATION_FACTORY_V3)) {
203-
Collections.addAll(extensions, ".yaml", ".yml");
204-
}
205-
if (isPresent(classLoader, JSON_TREE_PARSER_V2) || isPresent(classLoader, JSON_TREE_PARSER_V3)) {
206-
Collections.addAll(extensions, ".json", ".jsn");
207-
}
208-
extensions.add(".xml");
209-
return extensions;
144+
/**
145+
* {@inheritDoc}
146+
* @deprecated Since 4.0.0, in favor of the {@link ConfigurationFactory} SPI.
147+
*/
148+
@Override
149+
@Deprecated(since = "4.0.0", forRemoval = true)
150+
protected String[] getStandardConfigLocations() {
151+
return new String[] { "log4j2.xml" };
210152
}
211153

212-
private void addLocation(List<String> locations, String location, List<String> extensions) {
213-
extensions.forEach((extension) -> locations.add(location + extension));
154+
@Override
155+
protected @Nullable String getSelfInitializationConfig() {
156+
Configuration currentConfiguration = getLoggerContext().getConfiguration();
157+
return getConfigLocation(currentConfiguration);
214158
}
215159

216-
private boolean isPresent(ClassLoader classLoader, String... classNames) {
217-
for (String className : classNames) {
218-
if (!isClassAvailable(classLoader, className)) {
219-
return false;
220-
}
160+
@Override
161+
protected @Nullable String getSpringInitializationConfig() {
162+
ConfigurationFactory configurationFactory = ConfigurationFactory.getInstance();
163+
try {
164+
Configuration springConfiguration = configurationFactory.getConfiguration(getLoggerContext(), "-spring",
165+
null, getClassLoader());
166+
String configLocation = getConfigLocation(springConfiguration);
167+
return (configLocation != null && configLocation.contains("-spring")) ? configLocation : null;
168+
}
169+
catch (ConfigurationException ex) {
170+
STATUS_LOGGER.warn("Could not load Spring-specific Log4j Core configuration", ex);
171+
return null;
221172
}
222-
return true;
223173
}
224174

225-
protected boolean isClassAvailable(ClassLoader classLoader, String className) {
226-
return ClassUtils.isPresent(className, classLoader);
175+
private @Nullable String getConfigLocation(Configuration configuration) {
176+
// The location may be:
177+
// - null: if DefaultConfiguration is used (no explicit config loaded)
178+
// - a file path: if provided explicitly by the user
179+
// - a URI: if loaded from the classpath default or a custom location
180+
return configuration.getConfigurationSource().getLocation();
227181
}
228182

229183
@Deprecated(since = "4.0.0", forRemoval = true)
@@ -335,7 +289,7 @@ private void load(LoggingInitializationContext initializationContext, String loc
335289
Environment environment = initializationContext.getEnvironment();
336290
Assert.state(environment != null, "'environment' must not be null");
337291
applySystemProperties(environment, logFile);
338-
loadConfiguration(location, logFile, overrides);
292+
reconfigure(location, overrides);
339293
}
340294

341295
private List<String> getOverrides(LoggingInitializationContext initializationContext) {
@@ -346,66 +300,56 @@ private List<String> getOverrides(LoggingInitializationContext initializationCon
346300
return overrides.orElse(Collections.emptyList());
347301
}
348302

349-
/**
350-
* Load the configuration from the given {@code location}, creating a composite using
351-
* the configuration from the given {@code overrides}.
352-
* @param location the location
353-
* @param logFile log file configuration
354-
* @param overrides the overriding locations
355-
* @since 2.6.0
356-
*/
357-
protected void loadConfiguration(String location, @Nullable LogFile logFile, List<String> overrides) {
303+
private void reconfigure(String location, List<String> overrides) {
358304
Assert.notNull(location, "'location' must not be null");
359305
try {
360306
List<Configuration> configurations = new ArrayList<>();
361-
LoggerContext context = getLoggerContext();
362-
ResourceLoader resourceLoader = ApplicationResourceLoader.get();
363-
configurations.add(load(resourceLoader.getResource(location), context));
307+
ResourceLoader resourceLoader = ApplicationResourceLoader.get(getClassLoader());
308+
configurations.add(load(resourceLoader, location));
364309
for (String override : overrides) {
365-
Configuration overrideConfiguration = loadOverride(resourceLoader, override, context);
310+
Configuration overrideConfiguration = loadOverride(resourceLoader, override);
366311
if (overrideConfiguration != null) {
367312
configurations.add(overrideConfiguration);
368313
}
369314
}
370-
context.reconfigure(mergeConfigurations(configurations));
315+
this.loggerContext.reconfigure(mergeConfigurations(configurations));
371316
}
372317
catch (Exception ex) {
373-
throw new IllegalStateException("Could not initialize Log4J2 logging from " + location, ex);
318+
String message = "Could not initialize Log4J2 logging from " + location;
319+
if (!overrides.isEmpty()) {
320+
message += " with overrides " + overrides;
321+
}
322+
throw new IllegalStateException(message, ex);
374323
}
375324
}
376325

377-
private Configuration load(Resource resource, LoggerContext context) throws IOException {
326+
private Configuration load(ResourceLoader resourceLoader, String location) throws IOException {
378327
ConfigurationFactory factory = ConfigurationFactory.getInstance();
379-
if (resource.isFile()) {
380-
try (InputStream inputStream = resource.getInputStream()) {
381-
return factory.getConfiguration(context, new ConfigurationSource(inputStream, resource.getFile()));
382-
}
383-
}
384-
URL url = resource.getURL();
385-
AuthorizationProvider authorizationProvider = ConfigurationFactory
386-
.authorizationProvider(PropertiesUtil.getProperties());
387-
SslConfiguration sslConfiguration = url.getProtocol().equals("https")
388-
? SslConfigurationFactory.getSslConfiguration() : null;
389-
URLConnection connection = UrlConnectionFactory.createConnection(url, 0, sslConfiguration,
390-
authorizationProvider);
391-
try (InputStream inputStream = connection.getInputStream()) {
392-
return factory.getConfiguration(context,
393-
new ConfigurationSource(inputStream, url, connection.getLastModified()));
328+
Resource resource = resourceLoader.getResource(location);
329+
Configuration configuration = factory.getConfiguration(getLoggerContext(), null, resource.getURI(),
330+
getClassLoader());
331+
// The error handling in Log4j Core 2.25.x is not consistent:
332+
// some loading and parsing errors result in a null configuration,
333+
// others in an exception.
334+
if (configuration == null) {
335+
throw new ConfigurationException("Could not load Log4j Core configuration from " + location);
394336
}
337+
return configuration;
395338
}
396339

397-
private @Nullable Configuration loadOverride(ResourceLoader resourceLoader, String location, LoggerContext context)
398-
throws IOException {
340+
private @Nullable Configuration loadOverride(ResourceLoader resourceLoader, String location) throws IOException {
399341
if (location.startsWith(OPTIONAL_PREFIX)) {
400-
Resource resource = resourceLoader.getResource(location.substring(OPTIONAL_PREFIX.length()));
342+
String actualLocation = location.substring(OPTIONAL_PREFIX.length());
343+
Resource resource = resourceLoader.getResource(actualLocation);
401344
try {
402-
return (resource.exists()) ? load(resource, context) : null;
345+
return (resource.exists()) ? load(resourceLoader, actualLocation) : null;
403346
}
404-
catch (FileNotFoundException ex) {
347+
catch (ConfigurationException | IOException ex) {
348+
STATUS_LOGGER.debug("Could not load optional Log4j2 override from {}", actualLocation, ex);
405349
return null;
406350
}
407351
}
408-
return load(resourceLoader.getResource(location), context);
352+
return load(resourceLoader, location);
409353
}
410354

411355
private Configuration mergeConfigurations(List<Configuration> configurations) {
@@ -417,33 +361,11 @@ private Configuration mergeConfigurations(List<Configuration> configurations) {
417361

418362
@Override
419363
protected void reinitialize(LoggingInitializationContext initializationContext) {
420-
List<String> overrides = getOverrides(initializationContext);
421-
if (!CollectionUtils.isEmpty(overrides)) {
422-
reinitializeWithOverrides(overrides);
423-
}
424-
else {
425-
LoggerContext context = getLoggerContext();
426-
context.reconfigure();
427-
}
428-
}
429-
430-
private void reinitializeWithOverrides(List<String> overrides) {
431-
LoggerContext context = getLoggerContext();
432-
List<Configuration> configurations = new ArrayList<>();
433-
configurations.add(context.getConfiguration());
434-
ResourceLoader resourceLoader = ApplicationResourceLoader.get();
435-
for (String override : overrides) {
436-
try {
437-
Configuration overrideConfiguration = loadOverride(resourceLoader, override, context);
438-
if (overrideConfiguration != null) {
439-
configurations.add(overrideConfiguration);
440-
}
441-
}
442-
catch (IOException ex) {
443-
throw new RuntimeException("Failed to load overriding configuration from '" + override + "'", ex);
444-
}
445-
}
446-
context.reconfigure(mergeConfigurations(configurations));
364+
String currentLocation = getSelfInitializationConfig();
365+
// `reinitialize` is only triggered when `getSelfInitializationConfig` returns a
366+
// non-null value
367+
Assert.notNull(currentLocation, "'currentLocation' must not be null");
368+
load(initializationContext, currentLocation, null);
447369
}
448370

449371
@Override
@@ -584,8 +506,8 @@ public void cleanUp() {
584506
return configuration.getLoggers().get(name);
585507
}
586508

587-
private LoggerContext getLoggerContext() {
588-
return (LoggerContext) LogManager.getContext(false);
509+
LoggerContext getLoggerContext() {
510+
return this.loggerContext;
589511
}
590512

591513
private boolean isAlreadyInitialized(LoggerContext loggerContext) {
@@ -622,15 +544,20 @@ protected String getDefaultLogCorrelationPattern() {
622544
@Order(0)
623545
public static class Factory implements LoggingSystemFactory {
624546

625-
static final String LOG4J_CORE_CONTEXT_FACTORY = "org.apache.logging.log4j.core.impl.Log4jContextFactory";
547+
private static final String LOG4J_CORE_CONTEXT_FACTORY = "org.apache.logging.log4j.core.impl.Log4jContextFactory";
626548

627549
private static final boolean PRESENT = ClassUtils.isPresent(LOG4J_CORE_CONTEXT_FACTORY,
628550
Factory.class.getClassLoader());
629551

630552
@Override
631553
public @Nullable LoggingSystem getLoggingSystem(ClassLoader classLoader) {
632554
if (PRESENT) {
633-
return new Log4J2LoggingSystem(classLoader);
555+
try {
556+
return createLoggingSystem(classLoader);
557+
}
558+
catch (IllegalStateException ex) {
559+
// Log4j Core is not the active Log4j API provider
560+
}
634561
}
635562
return null;
636563
}

0 commit comments

Comments
 (0)