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

Nicolas Fränkel - Dec 13 '20 - - Dev Community

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())
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

<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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  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.

To go further:

Originally published at A Java Geek on December 13th 2020

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .