SpringBoot日志框架分析

本文简介

  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的日志框架,先添加日志的适配器,再添加实现类的依赖
  3. 使用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> 用于对日志事件进行过滤输出
posted @   SonicLi  阅读(264)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示