A dirty hack to ease the usage of Log4J2 in Spring Boot
Tags: java, maven, hack, logging
Published: 2020-12-13 17:13:48

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](https://blog.frankel.ch/thoughts-on-java-logging-and-slf4j/) 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.

```java
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](https://blog.frankel.ch/smart-logging-in-java8-kotlin/) 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](https://mvnrepository.com/artifact/org.slf4j/slf4j-api) at the time of this writing
2. The documentation [mentions it](http://www.slf4j.org/manual.html#fluent) in passing

I had a look at [Log4J2](https://logging.apache.org/log4j/2.x/). It has a lot of interesting features baked in:

* [Lazy logging](https://logging.apache.org/log4j/2.x/manual/api.html#Java_8_lambda_support_for_lazy_logging)
* [Garbage-free logging](https://logging.apache.org/log4j/2.x/manual/garbagefree.html)
* [Lookups](https://logging.apache.org/log4j/2.x/manual/lookups.html)
* A dedicated [JSON layout](https://logging.apache.org/log4j/2.x/manual/layouts.html#JSONLayout)
* etc.

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](https://logging.apache.org/log4j/2.x/manual/layouts.html#JSONLayout). 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:

```bash
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_:

```bash
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.

```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.

**To go further:**

* [Configure Log4j for Logging](https://docs.spring.io/spring-boot/docs/2.0.x/reference/html/howto-logging.html#howto-configure-log4j-for-logging)
* [Solving dependency conflicts in Maven](https://www.ricston.com/blog/solving-dependency-conflicts-maven/)

_Originally published at [A Java Geek](https://blog.frankel.ch/hack-ease-usage-log4j2-spring-boot) on December 13<sup>th</sup> 2020_