/ MAVEN, HACK, LOGGING, LOG4J2, SLF4J, SPRING BOOT

A dirty hack to ease the usage of Log4J2 in Spring Boot

Logging is one of the fundamental components of any application which runs in production. Yet, between performance and logging in critical environments, I’d favor the former. For that reason, modern logging frameworks should implement at least two requirements:

  1. Async appenders: the write operation shouldn’t be blocking the execution of the program
  2. Lazy computation: the framework doesn’t run expensive computations until they are needed - or never if that’s the case.

The first logging framework in the Java ecosystem was Log4J. When the main contributor left the project and went on to create SLF4J, Log4J became stale. More than a decade ago, I chose SLF4J over Log4J for that reason.

A couple of years ago, though, I started to become dissatisfied with SLF4J. In particular, even though Java 8 is available since 2014, it didn’t offer lazy computations.

LOGGER.debug("Cart total: {}", cart.getTotal())

In the above statement, the cart.getTotal() is an expensive call but it’s executed regardless of the log level. For example, if you set the log level to INFO, the runtime computes the cart’s total but discards it just afterward. A couple of workarounds that I described in this post are available. I find none of them satisfactory.

The 2.0 version implements lazy computations by allowing to provide Supplier arguments but:

  1. It’s available in alpha at the time of this writing
  2. The documentation mentions it in passing

I had a look at Log4J2. It has a lot of interesting features baked in:

The time has come for me to reassess my choice about my default choice for a logging framework. I’m considering to use Log4J2 in my next projects.

The problem is that Spring Boot made the same choice as I did: by default, it uses SLF4J. Spring Boot documents how to use Log4J2. It boils down to excluding the spring-boot-starter-logging in every Spring Boot starter and adding the spring-boot-starter-log4j2 dependency. This is repetitive and fragile: whenever you add a new starter, you must remember to exclude the logging starter.

Let’s hack Maven to make it easier. Here’s an extract of the result of executing mvn dependency:tree on one of my projects:

[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.4.0:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter:jar:2.4.0:compile
[INFO] |  |  +- org.springframework.boot:spring-boot-starter-logging:jar:2.4.0:compile (1)
[INFO] |  |  |  +- ch.qos.logback:logback-classic:jar:1.2.3:compile
[INFO] |  |  |  |  \- ch.qos.logback:logback-core:jar:1.2.3:compile
[INFO] |  |  |  +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.13.3:compile
[INFO] |  |  |  |  \- org.apache.logging.log4j:log4j-api:jar:2.13.3:compile
[INFO] |  |  |  \- org.slf4j:jul-to-slf4j:jar:1.7.30:compile
1 We want this one to go away because it brings in Logback transitively

The first step is to create an empty JAR:

touch foo
zip empty.zip foo
zip -d empty.zip foo
rm foo

The second step is to add it to our local Maven repository under the same coordinates as the legitimate starter but with a higher version number:

mvn install:install-file -Dfile=empty.zip \
                         -DgroupId=org.springframework.boot \
                         -DartifactId=spring-boot-starter-logging \
                         -Dversion=99 \
                         -Dpackaging=jar

ls $HOME/.m2/repository/org/springframework/boot/spring-boot-starter-logging/99

The third and final step is to add the newly created dependency to our POM. Because of its closest-version wins strategy, Maven will choose this direct dependency over other transitive dependencies. We shouldn’t forget to add the Log4J2 starter as well.

pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-logging</artifactId>
    <version>99</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

mvn dependency:tree now yields the desired result:

[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.4.0:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter:jar:2.4.0:compile      (1)
...
[INFO] +- org.springframework.boot:spring-boot-starter-logging:jar:99:compile    (2)
[INFO] \- org.springframework.boot:spring-boot-starter-log4j2:jar:2.4.0:compile  (3)
[INFO]    +- org.apache.logging.log4j:log4j-slf4j-impl:jar:2.13.3:compile
[INFO]    |  \- org.apache.logging.log4j:log4j-api:jar:2.13.3:compile
[INFO]    +- org.apache.logging.log4j:log4j-core:jar:2.13.3:compile
[INFO]    +- org.apache.logging.log4j:log4j-jul:jar:2.13.3:compile
[INFO]    \- org.slf4j:jul-to-slf4j:jar:1.7.30:compile
1 No more default logging starter
2 Our custom JAR that is empty
3 Log4J2 dependency

Note that this approach will work on one’s machine. If you deploy the empty JAR to your enterprise Maven proxy repository, it will work inside of it as well. But it won’t work on machines that don’t have access to the empty JAR: thus, it’s not an option for publicly available projects. This is a hack after all, albeit an elegant one.

Nicolas Fränkel

Nicolas Fränkel

Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Also double as a trainer and triples as a book author.

Read More
A dirty hack to ease the usage of Log4J2 in Spring Boot
Share this