The new features of SLF4J 2.x

SLF4J (www.slf4j.org) is a logging “facade” library that is very popular among Java developers because it is used by many frameworks and libraries. SLF4J has recently been updated to version 2.0.x. In this article, I will explain the new features of SLF4J 2.x.

Java version requirements

Versions of SLF4J up to 1.7.x require at least Java 6, now considered a rather “old” Java release. The new SLF4J 2.x version requires at least Java 8. Remember that Java 8 is a 2014 release, so nowadays, Java 8 is the minimum reasonable version for all new Java projects!

A different way to find the logging backend

All versions of SLF4J on the 1.x stream were based on a simple static binder mechanism to find a logging backend. Instead, the new SLF4J 2.x version relies on the ServiceLoader mechanism. But first of all, I would like to talk about the static binder mechanism.

The old static binder mechanism

In SLF4J 1.x, the slf4j-api artifact contains the following three particular classes:

  • org.slf4j.impl.StaticLoggerBinder
  • org.slf4j.impl.StaticMarkerBinder
  • org.slf4j.impl.StaticMDCBinder

These three classes are used to get the Logger Factory, the Marker Factory, and the MDC Adapter. However, in the slf4j-api project, the source code of these classes is only a “dummy” implementation with no particular usefulness.

When the slf4j-api artifact is built using Maven, the three individual .class files are deleted during the build phase. You can check this particular step by looking at the pom.xml file of the slf4j-api artifact (any 1.x version), for example, the following:

https://github.com/qos-ch/slf4j/blob/v_1.7.36/slf4j-api/pom.xml

You can see (near the end of the file) that the maven-antrun-plugin has been configured to execute two specific tasks:

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-antrun-plugin</artifactId>

            ........

        <configuration>
          <tasks>
            <echo>Removing slf4j-api's dummy StaticLoggerBinder and StaticMarkerBinder</echo>
            <delete dir="target/classes/org/slf4j/impl"/>
          </tasks>
        </configuration>
      </plugin>

The second task, a <delete>, physically removes the org/slf4j/impl package structure under target/classes. In practice, it removes the three class files StaticLoggerBinder.class, StaticMDCBinder.class, and StaticMarkerBinder.class.

So what is the real reason for this removal? The reason is simple: at runtime, the slf4j-api artifact expects to find the three classes (exactly with that name and package!) in an external logging backend like one of the artifacts slf4j-nop, slf4j-simple, slf4j-log4j12, etc. This is also why only one logging backend can be managed, not two or more.

This binding is precisely like any class A that requires a class B because A accesses a constructor/method/field of B.

The new lookup strategy using the ServiceLoader

Since SLF4J 2.x, the static binder mechanism has been abandoned in favor of the well-known ServiceLoader mechanism.

The java.util.ServiceLoader is a class introduced in Java 6 to provide a simple but effective way to find implementations of a “service” located somewhere on the “classpath” (mainly into .jar files). I suggest you start reading the Javadoc documentation of ServiceLoader if you don’t know anything about this mechanism.

The ServiceLoader mechanism is widely used in many frameworks, libraries, and APIs, for example, by:

  • the JDBC API (since version 4.0) to find JDBC drivers
  • the Java Scripting API (JSR-223) to find factories of Scripting engines
  • the JAXB API to find JAXB implementations
  • the JAXP API to find factories of XML parsers/transformers
  • the JAX-RS API to find implementations of message body readers/writers
  • the Jackson library to find factories/codecs/modules
  • the Eclipse Microprofile to find several config related services (config sources, converters, etc…)
  • the Micronaut Framework to find implementations of various kind of services

In SLF4J 2.x, a new interface is now named org.slf4j.spi.SLF4JServiceProvider. This interface, to be precise, has existed since version 1.8.0, but this version was only released as alpha/beta. The SLF4J 2.x API uses the ServiceLoader mechanism to find an implementation of this SLF4JServiceProvider interface. Once an implementation is found, the API uses the following three methods to find the same Logger Factory, Marker Factory, and MDC Adapter as in the 1.x version:

  • public ILoggerFactory getLoggerFactory()
  • public IMarkerFactory getMarkerFactory()
  • public MDCAdapter getMDCAdapter()

Note that ILoggerFactory, IMarkerFactory, and MDCAdapter are not new interfaces. They already existed in SLF4J 1.x.

The issue about the API version and the logging backend version

This change in SLF4J 2.x is an excellent improvement over the static binder mechanism because implementation classes do not need to be precisely named like org.slf4j.impl.StaticLoggerBinder. However, a critical issue is to consider: newer 2.x backends cannot work with the older 1.x API, and older 1.x backends cannot work with the newer 2.x API.

Therefore, if you see the “infamous” output notice:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

Or for SLF4J 2.x API:

SLF4J: No SLF4J providers were found.
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See https://www.slf4j.org/codes.html#noProviders for further details.

Then check in the following order:

  1. That you have one of the SLF4J logging backends (slf4j-simple or slf4j-log4j12 or slf4j-jcl, etc.)

  2. That the SLF4J API version and the SLF4J logging backend version are coherent:

    • 1.x API with 1.x backend
      or
    • 2.x API with 2.x backend

The new “fluent” API

The SLF4J 2.x version also provides another interesting new feature, a “fluent” API. You usually use the standard SLF4J API as in the following examples:

logger.info("Starting elaboration of informations");

logger.trace("Calculated x={} and y={}", x, y);

logger.error("Failed to load the image", anException);

The disadvantage of single logging methods like these is that many versions must be overloaded. For example, the Logger interface in SLF4J version 1.7.36 has 10 methods in overload for each debug/error/info/trace/warn level. Thus, a total of 50 methods plus some others!

The new fluent API can be used by applying the following steps:

  1. You use one of the atXyz() methods (atDebug(), atError(), atInfo(), etc.) on a Logger instance to obtain an object of type LoggingEventBuilder (an interface).

  2. You can then use methods like addArgument, addKeyValue, addMarker, setCause, and setMessage on the LoggingEventBuilder object. All these methods return the same LoggingEventBuilder object on which the method is invoked (this is a classic “builder” style).

  3. You perform the actual logging by calling one of the log methods, which are “terminal” operations.

If a logging level is disabled, the respective atXyz() method returns a LoggingEventBuilder object with a no-operation implementation. In practice, it does absolutely nothing!

So, for example:

logger.atTrace().setMessage("Calculated x={} and y={}")
        .addArgument(x).addArgument(y).log();

It is equivalent to:

logger.trace("Calculated x={} and y={}", x, y);

The above example may not be particularly eloquent; however, the new fluent API is helpful for several reasons:

  1. You can pass as many arguments as you want by calling the addArgument method many times.
  2. There is an addArgument method that receives a supplier (java.util.function.Supplier) to provide an argument only if it is actually necessary.
  3. You can add multiple Marker objects (only one Marker object is possible with single logging methods).
  4. You can add key-value pairs using the addKeyValue method. The default Logger implementation formats key-value pairs as key=value, adding them before the logging message, with multiple pairs separated by a space.

The addArgument with Supplier

The addArgument method with a Supplier requires a little explanation. If you do the following:

logger.trace("Computed data: {}", someobj.getData());

The getData() method is always invoked, even if the trace level is disabled. If getData() is particularly costly for CPU/memory usage, this may be a significant downside in some scenarios. The classic “idiom” with older SLF4J versions was to use the isTraceEnabled() method like this:

if (logger.isTraceEnabled()) {
    logger.trace("Computed data: {}", someobj.getData());
}

However, this is long and spans more lines. Instead, with SLF4J 2.x you can do the following:

logger.atTrace().setMessage("Computed data: {}").addArgument(() -> someobj.getData()).log();

Or, slightly shorter:

logger.atTrace().addArgument(() -> someobj.getData()).log("Computed data: {}");

In this way, the Supplier is never invoked if the trace level is disabled.

A note on compatibility

The SLF4J official documentation states that the new fluent API is backward-compatible. It means there is no need to change or upgrade existing logging frameworks. Indeed, the fluent API is only handled at the SLF4J level, and logging frameworks know nothing about it.

Similar Posts