前言
最近在读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_PROVIDER
和SLF4J_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-API
,SLF4J-JUL
,LOG4J-API
,LOG4J-to-SLF4J
等等Spring常用的jar包都会默认引用进来,因此可以说默认情况下SpringBoot确实做了一个Log系统实现技术的切换。
附:关于Spring和Java日志背景知识点
【JAVA的日志体系的部分补缺】【Spring5的LOG系统和Spring5.2.8的LOG部分的更新】