本文简介

  1. 第一部分,介绍spring-jcl适配各种日志框架的方式
  2. 第二部分,介绍slf4j适配各种日志框架的方式
  3. 第三部分,介绍下logback框架的使用及原理

 

一、spring-jcl分析

说明

Spring5.x开始自己实现了日志框架的适配器,就叫spring-jcl模块 ,该模块是对输出的日志框架进行适配,是从Apache的commons-logging改造而来。spring-jcl默认绑定的日志框架是JUL(全称Java util Logging,是java原生的日志框架)。

准备

本文依赖的Spring版本号:5.2.9.RELEASE(Spring5.X版本源码基本一致)

搜索打开LogAdapter类,maven依赖org.springframework:spring-jcl(如下图所示)

 

源码分析

LogAdapter类

LogAdapter类定义的常量如下,看一眼就意识到,这是根据类的全限定名来查找各个日志框架的。

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";

 

直接看下LogAdapter类的静态代码块,保留原有的英文注释,我加上了中文的逻辑说明,这段代码完整展示了spring-jcl日志框架的加载顺序

static {
   // 优先log4j
   if (isPresent(LOG4J_SPI)) {
      // 如果存在log4j-to-slf4j和slf4j,优先选择slf4j
      //(这里slf4j最终会绑定log4j-to-slf4j,本质上使用的还是log4j)
      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;
      }
      // 否则直接使用log4j
      else {
         // Use Log4j 2.x directly, including location awareness support
         logApi = LogApi.LOG4J;
      }
   }
   // 存在Full SLF4J SPI
   else if (isPresent(SLF4J_SPI)) {
      // Full SLF4J SPI including location awareness support
      logApi = LogApi.SLF4J_LAL;
   }
   // 存在Minimal SLF4J API
   else if (isPresent(SLF4J_API)) {
      // Minimal SLF4J API without location awareness support
      logApi = LogApi.SLF4J;
   }
   // 没有导入日志框架,默认使用JUL
   else {
      // java.util.logging as default
      logApi = LogApi.JUL;
   }
}

 

其中isPresent()方法源码如下,就是通过类加载器加载对应的日志框架类,返回是否导入了对应的日志框架。

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

 

LogAdapter是怎么被调用的呢?

顺着日志框架的使用方式即可看到,我依次贴下代码

// spring-jcl使用标准代码
import org.apache.commons.logging.LogFactory;
LogFactory.getLog(AdvanceEverydayApplication.class).info("hello world!");
// 截取LogFactory的getlog方法,
// 可以看到,最终就是调用了LogAdapter的createLog方法
public abstract class LogFactory {

   public static Log getLog(Class<?> clazz) {
      return getLog(clazz.getName());
   }
   
   public static Log getLog(String name) {
       return LogAdapter.createLog(name);
    }
}
// 截取LogAdapter的createLog方法
// 可以看到,这里就是根据logApi这个属性的值,调用不同框架的适配器,分别创建不同框架的实例
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就是最开始的那个静态代码块,确定导入了哪个日志框架

总结

spring-jcl是Spring自己的日志框架适配器,在什么依赖都未导入的情况下,默认使用JUL。引入了对应的日志框架依赖后,会自动使用对应的框架。

同时引入多个日志框架,spring-jcl的优先加载顺序是:log4j-to-slf4j > log4j > full slf4j > slf4j > jul

注意:

  • 这里的slf4j只是日志门面(facade模式),它必须绑定具体的日志框架才能工作(下一节详细分析)
  • full slf4j指的是SLF4J including location awareness support,对应org.slf4j.spi.LocationAwareLogger类,它可以提取发生的位置信息(例如方法名、行号等),必须配合桥接器(brigdes)使用
  • slf4j指的是标准的SLF4J接口,对应org.slf4j.Logger类

二、slf4j分析

概述

上面介绍了spring自带的日志适配器框架spring-jcl,但实际项目中大家更喜欢使用SLF4J,它不依赖于spring,架构设计更合理,可以兼容未来的日志框架。

SLF4J(Simple Logging Façade for Java)日志框架,是各种日志框架的简单门面(simple facade)或抽象接口,允许用户部署时选择具体的日志实现。

准备

slf4j与spring-jcl的使用方法是不同的(logger与log)

//slf4j api
LoggerFactory.getLogger(AdvanceEverydayApplication.class)
        .info("hello world!");

//spring-jcl api
LogFactory.getLog(AdvanceEverydayApplication.class)
        .info("hello world!");

 

slf4j的优点

  • 采用静态绑定,简单,健壮,并且避免了jcl的类加载器问题
  • 增强了参数化日志,提升性能
  • 可以支持未来的日志系统

 

使用slf4j的步骤

  1. 添加slf4j-api的依赖
  2. 绑定具体的日志实现框架
  1. 绑定已经实现了slf4j的日志框架,直接添加对应依赖
  2. (或者)绑定没有实现slf4j的日志框架,先添加日志的适配器,再添加实现类的依赖
  1. 使用slf4j的API在项目中进行统一的日志记录

(注意第2点,slf4j与spring-jcl不同,它只绑定一种日志框架,引入多种框架有可能会报错)

 

源码分析

下面源码是基于slf4j-api-1.7.30版本,依赖如下图所示

 

我们直接从LoggerFactory.getLogger()方法开始,一直到slf4j的绑定实现为止

// 只关注下面第3行代码即可
public static Logger getLogger(Class<?> clazz) {
    Logger logger = getLogger(clazz.getName());
    // 如果系统属性-Dslf4j.detectLoggerNameMismatch=true,进行检查
    if (DETECT_LOGGER_NAME_MISMATCH) {
        Class<?> autoComputedCallingClass = Util.getCallingClass();
        if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
            Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
                            autoComputedCallingClass.getName()));
            Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
        }
    }
    return logger;
}
// 关注第3行,怎样创建的ILoggerFactory?
public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    // 利用工厂对象获取Logger对象
    return iLoggerFactory.getLogger(name);
}
// 重点关注第8行即可,performInitialization()是只执行一次的初始化
// 常量代表了LoggerFactory的初始化状态,给出了中文注释
public static ILoggerFactory getILoggerFactory() {
    if (INITIALIZATION_STATE == UNINITIALIZED) {
        synchronized (LoggerFactory.class) {
            if (INITIALIZATION_STATE == UNINITIALIZED) {
                INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                performInitialization();
            }
        }
    }
    switch (INITIALIZATION_STATE) {
    case SUCCESSFUL_INITIALIZATION:
        // 初始化成功,存在StaticLoggerBinder,返回具体的工厂
        return StaticLoggerBinder.getSingleton().getLoggerFactory();
    case NOP_FALLBACK_INITIALIZATION:
        // 没有找到日志框架,返回一个无操作的工厂
        return NOP_FALLBACK_FACTORY;
    case FAILED_INITIALIZATION:
        // 日志框架初始化失败,需要抛出失败的原因
        throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
    case ONGOING_INITIALIZATION:
        // support re-entrant behavior.
        // See also http://jira.qos.ch/browse/SLF4J-97
        return SUBST_FACTORY;
    }
    throw new IllegalStateException("Unreachable code");
}
// 第3行bind,就是大家常说的slf4j静态绑定
private final static void performInitialization() {
    bind();
    if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
        // 判断是否匹配
        versionSanityCheck();
    }
}
// 代码很长,重点关注第13行,catch里面是slf4j-api的版本兼容提示
private final static void bind() {
    try {
        Set<URL> staticLoggerBinderPathSet = null;
        // skip check under android, see also
        // http://jira.qos.ch/browse/SLF4J-328
        if (!isAndroid()) {
            // 如果有多个日志框架,这里进行检测并提示
            staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
            reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
        }
        // the next line does the binding
        StaticLoggerBinder.getSingleton();
        // 设置为成功初始化状态
        INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
        // 报告实际绑定的日志框架
        reportActualBinding(staticLoggerBinderPathSet);
    } catch (NoClassDefFoundError ncde) {
        // StaticLoggerBinder这个类不存在,设置状态并打印提示
        String msg = ncde.getMessage();
        if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
            INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
            Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
            Util.report("Defaulting to no-operation (NOP) logger implementation");
            Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
        } else {
            failedBinding(ncde);
            throw ncde;
        }
    } catch (java.lang.NoSuchMethodError nsme) {
        // StaticLoggerBinder.getSingleton()这个方法不存在,设置状态并打印提示
        String msg = nsme.getMessage();
        if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
            INITIALIZATION_STATE = FAILED_INITIALIZATION;
            Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
            Util.report("Your binding is version 1.5.5 or earlier.");
            Util.report("Upgrade your binding to version 1.6.x.");
        }
        throw nsme;
    } catch (Exception e) {
        failedBinding(e);
        throw new IllegalStateException("Unexpected initialization failure", e);
    } finally {
        postBindCleanUp();
    }
}

这一句就是关键代码:StaticLoggerBinder.getSingleton()

StaticLoggerBinder这个类指定了包路径: import org.slf4j.impl.StaticLoggerBinder;

但是这个类并不在slf4j-api的包内,它是由各个日志框架提供的。

例如,logback-classic包里自带这个类,单例模式,直接适配slf4j

 

如果一个日志框架不带这个类怎么办?

自己写一个(😊),引入一个适配器就好了,例如log4j本身没有这个类,我们就引入log4j-slf4j-impl,就可以用slf4j的api调用log4j框架了。

可以参考下图,log4j-slf4j-impl这个包内一共只有10个类,主要就是提供了StaticLoggerBinder,注意指定了它的包路径 org.slf4j.impl

这就是slf4j静态绑定日志框架的方式,简单,稳定。

 

最后,附上一张SLF4J官网的架构示例图,相信通过上面的介绍,现在看这张图会很清晰

tips

这里有一个疑问,slf4j-api 中不包含 StaticLoggerBinder 类,为什么能编译通过呢?

我们项目中用到的 slf4j-api 是已经编译好的 class 文件,不需要再次编译,所以使用上没有问题。

至于编译前,slf4j-api代码中应该是包含 StaticLoggerBinder.java 的,且编译后也存在 StaticLoggerBinder.class ,发布时把它删除就可以了。

 

三、logback分析

简介

上面的介绍都是对日志框架的封装使用,下面以logback为例,来实际分析一下实际的日志框架

Logback是一个开源的日志组件,是log4j的作者开发的用来替代log4j的。它被分成三个不同的模块:logback-core,logback-classic,logback-access。

  1. logback-core 是其它两个模块的基础。
  2. logback-classic 模块可以看作是 log4j 的一个优化版本,它天然的支持 SLF4J。
  3. logback-access 提供了 http 访问日志的功能,可以与 Servlet 容器进行整合,例如:Tomcat、Jetty。

使用

实际使用中,如果不需要网络功能,只导入logback-core和logback-classic两个依赖包即可,别忘了在classpath路径下加入logback.xml配置文件。找了一个具体的配置示例,有详细的中文注释。

logback.xml示例

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
 
    <!-- 定义变量 -->
    <!-- 日志文件大小 -->
    <property name="log.maxSize" value="10MB"/>
 
    <!-- 日志占用最大空间 -->
    <property name="log.totalSizeCap" value="10GB"/>
 
    <!-- 定义日志文件 输入位置 -->
    <!-- 如果使用的是相对路径的话,当部署到tomcat路径下的时候,默认是输出到tomcat的bin目录下。
     "../logs" 的意思是把日志输出到tomcat的logs目录下 -->
    <property name="log.dir" value="../logs/ssm"/>
 
    <!-- 日志最大的历史 30天 -->
    <property name="log.maxHistory" value="30"/>
 
    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 
        <!-- 临界值过滤器,过滤掉 TRACE 和 DEBUG 级别的日志 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
 
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
 
    </appender>
 
    <!-- RollingFileAppender:滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件。 -->
    <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
 
        <!-- 如果是 true,日志被追加到文件结尾,如果是 false,清空现存文件,默认是true。 -->
        <append>true</append>
 
        <!-- 级别过滤器,只记录ERROR级别的日志 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
 
        <!-- 当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名。 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
 
            <!-- 必要节点,包含文件名及“%d”转换符,
                “%d”可以包含一个 java.text.SimpleDateFormat指定的时间格式,如:%d{yyyy-MM}。 -->
            <fileNamePattern>${log.dir}/error-log.%d{yyyy-MM-dd}.log</fileNamePattern>
 
            <!-- 可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。
            假设设置每天滚动,且 <maxHistory>是30,则只保存最近30天的文件,
            删除之前的旧文件。 注意,删除旧文件是,那些为了归档而创建的目录也会被删除。 -->
            <maxHistory>${log.maxHistory}</maxHistory>
 
        </rollingPolicy>
 
        <encoder>
            <pattern>%d %-4relative [%thread] %-5level %logger - %msg%n</pattern>
        </encoder>
 
    </appender>
 
    <!-- INFO -->
    <appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
 
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
 
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.dir}/info-log.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>${log.maxHistory}</maxHistory>
        </rollingPolicy>
 
        <encoder>
            <pattern>%d %-4relative [%thread] %-5level %logger - %msg%n</pattern>
        </encoder>
 
    </appender>
 
    <!-- DEBUG -->
    <appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
 
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>DEBUG</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
 
        <!-- 从日期与文件大小两个纬度控制日志文件分割 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 日志按日期分割时必须要有"%d",表示日期 -->
            <!-- 日志按大小分割时必须要有"%i",表示个数 -->
            <fileNamePattern>${log.dir}/debug-log.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>${log.maxSize}</maxFileSize>
            <maxHistory>${log.maxHistory}</maxHistory>
            <totalSizeCap>${log.totalSizeCap}</totalSizeCap>
        </rollingPolicy>
 
        <encoder>
            <pattern>%d %-4relative [%thread] %-5level %logger - %msg%n</pattern>
        </encoder>
    </appender>
 
    <!-- 根logger -->
    <!-- 日志级别:ALL < DEBUG < INFO < WARN < ERROR < FATAL < OFF -->
    <root level="DEBUG">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="DEBUG"/>
        <appender-ref ref="INFO"/>
        <appender-ref ref="ERROR"/>
    </root>
 
    <!-- name:用来指定受此logger约束的某一个包或者具体的某一个类。
        1、没有配置level,将继承它的上一级<root>的日志级别“debug”。
        2、additivity默认为true,将此logger的打印信息向上级传递。
        3、没有设置appender,此logger本身不打印任何日志信息。
        4、root接收到下级传递的信息,交给已配置好的名为“stdout”的appender处理。
    <!-- 处理com.snsprj.controller包下所有日志,
    只输出level >= DEBUG级别的日志-->
    <logger name="com.snsprj.controller" additivity="false" level="DEBUG">
        <appender-ref ref="INFO"/>
        <appender-ref ref="ERROR"/>
    </logger>
 
</configuration>

几个重要的元素如下:

  1. <configuration> 主要用于配置某些全局的日志行为,包含如下属性

属性名

描述

debug

是否打印 logback 的内部状态,开启有利于排查 logback 的异常。默认 false

scan

是否在运行时扫描配置文件是否更新,如果更新时则重新解析并更新配置。如果更改后的配置文件有语法错误,则会回退到之前的配置文件。默认 false

scanPeriod

多久扫描一次配置文件是否修改,单位可以是毫秒、秒、分钟或者小时。默认情况下,一分钟扫描一次配置文件。

  1. <appender> 用于定义日志的输出目的地和输出格式,被 logger 所持有。常用的有如下几种

类名

描述

ConsoleAppender

将日志通过 System.out 或者 System.err 来进行输出,即输出到控制台。

FileAppender

将日志输出到文件中。

RollingFileAppender

继承自 FileAppender,也是将日志输出到文件,但文件具有轮转功能。

DBAppender

将日志输出到数据库

SocketAppender

将日志以明文方式输出到远程机器

SSLSocketAppender

将日志以加密方式输出到远程机器

SMTPAppender

将日志输出到邮件

  1. <logger> 是用于配置打印日志的对象,通常用来对特定的包或类设置日志级别和输出方式
  2. <encoder> 负责将日志事件按照配置的格式转换为字节数组
  3. <filter> 用于对日志事件进行过滤输出