SpringBoot日志框架分析
本文简介
- 第一部分,介绍spring-jcl适配各种日志框架的方式
- 第二部分,介绍slf4j适配各种日志框架的方式
- 第三部分,介绍下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的步骤
- 添加slf4j-api的依赖
-
绑定具体的日志实现框架
- 绑定已经实现了slf4j的日志框架,直接添加对应依赖
- (或者)绑定没有实现slf4j的日志框架,先添加日志的适配器,再添加实现类的依赖
- 使用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-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。
- logback-core 是其它两个模块的基础。
- logback-classic 模块可以看作是 log4j 的一个优化版本,它天然的支持 SLF4J。
- 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>
几个重要的元素如下:
- <configuration> 主要用于配置某些全局的日志行为,包含如下属性
属性名 | 描述 |
debug | 是否打印 logback 的内部状态,开启有利于排查 logback 的异常。默认 false |
scan | 是否在运行时扫描配置文件是否更新,如果更新时则重新解析并更新配置。如果更改后的配置文件有语法错误,则会回退到之前的配置文件。默认 false |
scanPeriod | 多久扫描一次配置文件是否修改,单位可以是毫秒、秒、分钟或者小时。默认情况下,一分钟扫描一次配置文件。 |
- <appender> 用于定义日志的输出目的地和输出格式,被 logger 所持有。常用的有如下几种
类名 | 描述 |
ConsoleAppender | 将日志通过 System.out 或者 System.err 来进行输出,即输出到控制台。 |
FileAppender | 将日志输出到文件中。 |
RollingFileAppender | 继承自 FileAppender,也是将日志输出到文件,但文件具有轮转功能。 |
DBAppender | 将日志输出到数据库 |
SocketAppender | 将日志以明文方式输出到远程机器 |
SSLSocketAppender | 将日志以加密方式输出到远程机器 |
SMTPAppender | 将日志输出到邮件 |
- <logger> 是用于配置打印日志的对象,通常用来对特定的包或类设置日志级别和输出方式
- <encoder> 负责将日志事件按照配置的格式转换为字节数组
- <filter> 用于对日志事件进行过滤输出
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理