Application of adapter pattern in Java log

Posted by martins on Tue, 01 Feb 2022 01:07:17 +0100

There are many logging frameworks in Java. In project development, we often use them to print log information. Among them, log4j, logback, JUL(java.util.logging) provided by JDK and JCL(Jakarta Commons Logging) of Apache are commonly used.

Most logging frameworks provide similar functions, such as printing logs at different levels (debug, info, warn, erro...), but they do not implement a unified interface. This may be mainly due to historical reasons. Unlike JDBC, it formulated the interface specification of database operation at the beginning.

If we only develop a project for our own use, we can use any logging framework. Just choose one of log4j and logback. However, if we develop a component, framework and class library integrated into other systems, the choice of logging framework will not be so random.

For example, a component used in the project uses log4j to print logs, while our project itself uses logback. After introducing components into the project, our project has two sets of log printing frameworks. Each logging framework has its own unique configuration mode. Therefore, we need to write different configuration files for each log framework (for example, the file address of log storage and the format of log printing). If multiple components are introduced and the log framework used by each component is different, the management of the log itself becomes very complex. Therefore, in order to solve this problem, we need to unify the log printing framework.

If you are developing in Java, Slf4j is no stranger to you. It is equivalent to JDBC specification and provides a set of unified interface specification for printing logs. However, it only defines the interface and does not provide a specific implementation. It needs to be used in conjunction with other logging frameworks (log4j, logback...)

Moreover, slf4j appeared later than JUL, JCL, log4j and other logging frameworks. Therefore, these logging frameworks can not sacrifice version compatibility and transform the interface to comply with slf4j interface specification. Slf4j also considers this problem in advance, so it not only provides a unified interface definition, but also provides adapters for different logging frameworks. The interfaces of different log frameworks are encapsulated twice and adapted into a unified slf4j interface definition. Specific code examples are as follows:

// slf4j unified interface definition
package org.slf4j;
public interface Logger {
  public boolean isTraceEnabled();
  public void trace(String msg);
  public void trace(String format, Object arg);
  public void trace(String format, Object arg1, Object arg2);
  public void trace(String format, Object[] argArray);
  public void trace(String msg, Throwable t);
 
  public boolean isDebugEnabled();
  public void debug(String msg);
  public void debug(String format, Object arg);
  public void debug(String format, Object arg1, Object arg2)
  public void debug(String format, Object[] argArray)
  public void debug(String msg, Throwable t);

  //... Omit a bunch of interfaces such as info, warn and error
}

// Adapter for log4j logging framework
// The Log4jLoggerAdapter implements the LocationAwareLogger interface,
// LocationAwareLogger inherits from the Logger interface,
// This is equivalent to the Log4jLoggerAdapter implementing the Logger interface.
package org.slf4j.impl;
public final class Log4jLoggerAdapter extends MarkerIgnoringBase
  implements LocationAwareLogger, Serializable {
  final transient org.apache.log4j.Logger logger; // log4j
 
  public boolean isDebugEnabled() {
    return logger.isDebugEnabled();
  }
 
  public void debug(String msg) {
    logger.log(FQCN, Level.DEBUG, msg, null);
  }
 
  public void debug(String format, Object arg) {
    if (logger.isDebugEnabled()) {
      FormattingTuple ft = MessageFormatter.format(format, arg);
      logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
    }
  }
 
  public void debug(String format, Object arg1, Object arg2) {
    if (logger.isDebugEnabled()) {
      FormattingTuple ft = MessageFormatter.format(format, arg1, arg2);
      logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
    }
  }
 
  public void debug(String format, Object[] argArray) {
    if (logger.isDebugEnabled()) {
      FormattingTuple ft = MessageFormatter.arrayFormat(format, argArray);
      logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
    }
  }
 
  public void debug(String msg, Throwable t) {
    logger.log(FQCN, Level.DEBUG, msg, t);
  }
  //... Omit the implementation of a bunch of interfaces
}

Therefore, when developing business systems or frameworks and components, we uniformly use the interface provided by Slf4j to write the code for printing logs. The specific log framework implementation (log4j, logback...) can be dynamically specified (using Java SPI technology, I won't explain more here, but you can study it by yourself), Just import the corresponding SDK into the project.

However, you might say that if some old projects do not use Slf4j, but directly use JCL to print logs, what should we do if we want to replace it with other logging frameworks, such as log4j? In fact, Slf4j not only provides adapters from other logging frameworks to Slf4j, but also provides reverse adapters, that is, adaptation from Slf4j to other logging frameworks. We can first switch JCL to Slf4j, and then switch Slf4j to log4j. After two adapter conversions, we can successfully switch log4j to logback.

summary

  • Adapter mode is used to improve compatibility

reflection

Adapters can be implemented in two ways: class adapters and object adapters. Can the agent mode and decorator mode we mentioned earlier also be implemented in two ways (class agent mode, object agent mode, class decorator mode and object decorator mode)?

reference resources

51 | adapter mode: agent, adapter, bridge and decoration. What is the difference between these four modes?

Topics: Design Pattern slf4j