日志框架、日志等级、日志框架的正确使用方法
保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/54/ )。
1. 日志框架与日志等级
日志框架是一种在目标对象发生变化时将相关信息记录进日志文件的框架。这样,当目标对象出现问题或需要核查目标对象变动历史时,日志框架记录的日志文件便可以提供详实的资料。
起初,Java的日志打印依靠软件开发者自行编辑输出语句将日志输出到文件流中。例如通过“System.out.println
”方法打印普通信息或通过“System.err.println
”方法打印错误信息。
开发者自行编辑输出语句进行日志打印的方式非常烦琐,而且还会导致日志格式混乱,不利于日志分析软件的进一步处理。为了解决这些问题,产生了大量的日志框架。
经过多年的发展,Java领域的日志框架已经非常丰富,有log4j、Logging、commons-logging、slf4j、logback等,它们为Java的日志打印工作提供了极大的便利。
为了方便日志管理,日志框架大都对日志等级进行了划分。常见的日志等级划分方式如下:
- FATAL:致命等级的日志,指发生了严重的会导致应用程序退出的事件。
- ERROR:错误等级的日志,指发生了错误,但是不影响系统运行。
- WARN: 警告等级的日志,指发生了异常,可能是潜在的错误。
- INFO: 信息等级的日志,指一些在粗粒度级别上需要强调的应用程序运行信息。
- DEBUG:调试等级的日志,指一些细粒度的对于程序调试有帮助的信息。
- TRACE:跟踪等级的日志,指一些包含程序运行详细过程的信息。
有了以上日志划分后,在打印日志时我们就可以定义日志的级别。而进行日志的输出时也可以根据日志等级进行输出,防止大量的日志信息混杂在一起。目前在很多集成开发环境中可以调节日志的显示级别,使得具有一定级别以上的日志才会显示出来,这样能够根据不同的使用情形进行日志的筛选。下图展示了划分了等级的日志在集成开发环境IntelliJ IDEA Community Edition中的展示效果。
2. 日志框架的正确使用
在这一节,我们将通过MyBatis的源码学习如何正确地使用日志框架。
MyBatis作为一个ORM框架,需要在进行参数解析、数据库操作、结果生成过程中打印日志,而它的日志打印并不是自己实现的。而是要对接到应用设置的日志框架上,例如log4j、Logging、commons-logging、slf4j、logback等,由它们打印日志。因此,MyBatis不仅要使用日志框架,而且要根据需要使用多种日志框架。
MyBatis对接和调用日志框架的相关代码在其logging包中,该包中最重要的就是Log接口,它有11个实现,分布在logging
包的不同子包中。
Log接口及其实现类的类图如下所示。
我们先详细了解下Log接口中的方法。Log接口中定义了日志框架要实现的几个基本方法:
- error:打印Error级别日志
- warn:打印Warn级别日志
- debug:打印Debug级别日志
- trace:打印Trace级别日志
- isDebugEnabled:判断打印Debug级别的日志的功能是否开启
- isTraceEnabled:判断打印Trace级别日志的功能是否开启
上述各个方法主要是实现不同级别日志的打印功能。然而,其中的isDebugEnabled
方法和isTraceEnabled
方法略显突兀,我们单独进行下说明。
isDebugEnabled
方法和isTraceEnabled
方法是从效率角度考虑而设计的。
首先,Debug
和Trace
是两个级别比较低的日志,越是低级别的日志越有这样的特点:
- 很少开启:因为它们级别很低,多数时候该级别的信息不需要展示。
- 输出频次高:低级别日志的触发门槛很低,这意味着一旦它们开启,往往会以非常高的频率输出日志信息。
- 内容冗长:它们中往往包含非常丰富和细致的信息,因此信息内容往往十分冗长。
假如存在下面所示的日志打印操作,在日志打印过程中调用了trace方法。
trace("Application is : " + appName + "; " +
"Class is : " + className + "; " +
"Function is : " + funcitonName +". " +
"Params : " + params + "; " +
"Return is : " + result +".");
以“org.apache.commons.logging.impl.SimpleLog
”下的trace
方法(可以通过JakartaCommonsLoggingImpl
实现类中的trace方法追踪到该方法)为例,其具体实现如下所示。
public final void trace(Object message) {
if (this.isLevelEnabled(1)) {
this.log(1, message, (Throwable)null);
}
}
低级别的日志很少开启,这意味着this.isLevelEnabled(1)
的返回值大概率是false
。因此字符串拼接结果是无用的,会被直接丢弃。并且低级别日志输出频次高且内容冗长,这意味着这种无用的字符串拼接是频发的且资源消耗很大的。
要想避免上述无用的字符串操作导致的大量的系统资源消耗,就需要使用isDebugEnabled
方法和isTraceEnabled
方法对低级别的日志输出进行前置判断,如下所示。
if (log.isTraceEnabled()){
trace("Application is : " + appName + "; " +
"Class is : " + className + "; " +
"Function is : " + funcitonName +". " +
"Params : " + params + "; " +
"Return is : " + result +".");
}
这样,借助isTraceEnabled方法就避免了资源的浪费。(均参考自《通用源码阅读指导书——MyBatis源码详解》)
在阅读源码的过程中,读懂源码只是完成了浅层知识的学习。在读懂源码的同时思考源码为何这么设计将会使我们有更大收获,而这也会使我们更容易读懂源码。
在Log接口的11个实现类中,最简单的实现类就是NoLoggingImpl类,因为它是一种不打印日志的实现,内部几乎没有任何的操作逻辑。StdOutImpl实现类也非常简单,对于error级别的日志调用了System.err.println
进行打印,而对其他级别的日志调用了System.out.println
进行打印。
剩下的9个实现类中,Slf4jLocationAwareLoggerImpl
类和Slf4jLoggerImpl
类是Slf4jImpl
类的装饰器,Log4j2AbstractLoggerImpl
类和Log4j2LoggerImpl
类是Log4j2Impl
类的装饰器。这四个装饰器类结构非常简单,我们不再展开介绍。
参考《通用源码阅读指导书——MyBatis源码详解》,我们重点分析剩下的5个实现类,它们是JakartaCommonsLoggingImpl
、Jdk14LoggingImpl
、Log4jImpl
、Log4j2Impl
、Slf4jImpl
。我们以commons子包中的JakartaCommonsLoggingImpl为例,查看其具体实现。下面代码是JakartaCommonsLoggingImpl
类的部分源码。
public class JakartaCommonsLoggingImpl implements org.apache.ibatis.logging.Log {
private final Log log;
public JakartaCommonsLoggingImpl(String clazz) {
log = LogFactory.getLog(clazz);
}
@Override
public boolean isDebugEnabled() {
return log.isDebugEnabled();
}
@Override
public boolean isTraceEnabled() {
return log.isTraceEnabled();
}
// 省略其他代码
}
可以看出,JakartaCommonsLoggingImpl是一个典型的对象适配器。它的内部持有一个“org.apache.commons.logging.Log
”对象,然后所有方法都讲操作委托给了“org.apache.commons.logging.Log
”对象。
关于适配器的原理,即具体实现如何注入到其中我们不再展开介绍。这里,推荐大家阅读《通用源码阅读指导书——MyBatis源码详解》。
《通用源码阅读指导书——MyBatis源码详解》是一本以MyBatis的源码为实例讲述源码阅读方法的书籍,并且附带有示例项目源码,MyBatis的全中文注解。书籍还总结了大量的编程知识和架构经验,对提升编程和架构能力十分有用,非常推荐。
可以访问个人知乎阅读更多文章:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。