1. Introduction
In this tutorial, we’ll explore the newly introduced Logging API in Java 9 and implement some examples to cover the most common cases.
This API has been introduced in Java to provide a common mechanism to handle all the platform logs and to expose a service interface that can be customized by libraries and applications. This way, the JDK platform logs can use the same logging framework as the application, and the project dependencies can be reduced.
2. Creating a Custom Implementation
In this section, we’re going to show the main classes of the Logging API that we have to implement to create a new logger. We’ll do so by implementing a simple logger that prints all the logs to the console.
2.1. Creating the Logger
The main class that we have to create is the Logger. This class has to implement the System.Logger interface and these four methods at least:
- getName(): returns the name of the logger. It’ll be used by the JDK to create loggers by name
- isLoggable(): indicates what levels the logger is enabled for
- log(): it’s the method that prints the log to whatever underlying system the application is using- the console in our case. There are 2 log() methods to implement, each of them receiving different parameters
Let’s see how our implementation will look like:
public class ConsoleLogger implements System.Logger {
@Override
public String getName() {
return "ConsoleLogger";
}
@Override
public boolean isLoggable(Level level) {
return true;
}
@Override
public void log(Level level, ResourceBundle bundle, String msg, Throwable thrown) {
System.out.printf("ConsoleLogger [%s]: %s - %s%n", level, msg, thrown);
}
@Override
public void log(Level level, ResourceBundle bundle, String format, Object... params) {
System.out.printf("ConsoleLogger [%s]: %s%n", level,
MessageFormat.format(format, params));
}
}
Our ConsoleLogger class overrides the four methods mentioned. The getName() method returns a String, while the isLoggable() method returns true in all cases. Finally, we have the 2 log() method that output to the console.
2.2. Creating the LoggerFinder
Once we have our logger created, we need to implement a LoggerFinder that creates instances of our ConsoleLogger.
To do so, we have to extend the abstract class System.LoggerFinder and implement the getLogger() method:
public class CustomLoggerFinder extends System.LoggerFinder {
@Override
public System.Logger getLogger(String name, Module module) {
return new ConsoleLogger();
}
}
In this case, we’re always returning our ConsoleLogger.
Finally, we need to register our LoggerFinder as a Service so it can be discovered by the JDK. If we don’t provide an implementation, the SimpleConsoleLogger will be used by default.
The mechanism used by the JDK to load the implementations is the ServiceLoader. You can find more information about it in this tutorial.
Since we’re using Java 9, we’ll package our class in a module and register our service in the module-info.java file:
module com.baeldung.logging {
provides java.lang.System.LoggerFinder
with com.baeldung.logging.CustomLoggerFinder;
exports com.baeldung.logging;
}
For more information about Java modules, check out this other tutorial.
2.3. Testing Our Example
To test our example, let’s create another module that will act as an application. This will only contain the Main class that uses our service implementation.
This class will get an instance of our ConsoleLogger by calling the System.getLogger() method:
public class MainApp {
private static System.Logger LOGGER = System.getLogger("MainApp");
public static void main(String[] args) {
LOGGER.log(Level.ERROR, "error test");
LOGGER.log(Level.INFO, "info test");
}
}
Internally, the JDK will pick up our CustomLoggerFinder implementation and create an instance of our ConsoleLogger.
After that, let’s create the module-info file for this module:
module com.baeldung.logging.app {
}
At this point, our project structure will look like this:
├── src
│ ├── modules
│ │ ├── com.baeldung.logging
│ │ │ ├── com
│ │ │ │ └── baeldung
│ │ │ │ └── logging
│ │ │ │ ├── ConsoleLogger.java
│ │ │ │ └── CustomLoggerFinder.java
│ │ │ └── module-info.java
│ │ ├── com.baeldung.logging.app
│ │ │ ├── com
│ │ │ │ └── baeldung
│ │ │ │ └── logging
│ │ │ │ └── app
│ │ │ │ └── MainApp.java
│ │ │ └── module-info.java
└──
Finally, we’re going to compile our two modules, and we’ll place them in a mods directory:
javac --module-path mods -d mods/com.baeldung.logging \
src/modules/com.baeldung.logging/module-info.java \
src/modules/com.baeldung.logging/com/baeldung/logging/*.java
javac --module-path mods -d mods/com.baeldung.logging.app \
src/modules/com.baeldung.logging.app/module-info.java \
src/modules/com.baeldung.logging.app/com/baeldung/logging/app/*.java
Finally, let’s run the Main class of the app module:
java --module-path mods \
-m com.baeldung.logging.app/com.baeldung.logging.app.MainApp
If we take a look at the console output we can see that our logs are printed using our ConsoleLogger:
ConsoleLogger [ERROR]: error test
ConsoleLogger [INFO]: info test
3. Adding an External Logging Framework
In our previous example, we were logging all our messages to the console, which is the same as what the default logger does. One of the most useful uses of the Logging API in Java 9 is to let applications route the JDK logs to the same logging framework the application is using, and that’s what we’re going to do in this section.
We’ll create a new module that uses SLF4J as logging facade and Logback as logging framework.
Since we’ve already explained the basics in the previous section, now we can focus on how to add an external logging framework.
3.1. Custom Implementations Using SLF4J
First, we’ll implement another Logger that will create a new SLF4J logger for each instance:
public class Slf4jLogger implements System.Logger {
private final String name;
private final Logger logger;
public Slf4jLogger(String name) {
this.name = name;
logger = LoggerFactory.getLogger(name);
}
@Override
public String getName() {
return name;
}
//...
}
Notice that this Logger is an org.slf4j.Logger.
For the rest of the methods, we’ll rely on the implementation on the SLF4J logger instance. Therefore, our Logger will be enabled if the SLF4J logger is enabled:
@Override
public boolean isLoggable(Level level) {
switch (level) {
case OFF:
return false;
case TRACE:
return logger.isTraceEnabled();
case DEBUG:
return logger.isDebugEnabled();
case INFO:
return logger.isInfoEnabled();
case WARNING:
return logger.isWarnEnabled();
case ERROR:
return logger.isErrorEnabled();
case ALL:
default:
return true;
}
}
And the log methods will call the appropriate SLF4J logger method depending on the log level used:
@Override
public void log(Level level, ResourceBundle bundle, String msg, Throwable thrown) {
if (!isLoggable(level)) {
return;
}
switch (level) {
case TRACE:
logger.trace(msg, thrown);
break;
case DEBUG:
logger.debug(msg, thrown);
break;
case INFO:
logger.info(msg, thrown);
break;
case WARNING:
logger.warn(msg, thrown);
break;
case ERROR:
logger.error(msg, thrown);
break;
case ALL:
default:
logger.info(msg, thrown);
}
}
@Override
public void log(Level level, ResourceBundle bundle, String format, Object... params) {
if (!isLoggable(level)) {
return;
}
String message = MessageFormat.format(format, params);
switch (level) {
case TRACE:
logger.trace(message);
break;
// ...
// same as the previous switch
}
}
Finally, let’s create a new LoggerFinder that uses our Slf4jLogger:
public class Slf4jLoggerFinder extends System.LoggerFinder {
@Override
public System.Logger getLogger(String name, Module module) {
return new Slf4jLogger(name);
}
}
3.2. Module Configuration
Once we have all our classes implemented, let’s register our service in our module and add the dependency of the SLF4J module:
module com.baeldung.logging.slf4j {
requires org.slf4j;
provides java.lang.System.LoggerFinder
with com.baeldung.logging.slf4j.Slf4jLoggerFinder;
exports com.baeldung.logging.slf4j;
}
This module will have the following structure:
├── src
│ ├── modules
│ │ ├── com.baeldung.logging.slf4j
│ │ │ ├── com
│ │ │ │ └── baeldung
│ │ │ │ └── logging
│ │ │ │ └── slf4j
│ │ │ │ ├── Slf4jLoggerFinder.java
│ │ │ │ └── Slf4jLogger.java
│ │ │ └── module-info.java
└──
Now we can compile this module into the mods directory as we did in the previous section.
Notice that we have to place the slf4j-api jar in the mods directory to compile this module. Also, keep in mind to use a modularized version of the library. The latest version can be found in Maven Central.
3.3. Adding Logback
We’re almost done, but we still need to add the Logback dependencies and configuration. To do so, place the logback-classic and logback-core jars in the mods directory.
As before, we have to make sure we’re using a modularized version of the library. Again, the latest version can be found in Maven Central.
Finally, let’s create a Logback configuration file and place it in our mods directory:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>
%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} -- %msg%n
</pattern>
</encoder>
</appender>
<root>
<appender-ref ref="STDOUT"/>
</root>
</configuration>
3.4. Running Our Application
At this point, we can run our app using our SLF4J module.
In this case, we also need to specify our Logback configuration file:
java --module-path mods \
-Dlogback.configurationFile=mods/logback.xml \
-m com.baeldung.logging.app/com.baeldung.logging.app.MainApp
Finally, if we check the output we can see that our logs are printed using our Logback configuration:
2018-08-25 14:02:40 [main] ERROR MainApp -- error test
2018-08-25 14:02:40 [main] INFO MainApp -- info test
4. Conclusion
We’ve shown in this article how to create a custom logger in Java 9 by using the new Platform Logging API. Also, we’ve implemented an example using an external logging framework, which is one of the most useful use cases of this new API.
As always, the full source code of the examples is available over on GitHub.