How do you switch a Spring Boot application from Logback to Log4j2, and what are the trade-offs?
answer
- exclude starter-logging, add starter-log4j2
- Gradle configurations.all exclude one place
- app code unchanged (SLF4J facade)
- log4j2-spring.xml + <SpringProfile>/<SpringProperty> (capitalized)
- async LMAX Disruptor; Log4Shell -> patch versions
basics
~10 sExclude spring-boot-starter-logging (which brings Logback) everywhere it's pulled in, then add spring-boot-starter-log4j2. Your SLF4J code is unchanged; put Log4j2 config in log4j2-spring.xml.
solid answer
~40 sBecause every starter transitively includes spring-boot-starter-logging (Logback), you must exclude it and add spring-boot-starter-log4j2 instead. In Maven, exclude on spring-boot-starter (or use a dependencyManagement/exclusion) and add the log4j2 starter; in Gradle, use a module-wide configurations.all { exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' }. Application code is untouched because it targets SLF4J — the log4j2 starter provides the SLF4J-to-Log4j2 binding. Name your config log4j2-spring.xml so Spring Boot loads it and enables <SpringProfile>/<SpringProperty>. Reasons to switch: async loggers via LMAX Disruptor for very high throughput, richer plugin ecosystem, and different lookup/format features. Trade-off: extra dependency management, the Log4Shell history means you must keep versions patched, and most apps are fine on Logback.
code
kotlin · 11 lines// build.gradle.kts — drop Logback everywhere, use Log4j2 instead
configurations.all {
// Every starter transitively pulls Logback in; kill it in one place
exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging")
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-log4j2")
// No application code changes — code still uses org.slf4j.Logger
}go deeper
Know it's an exclude-Logback + add-Log4j2-starter swap and that code doesn't change.
Explain the single-binding constraint and the -spring config file naming.
Weigh async/throughput benefits vs dependency and security-maintenance costs; do the Gradle one-place exclusion.
Own the org-wide decision: BOM-managed versions, CVE patch posture post-Log4Shell, and whether the throughput gain justifies deviating from the default.
## Why an exclusion is required Log4j2 and Logback are both concrete SLF4J backends. Only **one** SLF4J binding may be active. Since `spring-boot-starter-logging` (Logback) is transitively dragged in by essentially every starter, simply adding the Log4j2 starter would put two bindings on the classpath. You must first **exclude** the Logback starter. ### Maven ```xml <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> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency> ``` If multiple starters pull logging in, exclude on each (or centralize the exclusion). ### Gradle (cleaner — one place) ```groovy configurations.all { exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' } dependencies { implementation 'org.springframework.boot:spring-boot-starter-log4j2' } ``` ## What the log4j2 starter brings `spring-boot-starter-log4j2` includes `log4j-core`, `log4j-api`, and the **`log4j-slf4j2-impl`** binding (SLF4J → Log4j2). It also brings bridges so that code using the java.util.logging or Commons Logging APIs still funnels into Log4j2. Crucially your app code — which imports `org.slf4j.Logger` — needs **zero changes**. That's the whole point of the facade. ## Configuration file Analogous to Logback's `-spring` convention, name the file **`log4j2-spring.xml`** so Spring Boot's `Log4J2LoggingSystem` loads it and enables the Spring extensions `<SpringProfile>` and `<SpringProperty>` (note the different capitalization from Logback's tags). A plain `log4j2.xml` is loaded by Log4j2 directly and won't see profiles. ```xml <Configuration> <SpringProperty name="appName" source="spring.application.name"/> <Appenders> <Console name="CONSOLE" target="SYSTEM_OUT"> <PatternLayout pattern="%d %-5p [${appName}] %c{1} - %m%n"/> </Console> </Appenders> <Loggers> <SpringProfile name="dev"><Root level="DEBUG"><AppenderRef ref="CONSOLE"/></Root></SpringProfile> <SpringProfile name="prod"><Root level="INFO"><AppenderRef ref="CONSOLE"/></Root></SpringProfile> </Loggers> </Configuration> ``` ## Why choose Log4j2 - **Async Loggers** backed by the LMAX Disruptor give very high throughput and low latency for logging-heavy workloads (enable with `-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector`). - Rich plugin/appender ecosystem and flexible lookups/layouts. - Garbage-free mode option to reduce allocation. ## Trade-offs / when NOT to bother - Logback already supports async via `AsyncAppender` and is the default — most apps never need to switch. - Extra dependency-management burden; a stray transitive Logback dependency reintroduces a second binding and warnings. - **Security history:** Log4Shell (CVE-2021-44228) taught teams that Log4j2 versions must be aggressively kept patched. Spring Boot's BOM manages a safe version, but you own upgrades. - The Spring extension tags differ in casing (`<SpringProfile>` vs Logback's `<springProfile>`) — a common copy-paste bug. ## Verifying the swap At startup, a Log4j2-configured app logs via Log4j2's initialization; you can confirm no 'multiple SLF4J providers' warning appears and that the binding is `log4j-slf4j2-impl`.
- After adding spring-boot-starter-log4j2, why might you still see a 'multiple SLF4J providers' warning?A transitive dependency reintroduced Logback (spring-boot-starter-logging or logback-classic). Two bindings are on the classpath. Find it with the dependency tree and exclude it; the Gradle configurations.all exclude covers all sources at once.
- Does switching to Log4j2 require changing how you obtain loggers in code?No. Application code keeps using org.slf4j.LoggerFactory.getLogger(...). The SLF4J facade is unchanged; only the binding on the classpath differs, which is the entire benefit of coding to SLF4J.
saying these in an interview costs you the question
- Adding the log4j2 starter without excluding starter-logging (two bindings)
- Thinking application logging code must be rewritten to Log4j2's API
- Using log4j2.xml (not -spring) and expecting <SpringProfile> to work
- Copying Logback's lowercase <springProfile> into Log4j2 config
- Assuming Log4j2 is always faster/necessary when Logback already supports async