Keywords: Spring Boot | Log4j2 | Logging Configuration Conflict | Gradle Dependency Management | SLF4J Bridge
Abstract: This article provides an in-depth analysis of common logging configuration conflicts in Spring Boot projects, particularly the LoggingException caused by the simultaneous presence of log4j-slf4j-impl and log4j-to-slf4j. By examining Gradle dependency management mechanisms, it offers a solution to exclude the spring-boot-starter-logging module at the root level, comparing different exclusion approaches. With practical code examples, the paper explains how Log4j2 and SLF4J bridges work, helping developers understand logging framework integration and avoid similar configuration errors.
Problem Context and Error Analysis
In Spring Boot 2.x projects, developers often encounter the following runtime exception when attempting to use Log4j2 as the logging framework:
Caused by: org.apache.logging.log4j.LoggingException: log4j-slf4j-impl cannot be present with log4j-to-slf4j
The core issue lies in the conflict between logging framework bridges. Log4j2 provides two distinct SLF4J bridge modules: log4j-slf4j-impl allows SLF4J API calls to be handled by Log4j2, while log4j-to-slf4j redirects Log4j2 API calls to SLF4J. When both are present on the classpath, they create a circular dependency, causing initialization failure.
Common Pitfalls in Dependency Configuration
Many developers exclude the default logging framework only from specific starter modules in Gradle configurations, for example:
dependencies {
implementation('org.springframework.boot:spring-boot-starter') {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
}
The problem with this approach is that other Spring Boot starters (such as spring-boot-starter-web, spring-boot-starter-data-jpa, etc.) may still introduce spring-boot-starter-logging, which by default includes Logback and potentially conflicting bridges.
Fundamental Solution
According to Spring Boot official documentation, the correct approach is to globally exclude the spring-boot-starter-logging module at the configuration level. Here is a complete configuration example for Gradle projects:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
// Other dependencies...
}
configurations {
all {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
}
This configuration ensures that no modules introduce the default Logback dependencies, thereby avoiding bridge conflicts. In contrast, Maven projects can achieve similar results by adding exclusions to the spring-boot-starter dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
Considerations for Log4j2 Configuration Files
After resolving dependency conflicts, it is essential to correctly configure the log4j2.xml file. Note that Log4j2 configuration syntax differs from Logback, particularly in namespaces and class names. Below is a corrected example snippet:
<?xml version="1.0" encoding="UTF-8" ?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
Key differences include using <Configuration> instead of <log4j:configuration>, and correct Appender class names (e.g., ConsoleAppender in Log4j2 corresponds to Console).
Code-Level Integration Verification
Once configured, you can verify that logging output works correctly in controllers. The following example demonstrates proper use of SLF4J API with Log4j2 integration:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
private static final Logger logger = LoggerFactory.getLogger(DemoController.class);
@GetMapping("/test")
public String testEndpoint() {
logger.debug("Debug message: Entering testEndpoint method");
logger.info("Business processing completed");
return "Success";
}
}
This approach ensures that SLF4J API calls are automatically routed to Log4j2 implementation without causing bridge conflicts.
Additional Notes and Best Practices
Beyond the above solution, consider the following points:
- Version Compatibility: Ensure the Spring Boot version used (e.g., 2.3.0+) natively supports Log4j2 to avoid introducing incompatible bridges.
- Dependency Inspection: Regularly check the dependency tree using
gradle dependenciesormvn dependency:treecommands to confirm no unexpected logging frameworks are introduced. - Testing Verification: After integrating Log4j2, write simple test cases to verify that log output meets expectations, particularly for debug-level logs.
By systematically excluding the default logging framework, correctly configuring Log4j2 dependencies and configuration files, developers can completely resolve conflicts between log4j-slf4j-impl and log4j-to-slf4j, building a stable and reliable logging system.