前言

最近在读SpringBoot源码的时候发现了一个变化,根据博主以往的经验来说,Spring系列用Log4j 2.x作为日志记录的工具是板上钉钉的事情。但是在跟踪调试最新版的SpringBoot的时候,却发现其日记记录系统变成了SLF4J,追踪一下版本发现早在SpringBoot 2.1.18.RELEASE就已经做了修改,真实三日不见当刮目相看,更新的太快了。那么本篇就从源码里面看下新版本到底和旧版本有什么区别。这里版本的对比是2.1.1.RELEASE和2.3.5.RELEASE最新版本的对比。更多Spring内容进入【Spring解读系列目录】。

2.1.1.RELEASE版本

首先看下老版本是怎么做Log的,随便找到一个记录log的位置。比如就从Tomcat的log里找到TomcatWebServer#logger属性。

private static final Log logger = LogFactory.getLog(TomcatWebServer.class);
一层一层的点进去LogFactory#getLog(java.lang.Class<?>)。
public static Log getLog(Class<?> clazz) {
   return getLog(clazz.getName());
}

继续进入LogFactory#getLog(java.lang.String)

public static Log getLog(String name) {
   return LogAdapter.createLog(name);
}

最终就到了创建对象的方法LogAdapter#createLog()

public static Log createLog(String name) {
   switch (logApi) {
      case LOG4J:
         return Log4jAdapter.createLog(name);
      case SLF4J_LAL:
         return Slf4jAdapter.createLocationAwareLog(name);
      case SLF4J:
         return Slf4jAdapter.createLog(name);
      default:
         // Defensively use lazy-initializing adapter class here as well since the
         // java.logging module is not present by default on JDK 9. We are requiring
         // its presence if neither Log4j nor SLF4J is available; however, in the
         // case of Log4j or SLF4J, we are trying to prevent early initialization
         // of the JavaUtilLog adapter - e.g. by a JVM in debug mode - when eagerly
         // trying to parse the bytecode for all the cases of this switch clause.
         return JavaUtilAdapter.createLog(name);
   }
}

其实这里看的出来基本上创建Log对象是根据switch语句判断logApi字段内容做的,它的默认的log系统依然是JUL。但是要想知道switch走的分支,就得找到logApi在哪里初始化的,位置还在本类里面的一个static块里。

static {
   ClassLoader cl = LogAdapter.class.getClassLoader();
   try {
      // Try Log4j 2.x API
      Class.forName("org.apache.logging.log4j.spi.ExtendedLogger", false, cl);
      logApi = LogApi.LOG4J;
   }
   catch (ClassNotFoundException ex1) {
      try {
         // Try SLF4J 1.7 SPI
         Class.forName("org.slf4j.spi.LocationAwareLogger", false, cl);
         logApi = LogApi.SLF4J_LAL;
      }
      catch (ClassNotFoundException ex2) {
         try {
            // Try SLF4J 1.7 API
            Class.forName("org.slf4j.Logger", false, cl);
            logApi = LogApi.SLF4J;
         }
         catch (ClassNotFoundException ex3) {
            // Keep java.util.logging as default
         }
      }
   }
}

在这里可以很明显的看到,首先SpringBoot去找LOG4J的包是不是存在,如果存在就用LOG4J,如果不存在报异常在catch块里尝试去找SLF4J_LAL,如果还是找不到就使用SLF4J。如果是还是被捕获异常了logApi没有被赋值那就走default使用JUL做log记录。

2.3.5.RELEASE版本

最新版本中我们去同样的位置,也是一步一步的往里面进入。

private static final Log logger = LogFactory.getLog(TomcatWebServer.class);
LogFactory#getLog(java.lang.Class<?>)。
public static Log getLog(Class<?> clazz) {
    return getLog(clazz.getName());
}

LogFactory#getLog(java.lang.String)。

public static Log getLog(String name) {
   return LogAdapter.createLog(name);
}

LogAdapter#createLog();

public static Log createLog(String name) {
   switch (logApi) {
      case LOG4J:
         return Log4jAdapter.createLog(name);
      case SLF4J_LAL:
         return Slf4jAdapter.createLocationAwareLog(name);
      case SLF4J:
         return Slf4jAdapter.createLog(name);
      default:
         // Defensively use lazy-initializing adapter class here as well since the
         // java.logging module is not present by default on JDK 9. We are requiring
         // its presence if neither Log4j nor SLF4J is available; however, in the
         // case of Log4j or SLF4J, we are trying to prevent early initialization
         // of the JavaUtilLog adapter - e.g. by a JVM in debug mode - when eagerly
         // trying to parse the bytecode for all the cases of this switch clause.
         return JavaUtilAdapter.createLog(name);
   }
}

一路走下来发现代码是一模一样的,那么问题就一定出现在logApi生成的时候,那么去static块里看下新版本是怎么更新的。

新版本的更新

private static final String LOG4J_SPI = "org.apache.logging.log4j.spi.ExtendedLogger";

private static final String LOG4J_SLF4J_PROVIDER = "org.apache.logging.slf4j.SLF4JProvider";

private static final String SLF4J_SPI = "org.slf4j.spi.LocationAwareLogger";

private static final String SLF4J_API = "org.slf4j.Logger";

private static final LogApi logApi;

static {
   if (isPresent(LOG4J_SPI)) {
      if (isPresent(LOG4J_SLF4J_PROVIDER) && isPresent(SLF4J_SPI)) {
         // log4j-to-slf4j bridge -> we'll rather go with the SLF4J SPI;
         // however, we still prefer Log4j over the plain SLF4J API since
         // the latter does not have location awareness support.
         logApi = LogApi.SLF4J_LAL;
      }
      else {
         // Use Log4j 2.x directly, including location awareness support
         logApi = LogApi.LOG4J;
      }
   }
   else if (isPresent(SLF4J_SPI)) {
      // Full SLF4J SPI including location awareness support
      logApi = LogApi.SLF4J_LAL;
   }
   else if (isPresent(SLF4J_API)) {
      // Minimal SLF4J API without location awareness support
      logApi = LogApi.SLF4J;
   }
   else {
      // java.util.logging as default
      logApi = LogApi.JUL;
   }
}

很明显代码变更了很多,首先包路径被抽取出来了,然后增加了一个isPresent(LOG4J_SPI)做逻辑判断,取消了以前是用的try-catch。这一套下来,看起来代码确实更舒服了。那么我们看下isPresent(LOG4J_SPI)是什么内容。

private static boolean isPresent(String className) {
   try {
      Class.forName(className, false, LogAdapter.class.getClassLoader());
      return true;
   }
   catch (ClassNotFoundException ex) {
      return false;
   }
}

进入以后可以看到这里仍然是用try-catch进行的查找,如果发现了传递的进来的类,那么就返回true,如果没有发现就捕获异常返回false。那么在外面如果是false就跳过这个logApi赋值,去下一个分支。组合起来提高了代码的复用率这点很值得学习。

返回static块,可以看到如果发现LOG4J_SPI存在,那么就继续判断LOG4J_SLF4J_PROVIDERSLF4J_SPI这两个包是否存在。如果存在那么就用SLF4J_LAL,否则就用LOG4J。但是Spring官方在这里却说we still prefer Log4j over the plain SLF4J API since the latter does not have location awareness support. 既然目前还是更倾向于用Log4J,那么对于为何要更换默认实施确实很费解。因为只要引入spring-boot-starter或者spring-boot-starter-web就会把所有支持的Log包下载下来,像SLF4J-APISLF4J-JULLOG4J-APILOG4J-to-SLF4J等等Spring常用的jar包都会默认引用进来,因此可以说默认情况下SpringBoot确实做了一个Log系统实现技术的切换。

附:关于Spring和Java日志背景知识点

【JAVA的日志体系的部分补缺】【Spring5的LOG系统和Spring5.2.8的LOG部分的更新】