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 a large number of frameworks and libraries. SLF4J has recently been updated to version 2.0.x. In this article I am going to explain the new features of SLF4J 2.x.

Java version requirements

Versions of SLF4J up to 1.7.x require at least Java 6, which is now considered a very old Java release. The new SLF4J 2.x version requires at least Java 8. Keep in mind that Java 8 is a 2014 release, so nowadays Java 8 is absolutely a reasonable minimum 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. The new SLF4J 2.x version instead relies on the well known ServiceLoader mechanism.

But first of all, I would like to talk about the static binder mechanism. In SLF4J 1.x the source code of the slf4j-api artifact contains the following three particular classes:

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

The source code of these classes is just only a “dummy” implementation, without any particular use.

When the slf4j-api artifact is built using Maven, the three respective .class files are deleted during the build phase. You can check this special 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 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> task, physically removes the org/slf4j/impl package structure under target/classes and hence also removing the files StaticLoggerBinder.class, StaticMDCBinder.class and StaticMarkerBinder.class.

So what is the real reason for this removal? The reason is rather 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-simple, slf4j-jdk14, etc… Clearly, only one logging backend can be managed, not two or more.

This binding is exactly like any class A that requires a class B because A accesses a constructor, method or field of B. This is indeed generally called a “static binding” in Java.

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 that was 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. Just to make a short (and certainly not exhaustive) list, it is used 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 there is now a new interface named org.slf4j.spi.SLF4JServiceProvider (to be precise, this interface exists 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 interface. Once an implementation is found, the API uses three methods of SLF4JServiceProvider to find logger, marker and MDC concrete strategies:

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

Note that ILoggerFactory, IMarkerFactory and MDCAdapter are not new interfaces. They already existed before version 2.x.

The issue about API version and logging backend version

This change in SLF4J 2.x is certainly a great improvement over the static binder mechanism, because implementation classes do not need to be exactly named like org.slf4j.impl.StaticLoggerBinder.

However, there is an important issue 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 feature, a new “fluent” API. Normally you use the 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 having single logging methods like these is that there must be many versions in overload. The Logger interface in SLF4J version 1.7.36 has 10 methods for each of debug/error/info/trace/warn levels (thus, a total of 50 methods plus some others!).

The 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, setMessage on the LoggingEventBuilder object. All these methods return the same LoggingEventBuilder object on which the method is invoked (classic “builder” pattern).
  3. You perform the actual logging by calling one of the log methods, which are “terminal” methods.

If a logging level is disabled, the respective atXyz() method returns a LoggingEventBuilder object that has 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();

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 useful for several reasons:

  1. You can pass as many arguments as you want by calling many times the addArgument method.
  2. There is an addArgument method that receives a supplier (java.util.function.Supplier) to provide an argument only if it is really necessary.
  3. You can add multiple Marker objects (with single logging methods you can pass only one Marker object).
  4. You can add key-value pairs using the addKeyValue methods. 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 (about CPU/memory usage), this may be an important 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 longer 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 that there is no need to change or upgrade existing logging frameworks. The fluent API is only handled at the SLF4J level and logging frameworks don’t know anything about it.

Similar Posts