© Fu Cheng 2018

Fu Cheng, Exploring Java 9, https://doi.org/10.1007/978-1-4842-3330-6_6

6. The Platform Logging API and Service

Fu Cheng

(1)Auckland, New Zealand

Logging is an important aspect of application development. Many logging frameworks are available for use in the Java platform, including popular choices like Apache Log4j 2 ( https://logging.apache.org/log4j/2.x/ ) and Logback ( https://logback.qos.ch/ ). SLF4J is commonly used as the facade of different logging implementations. Java also has its own logging implementation, the java.util.logging API, which was added in Java 1.4. Even though java.util.logging is the built-in logging solution used by the Java standard library, it’s not very popular. Most Java applications still use external logging frameworks. The JDK itself and the applications running on top of it may use different logging frameworks. If an application error is actually related to the Java standard library, even though such an error is unlikely, then two separate logging frameworks will make the error diagnostics much harder. In situations in which you are using two separate logging frameworks, you will also need to configure both logging frameworks, and their configurations are likely to have duplications.

In Java 9, it’s now possible to use the same logging framework for both the JDK and the application by using the class java.lang.System.LoggerFinder. LoggerFinder is responsible for managing loggers used by the JDK. The JVM has a single system-wide LoggerFinder instance that is loaded using the service provider mechanism with ServiceLoader. If no LoggerFinder provider is found, the default LoggerFinder implementation will use java.util.logging as the logging framework when the module java.logging is present. If the module java.logging is not present, then the default LoggerFinder implementation outputs log messages to the console using System.err.

The static method getLoggerFinder() of LoggerFinder returns the single LoggerFinder instance in the JVM. Subclasses of LoggerFinder need to implement the abstract method System.Logger getLogger(String name, Module module), which returns the logger instance of interface java.lang.System.Logger for a given module. System. Logger has different methods for logging messages of different levels. LoggerFinder also has the method System.Logger getLocalizedLogger(String name, ResourceBundle bundle, Module module) to return a localizable logger instance for a given module.

System.Logger is the primary class used by applications to log messages. Table 6-1 shows methods of System.Logger.

Table 6-1. Methods of System.Logger

Method

Description

String getName()

Returns the name of this logger.

boolean isLoggable(System.Logger.Level level)

Checks if a log message of the given level would be logged by this logger.

void log(System.Logger.Level level, String msg)

Logs a message of the given level.

void log(System.Logger.Level level, Supplier<String> msgSupplier)

Logs a message of the given level that is produced by the given supplier function. The supplier function is only called when the message will be logged.

void log(System.Logger.Level level, Object obj)

Logs a message of the given level by calling the object’s toString() method.

void log(System.Logger.Level level, String msg, Throwable thrown)

Logs a message of the given level with a given throwable.

void log(System.Logger.Level level, Supplier<String> msgSupplier, Throwable thrown)

Logs a message of the given level that is produced by the given supplier function with a given throwable.

void log(System.Logger.Level level, String format, Object… params)

Logs a message generated from the format and an optional list of parameters. The message uses the format specified in MessageFormat.

void log(System.Logger.Level level, ResourceBundle bundle, String msg, Throwable thrown)

Logs a localized message of the given level with a given throwable.

void log(System.Logger.Level level, ResourceBundle bundle, String format, Object… params)

Logs a localized message generated from the format and an optional list of parameters.

System.Logger defines its own enum for logging levels in System.Logger.Level. These levels can be easily mapped to logging levels in other logging frameworks. These logging levels are ALL, TRACE, DEBUG, INFO, WARNING, ERROR, and OFF.

Default LoggerFinder Implementation

Let’s first take a closer look at the default LoggerFinder implementation.

The class jdk.internal.logger.LoggerFinderLoader in the module java.base is responsible for loading the LoggerFinder implementation. It first tries to use ServiceLoader to load the implementation of LoggerFinder. If no provider exists, it uses the ServiceLoader again to load an implementation of the class jdk.internal.logger.DefaultLoggerFinder. If it still cannot find any providers, it uses a new instance of DefaultLoggerFinder. DefaultLoggerFinder itself is a logging implementation that uses the class jdk.internal.logger.SimpleConsoleLogger to write log messages to the console using System.err.

The class sun.util.logging.internal.LoggingProviderImpl in the module java. logging is an implementation of DefaultLoggerFinder that delegates the logging requests to the underlying loggers created from java.util.logging; see the module declaration of java.logging in Listing 6-1.

Listing 6-1. Module Declaration of java.logging
module java.logging {
  exports java.util.logging;


  provides jdk.internal.logger.DefaultLoggerFinder with
      sun.util.logging.internal.LoggingProviderImpl;
}

Creating Custom LoggerFinder Implementations

Now you can create your own LoggerFinder implementations. Since LoggerFinder can change the system’s default logging implementation, when a SecurityManager is installed, the implementation of LoggerFinder should check the permission loggerFinder first.

Here I am going to use SLF4J with Logback as the logging framework to demonstrate how to create a custom LoggerFinder. In Listing 6-2, the class SLF4JLoggerFinder is the LoggerFinder implementation. The method checkPermission() is used to check the required permission. In the method getLogger(), I use the SLF4J method LoggerFactory.getLogger() to create a new org.slf4j.Logger instance. The logger name is created from the module name and the provided name. The return value of getLogger() is an instance of the class SLF4JLoggerWrapper, which is shown in Listing 6-3.

Listing 6-2. SLF4JLoggerFinder
public class SLF4JLoggerFinder extends System.LoggerFinder {

  private static final RuntimePermission LOGGERFINDER_PERMISSION =
      new RuntimePermission("loggerFinder");


  public SLF4JLoggerFinder() {
    this.checkPermission();
  }


  private void checkPermission() {
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      sm.checkPermission(LOGGERFINDER_PERMISSION);
    }
  }


  @Override
  public System.Logger getLogger(final String name, final Module module) {
    checkPermission();
    final Logger logger = LoggerFactory
        .getLogger(String.format("%s/%s", module.getName(), name));
    return new SLF4JLoggerWrapper(logger);
  }
}

The class SLF4JLoggerWrapper implements the interface System.Logger. In the method isLoggable(), it delegates to different methods in the wrapped org.slf4j.Logger instance the task of checking whether the logging level should be logged. For example, if the level is DEBUG, it delegates to the method isDebugEnabled(). The logging methods in org.slf4j.Logger—for example, trace() and debug()—accept different parameters than the method log() in System.Logger, so I need to create adapters for localization and message format. The method localizedMessage() returns the localized message retrieved from the ResourceBundle. The method doLog() only takes plain messages and delegates to different logging methods in org.slf4j.Logger based on the level. In the method log() of SLF4JLoggerWrapper, the plain message is prepared first and passed to doLog() for logging.

Listing 6-3. SLF4JLoggerWrapper
public class SLF4JLoggerWrapper implements System.Logger {

  private final Logger logger;

  public SLF4JLoggerWrapper(final Logger logger) {
    this.logger = logger;
  }


  @Override
  public String getName() {
    return this.logger.getName();
  }


  @Override
  public boolean isLoggable(final Level level) {
    switch (level) {
      case ALL:
      case TRACE:
        return this.logger.isTraceEnabled();
      case DEBUG:
        return this.logger.isDebugEnabled();
      case INFO:
        return this.logger.isInfoEnabled();
      case WARNING:
        return this.logger.isWarnEnabled();
      case ERROR:
        return this.logger.isErrorEnabled();
      default:
        return false;
    }
  }


  @Override
  public void log(final Level level, final ResourceBundle bundle,
      final String msg, final Throwable thrown) {
    this.doLog(level, localizedMessage(bundle, msg), thrown);
  }


  @Override
  public void log(final Level level, final ResourceBundle bundle,
      final String format, final Object... params) {
    this.doLog(level,
        String.format(localizedMessage(bundle, format), params),
        null);
  }


  private void doLog(final Level level,
      final String msg,
      final Throwable thrown) {
    switch (level) {
      case ALL:
      case TRACE:
        this.logger.trace(msg, thrown);
        break;
      case DEBUG:
        this.logger.debug(msg, thrown);
        break;
      case INFO:
        this.logger.info(msg, thrown);
        break;
      case WARNING:
        this.logger.warn(msg, thrown);
        break;
      case ERROR:
        this.logger.error(msg, thrown);
        break;
    }
  }


  private String localizedMessage(final ResourceBundle resourceBundle,
      final String msg) {
    if (resourceBundle != null) {
      try {
        return resourceBundle.getString(msg);
      } catch (final MissingResourceException e) {
        return msg;
      }
    }
    return msg;
  }
}

The file module-info. javaof this module in Listing 6-4 uses provides to declare the provider of System.LoggerFinder.

Listing 6-4. Module Declaration
module feature.logging {
    requires transitive slf4j.api;
    provides java.lang.System.LoggerFinder
        with io.vividcode.feature9.logging.SLF4JLoggerFinder;
}

Now I can use the logging service to log messages. I create a logger using the method System.getLogger(), then use the method log() to log messages. Here I use some Java standard classes to see how both application and platform logs are combined.

Listing 6-5. Using the Logging Service
public class Main {

  private static final System.Logger LOGGER = System.getLogger("Main");

  public static void main(final String[] args) {
    LOGGER.log(Level.INFO, "Run!");
    try {
      new URL("https://google.com").openConnection().connect();
    } catch (final IOException e) {
      LOGGER.log(Level.WARNING, "Failed to connect", e);
    }
  }
}

Listing 6-6 shows the logs. In the output, you can see both application logs and JDK logs. Here I configured Logback to log out all messages.

Listing 6-6. Log Messages
2017-09-20 21:23:56,202 |  INFO [      main] feature.runtime/Main : Run!
2017-09-20 21:23:56,448 | TRACE [      main] .h.HttpURLConnection : ProxySelector Request for https://google.com/
2017-09-20 21:23:56,450 | TRACE [      main] .h.HttpURLConnection : Looking for HttpClient for URL https://google.com and proxy value of DIRECT
2017-09-20 21:23:56,451 | TRACE [      main] .h.HttpURLConnection : Creating new HttpsClient with url:https://google.com and proxy:DIRECT with connect timeout:-1
2017-09-20 21:23:56,516 | TRACE [      main] .h.HttpURLConnection : Proxy used: DIRECT

Summary

With the new platform logging API and service, you can now use a single logging implementation for both the JDK itself and applications. This makes the error diagnostics much easier. In next chapter, we’ll discuss reactive streams.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.58.116.51