当前位置: 首页 > 编程笔记 >

浅谈slf4j中的桥接器是如何运作的

宁锐
2023-03-14
本文向大家介绍浅谈slf4j中的桥接器是如何运作的,包括了浅谈slf4j中的桥接器是如何运作的的使用技巧和注意事项,需要的朋友参考一下

阅读分析 slf4j 的日志源码,发现其中涵盖了许多知识点和优秀的设计,关键它们都是活生生的实践案例。写成系列文章与大家分享,欢迎持续关注。

前言

在日志框架 slf4j 中有一组项目,除了核心的 slf4j-api 之外,还有 slf4j-log4j12、slf4j-jdk14 等项目。这一类项目统称桥接器项目,针对不同的日志框架有不同的桥接器项目。

在使用 logback 日志框架时,并没有针对的桥接器,这是因为 logback 与 slf4j 是一个作者所写,在 logback 中直接实现了 slf4j 的 SPI 机制。

但如果使用其他日志框架,那么就必须要用到桥机器相关依赖。比如,当我们基于 log4j 使用 slf4j 时,除了需要引入 log4j 的 jar 包依赖,还需要引入 slf4j 的下面两个依赖:

<dependency>
 <groupId>org.slf4j</groupId>
 <artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
 <groupId>org.slf4j</groupId>
 <artifactId>slf4j-log4j12</artifactId>
</dependency>

slf4j-api 为核心依赖,必须引入,而 slf4j-log4j12 就是桥接器用来在 slf4j 和 log4j 之间进行过渡和封装。下面,我们就聊聊桥接器项目的核心实现。

slf4j-log4j12 桥接器的价值

要了解桥接器的运作,首先需要回顾一下 slf4j 的 SPI 机制。在我们通过 LoggerFactory.getLogger(Foo.class); 时,slf4j 会通过 SPI 机制寻找并初始化 SLF4JServiceProvider 的实现类。

然后,通过 SLF4JServiceProvider 的实现类来获取日志相关的具体工厂类对象,进而进行日志功能的处理。先来看一下 SLF4JServiceProvider 的接口定义:

public interface SLF4JServiceProvider {

  /**
   * 返回ILoggerFactory的实现类,用于LoggerFactory类的绑定
   */
  ILoggerFactory getLoggerFactory();

  /**
   * 返回IMarkerFactory实例
   */
  IMarkerFactory getMarkerFactory();

  /**
   * 返回MDCAdapter实例
   */
  MDCAdapter getMDCAdapter();

  /**
   * 获取请求版本
   */
  String getRequesteApiVersion();

  /**
   * 初始化,实现类中一般用于初始化ILoggerFactory等
   */
  void initialize();
}

SLF4JServiceProvider 接口是在 slf4j-api 中定义的,具体的实现类由其他日志框架来完成。但是像 log4j(logback“敌对阵营”)是不会在框架内实现该接口的。那么,怎么办?

针对此问题,slf4j 提供了 slf4j-log4j12 这类桥接器的过渡项目。在其中实现 SLF4JServiceProvider 接口,并对 Log4j 日志框架接口进行封装,将 Logger(slf4j) 接收到的命令全部委托给 Logger(log4j) 去完成,在使用者无感知的情况下完成偷天换日。

slf4j-log4j12 的核心实现类

理解了桥接器的存在价值及原理,下面就来看看 slf4j-log4j12 是如何实现这一功能的。

首先来看看核心实现类之一 Log4j12ServiceProvider。它实现了 SLF4JServiceProvider 接口,主要功能就是完成接口中定义的相关工厂接口的实现。源代码如下:

public class Log4j12ServiceProvider implements SLF4JServiceProvider {

  public static String REQUESTED_API_VERSION = "1.8.99"; 

  private ILoggerFactory loggerFactory; 
  private IMarkerFactory markerFactory; 
  private MDCAdapter mdcAdapter;
  
  public Log4j12ServiceProvider() {
    try {
      @SuppressWarnings("unused")
      Level level = Level.TRACE;
    } catch (NoSuchFieldError nsfe) {
      Util.report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version");
    }
  }

  @Override
  public void initialize() {
    loggerFactory = new Log4jLoggerFactory();
    markerFactory = new BasicMarkerFactory();
    mdcAdapter = new Log4jMDCAdapter();
  }
  
  @Override
  public ILoggerFactory getLoggerFactory() {
    return loggerFactory;
  }

  @Override
  public IMarkerFactory getMarkerFactory() {
    return markerFactory;
  }

  @Override
  public MDCAdapter getMDCAdapter() {
    return mdcAdapter;
  }

  @Override
  public String getRequesteApiVersion() {
    return REQUESTED_API_VERSION;
  }
}



该类的实现看起来很简单,构造方法中通过尝试使用 log4j 的 Level.TRACE 调用来验证 log4j 的版本是否符合要求。log4j1.2.12 之前并没有 Level.TRACE,所以会抛出异常,并打印日志信息。不得不赞叹作者在此处检查版本的巧妙用法。

而这里对接口中返回的实现类主要通过 initialize() 方法来实现的。这里我们重点看 Log4jLoggerFactory 类的实现。

public class Log4jLoggerFactory implements ILoggerFactory {

  private static final String LOG4J_DELEGATION_LOOP_URL = "http://www.slf4j.org/codes.html#log4jDelegationLoop";

  // check for delegation loops
  static {
    try {
      Class.forName("org.apache.log4j.Log4jLoggerFactory");
      String part1 = "Detected both log4j-over-slf4j.jar AND bound slf4j-log4j12.jar on the class path, preempting StackOverflowError. ";
      String part2 = "See also " + LOG4J_DELEGATION_LOOP_URL + " for more details.";

      Util.report(part1);
      Util.report(part2);
      throw new IllegalStateException(part1 + part2);
    } catch (ClassNotFoundException e) {
      // this is the good case
    }
  }

  ConcurrentMap<String, Logger> loggerMap;

  public Log4jLoggerFactory() {
    loggerMap = new ConcurrentHashMap<>();
    // force log4j to initialize
    org.apache.log4j.LogManager.getRootLogger();
  }

  @Override
  public Logger getLogger(String name) {
    Logger slf4jLogger = loggerMap.get(name);
    if(slf4jLogger != null) {
      return slf4jLogger;
    } else {
      org.apache.log4j.Logger log4jLogger;
      if(name.equalsIgnoreCase(Logger.ROOT_LOGGER_NAME)) {
        log4jLogger = LogManager.getRootLogger();
      } else {
        log4jLogger = LogManager.getLogger(name);
      }

      Logger newInstance = new Log4jLoggerAdapter(log4jLogger);
      Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
      return oldInstance == null ? newInstance : oldInstance;
    }
  }
}

在 Log4j12ServiceProvider 中进行了 Log4jLoggerFactory 的实例化操作,也就直接 new 出来一个对象。我们知道,在 new 对象执行会先执行 static 代码块,本类的静态代码块的核心工作就是检查依赖文件中是否同时存在反向桥接器的依赖。

其中,org.apache.log4j.Log4jLoggerFactory 是反向桥接器 log4j-over-slf4j 项目中的类,如果加装到了,说明存在,则抛出异常,打印日志信息。此处再次赞叹作者运用的技巧的巧妙。

在 Log4jLoggerFactory 的构造方法中,做了两件事:第一,初始化一个 ConcurrentMap 变量,用于存储实例化的 Logger;第二,强制初始化 log4j 的组件,其中强制初始化 log4j 的组件是通过 getRootLogger 方法,来初始化一些静态的变量。

构造方法时初始化了 ConcurrentMap 变量,在 Log4jLoggerFactory 实现的 getLogger 方法中,先从 Map 中获取一下是否存在对应的 Logger,如果存在直接返回,如果不存在则进行构造。而构造的 Log4jLoggerAdapter 类很显然使用了适配器模式,它内部持有了 log4j 的 Logger 对象,自身又实现了 slf4j 的 Logger 接口。

下面看一下 Log4jLoggerAdapter 的部分代码实现:

public final class Log4jLoggerAdapter extends LegacyAbstractLogger implements LocationAwareLogger, Serializable {

  final transient org.apache.log4j.Logger logger;

  Log4jLoggerAdapter(org.apache.log4j.Logger logger) {
    this.logger = logger;
    this.name = logger.getName();
    traceCapable = isTraceCapable();
  }

  @Override
  public boolean isDebugEnabled() {
    return logger.isDebugEnabled();
  }


  @Override
  public void log(Marker marker, String callerFQCN, int level, String msg, Object[] arguments, Throwable t) {
    Level log4jLevel = toLog4jLevel(level);
    NormalizedParameters np = NormalizedParameters.normalize(msg, arguments, t);
    String formattedMessage = MessageFormatter.basicArrayFormat(np.getMessage(), np.getArguments());
    logger.log(callerFQCN, log4jLevel, formattedMessage, np.getThrowable());
  }

  public void log(LoggingEvent event) {
    Level log4jLevel = toLog4jLevel(event.getLevel().toInt());
    if (!logger.isEnabledFor(log4jLevel))
      return;

    org.apache.log4j.spi.LoggingEvent log4jevent = toLog4jEvent(event, log4jLevel);
    logger.callAppenders(log4jevent);

  }
  
  // 省略其他方法
}

源码中,通过构造方法传入 log4j 的 Logger 对象,而 Log4jLoggerAdapter 对外提供的方法,都是通过 log4j 的 Logger 进行具体实现。

总之,slf4j 的 Logger 接口的方法通过 Log4jLoggerAdapter 进行包装和转换,交由 log4j 的 Logger 去执行,这就达到了连接 slf4j-api 和 log4j 的目的。而此时,slf4j-api 不并关系日志是如何实现记录,对此也无感知。

小结

本文通过源码跟踪,逐步分析了 slf4j 项目中桥接器项目的运作机制,其中还涉及到了 SPI 机制、版本及依赖检查小技巧、桥接器运作本质(适配器模式)等。其实,在 slf4j 项目中还有文中提到的反向桥接器,其实基本机制也是如此,感兴趣的朋友可以阅读一下 log4j-over-slf4j 中的源码

到此这篇关于浅谈slf4j中的桥接器是如何运作的 的文章就介绍到这了,更多相关slf4j 桥接器内容请搜索小牛知识库以前的文章或继续浏览下面的相关文章希望大家以后多多支持小牛知识库!

 类似资料:
  • 本文向大家介绍浅谈PHP中的<<<运算符,包括了浅谈PHP中的<<<运算符的使用技巧和注意事项,需要的朋友参考一下 PHP中提供了<<<运算符构建多行字符串序列的方法,通常称为here-document或表示为heredoc的简写。 这种方法详细表述了字符串的字面值,并在文本中保留了字符串的下划线和其他空格(包括缩进)。例如: PHP解析器会将两个_END标签中间的所有内容输出,就相当于一个用双引

  • 本文向大家介绍浅谈Webpack 是如何加载模块的,包括了浅谈Webpack 是如何加载模块的的使用技巧和注意事项,需要的朋友参考一下 Webpack 在前端开发中作为模块打包工具非常受开发者的青睐,丰富的 loader 使它可以实现各种各样的功能。本文将通过 webpack 来打包一个 js 文件,看看 webpack 是如何加载各个模块的。 两个简单的源文件 为了方便分析 webpack 加载

  • 本文向大家介绍浅谈SpringBoot是如何实现日志的,包括了浅谈SpringBoot是如何实现日志的的使用技巧和注意事项,需要的朋友参考一下 前言 休息日闲着无聊看了下 SpringBoot 中的日志实现,把我的理解跟大家说下。 门面模式 说到日志框架不得不说门面模式。门面模式,其核心为外部与一个子系统的通信必须通过一个统一的外观对象进行,使得子系统更易于使用。用一张图来表示门面模式的结构为:

  • 本文向大家介绍浅谈Node.js中的定时器,包括了浅谈Node.js中的定时器的使用技巧和注意事项,需要的朋友参考一下 Node.js中定时器的实现 上一篇博文提到,在Node中timer并不是通过新开线程来实现的,而是直接在event loop中完成。下面通过几个JavaScript的定时器示例以及Node相关源码来分析在Node中,timer功能到底是怎么实现的。 JavaScript中定时器

  • 问题内容: 我目前正在观察一个第三方库(即restfb)正在使用java.util.logging,即使我的logback.xml中没有配置SLF4J控制台附加程序,我也看到这些日志最终出现在STDOUT中。我的类路径中还有jul- slf4j 桥。安装网桥时,jul-to- slf4j网桥是否仅登录到由logback配置的附加程序,还是也登录到stdout? 问题答案: 您需要致电。您还需要在j

  • 本文向大家介绍浅谈Javascript如何实现匀速运动,包括了浅谈Javascript如何实现匀速运动的使用技巧和注意事项,需要的朋友参考一下 网站上会经常用到Javascript 中的运动,这次与大家分享一下一些运动的基本应用 。 方便大家在开发中能够直接使用。 代码简单易懂,适用于初学者。最后会一步一步整理出一套自己的运动框架。 应用案例效果图: 鼠标移动到分享上 ,左边的div就会显示出来。