Java日志框架体系

日志

为什么需要日志?

什么是日志?

日志是用于记录系统的关键信息,帮助我们及时、全面的了解系统运行的轨迹、状态等信息,从而让我们可以更好的掌控系统:

  • 在系统出现问题时,能够快速、准确的定位出问题;
  • 通过日志发现系统存在的潜在问题,及时有效解决避免问题的出现;
  • 生产环境下,采集业务数据进行整理分析,了解各种业务的数据量、数据特点等消息,接口是否出现性能瓶颈等,为系统下一步的开发设计、重构优化等提供有效的数据支撑;

项目开发阶段,我们可以依赖IDE开发工具的Debug调试功能追踪解决Bug,但是项目发布到测试、生产环境就不允许我们这么做了,所以,日志的作用在测试、生产环境等没有Debug调试工具的环境下了解系统运行中的关键信息。

日志不可能记录系统运行的全部消息,即使可行我们也不能这么做,怎么判断哪些信息是关键信息需要记录日志,这个就是日志的精髓所在。优秀的项目是能根据日志快速定位问题的,而不是在线调试、或者半天找不到有用的日志而抓狂。

日志实现框架

Log4j

Log4j是目前最为流行的Java日志框架之一,1999年发布首个版本,2012年发布最后一个版本,2015年正式宣布终止,官方也已不建议使用,并逐步被LogbackLog4j2等日志框架所替代,可是无法掩饰光辉历程,以及优良的设计理念。尽管Log4j有着出色的历史战绩,但早已不是Java日志框架的最优选择。

Log4j API核心类:

  • 日志对象:org.apache.log4j.Logger
  • 日志级别:org.apache.log4j.Level
  • 日志管理器:org.apache.log4j.LogManager
  • 日志仓储:org.apache.log4j.spi.LoggerRepository
  • 日志附加器:org.apache.log4j.Appender
  • 日志过滤器:org.apache.log4j.spi.Filter
  • 日志格式布局:org.apache.log4j.Layout
  • 日志事件:org.apache.log4j.LoggingEvent
  • 日志配置器:org.apache.log4j.spi.Configurator
  • 日志诊断上下文:org.apache.log4j.NDCorg.apache.log4j.MDC

JUL

Java LoggingJava标准的日志框架,也称为Java Logging API,即 JSR 47。从Java 1.4版本开始,Java Logging成为 Java SE的功能模块,其实现类存放在java.util.logging包下。

使用Java Logging的最大好处就是是JDK内置,默认配置文件位于:jre/lib/logging.properties,具体可以查看LogManagerreadConfiguration方法,启动的时候可以通过设置VM参数java.util.logging.config.file指定配置文件。

Java Logging API核心类:

  • 日志对象:java.util.logging.Logger
  • 日志级别:java.util.logging.Level
  • 日志管理器:java.util.logging.LogManager
  • 日志处理器:java.util.logging.Handler
  • 日志过滤器:java.util.logging.Filter
  • 日志格式器:java.util.logging.Formatter
  • 日志记录:java.util.logging.LogRecord
  • 日志权限:java.util.logging.LoggingPermission
  • 日志JMX接口:java.util.logging.LoggingMXBean

Logback

LogbackLog4j创始人设计的又一个开源日志框架,可以看成Log4j的替代者,在架构和特征上有着相当提升。Logback当前分成三个模块:

  • logback-core:其它两个模块的基础模块,提供一些关键的通用机制
  • logback-classic:地位和作用等同于Log4j,也被认为是Log4j的一个改进版,并且实现了SLF4J API
  • logback-accesslogback-access访问模块与TomcatJettyServlet容器集成配置Http访问的access日志

1552315348282

Logback核心类:

  • 日志对象:ch.qos.logback.classic.Logger
  • 日志级别:ch.qos.logback.classic.Level
  • 日志管理器:ch.qos.logback.classic.LoggerContext
  • 日志附加器:ch.qos.logback.core.Appender
  • 日志过滤器:ch.qos.logback.core.filter.Filter
  • 日志格式布局:ch.qos.logback.core.Layout
  • 日志事件:ch.qos.logback.classic.spi.LoggingEvent
  • 日志配置器:ch.qos.logback.classic.spi.Configurator

核心架构

1551851562345

上图是logback日志框架的输出日志的核心流程:

  • Logger作为日志框架的代言人,程序开发通过Logger即可完成日志输出工作;
  • Logger拿到程序传入的日志信息,通过Filter进行过滤,一般是对日志级别Level进行过滤,然后将符合条件的日志封装成LoggingEvent对象,并交接给关联的Appender对象进行后续处理;
  • Appender完成日志输出工作,一般Appender也会有个Filter过滤流程,将过滤成功的日志输出到控制台、文件、网络等操作;

LoggerAppender是日志框架比较核心组件,Logger代表日志输入源,其配置样例见下:

1551852736921

Appender代表日志输出源,其配置样例见下:

1551852805112

LoggerAppender相互独立,都可以实现对日志过滤操作,同时可以实现多对多映射关系,在开发中可以对这些特性灵活应用。比如:生产中一个很常见的做法就是构建一个Level=ErrorAppender,然后让所有的Logger都指向该Appender就可以实现汇聚系统中所有Error级别的日志,可以快速监测系统运行是否出现异常状况。

Appender

1551841803031

<appender>节点被配置时,必须配置两个属性nameclassname指定Appender的名称,而class指定Appender具体的实现类。

Appender核心类结构图:

1551841036048

UnsynchronizedAppenderBase:非线程安全的Appender基类,即public void doAppend(E eventObject)没有使用synchronized关键字,而AppenderBase类中的doAppend()方法都使用了synchronized关键字:public synchronized void doAppend(E eventObject)

Level

日志可以分配级别,包括:ALLTRACEDEBUGINFOWARNERROROFF,其中ALLOFF日志级别是用于AppenderLogger过滤使用。

  • TRACE(追踪):输出更细致的程序运行轨迹;
  • DEBUG(调试):这个级别一般记录一些运行中的中间参数信息,只允许在开发环境开启,选择性在测试环境开启;
  • INFO(信息):用来记录程序运行中的一些有用的信息,例如:程序运行开始、结束、耗时、重要参数等信息,需要注意有选择性的有意义的输出,到时候自己找问题看一堆日志却找不到关键日志就没有意义了;
  • WARN(警告):一般用来记录一些用户输入参数错误;
  • ERROR(错误):一般用来记录程序中发生的任何异常错误信息(Throwable),或者是记录业务逻辑错误;

Logger

通过LoggerFactory获取LoggerLogger getLogger(String name)LoggerFactory采用工厂设计模式,内部维护一个Map缓存所有生成的Logger实例信息:Map<String, Logger> loggerCache = new ConcurrentHashMap()

继承规则

Logger是有层次关系的,我们可一般性的理解为包名之间的父子继承关系。每个Logger通常以class全限名称为其名称。子Logger通常会从父Logger继承Logger级别Appender等信息。

1551856364301

统一日志API

日志框架无论Log4j还是Logback,虽然它们功能完备,但是各自API相互独立,并且各自为政。当应用系统在团队协作开发时,由于工程师人员可能有所偏好,因此,可能导致一套系统同时出现多套日志框架情况。

其次,最流行的日志框架基本上基于实现类编程,而非接口编程,因此,暴露一些无关紧要的细节给用户,这种耦合性是没有必要的。

诸如此类的原因,开源社区提供统一日志API框架,最为流行的是:

  • apache commons-logging:简称JCL,适配log4jjava logging
  • slf4j:适配log4jlog4j2java logginglogback

统一日志API,即日志门面接口层,直白点讲:提供了操作日志的接口,而具体实现交由LogbackLog4j等日志实现框架,这样就可以实现程序与具体日志框架间的解耦,对于底层日志框架的改变,并不影响到上层的业务代码,可以灵活切换日志框架。

日志体系

现在日志框架众多:slf4jjcljullog4jlog4j2logback等,它们之间存在什么样的关系,我们在开发过程中又如何选取这些日志框架呢?

首先,看下Java日志体系:

1541036373003

通过上图可以概括日志体系大致分为三层:日志接口门面层、绑定/桥接层以及日志实现层。

1
2
3
4
5
6
7
8
9
10
11
12
13
jcl-over-slf4j.jar(jcl -> slf4j):将commons-logging日志桥接到slf4j
jul-to-slf4j.jar(jul -> slf4j):java.util.logging的日志桥接到slf4j

log4j-over-slf4j.jar(log4j -> slf4j):将log4j的日志,桥接到slf4j
slf4j-log4j12.jar(slf4j -> log4j):slf4j绑定到log4j,所以这个包不能喝log4j-over-slf4j.jar不能同时使用,会出现死循环

slf4j-jcl.jar(slf4j -> jcl):slf4j绑定到commons-logging日志框架上
slf4j-jdk14.jar(slf4j -> jul):slf4j绑定到jdk日志框架上,不能喝jul-to-slf4j.jar同时使用,会出现死循环

slf4j-nop.jar:slf4j的空接口输出绑定,丢弃所有日志输出
slf4j-simple.jar:slf4j自带的简单日志输出接口
log4j-slf4j-impl.jar(slf4j -> log4j2):将slf4j绑定到log4j2日志框架上,不能和log4j-to-slf4j同时使用
log4j-to-slf4j.jar(log4j2 -> slf4j):将log4j2日志桥接到slf4j上,不能和log4j-slf4j-impl同时使用

最为熟悉和使用率较高的log4j其实就位于日志实现层,即其为一种日志实现框架。既然log4j已经足够系统使用进行日志输出了,为啥还多此一举弄个日志接口门面层绑定/桥接层?看下图:

1542695348705

系统A集成了模块A模块B模块C三个模块,但是这三个模块使用了不同的日志实现框架,现在系统A相当于同时存在了三个日志框架,那如何进行配置呢?每个框架都构建一个配置文件这种肯定是不行的,没法进行统一管理,日志较为混乱。

现在看下如何解决上述问题:

1542693538215

模块A模块B模块C采用slf4j日志接口框架,而非具体日志实现类,具体使用哪种日志实现框架是由系统A配置决定的,系统A把slf4j绑定到logback,则统一采用logback日志框架,slf4j绑定到log4j则统一采用log4j日志框架。日志接口 --> 日志绑定 --> 日志实现,日志接口和日志实现进行了解耦,模块只关注接口不关注实现,具体采用哪种实现是由其所在的系统环境决定,这样就可以实现日志的统一配置和管理。

对于上述解决方案,如果模块A模块B模块C是新开发统一采用slf4j日志接口框架没问题,但是对于旧系统,比如模块B模块C都是很久之前开发的模块,分别采用了不同的日志实现框架,见下图:

1542697115395

如果系统A把slf4j绑定到logback日志框架上,但是模块B模块C由于没有采用slf4j,绑定对于它们来说是无效的,这时候就要使用桥接

1542697797159

桥接的大致结构如上图,通过桥接把log4j、jdk log桥接到slf4j上,由于slf4j又被绑定到了logback上,则模块B模块C最终会被logback纳管,而不是log4j和jdk log,同样可以实现日志统一配置管理。

spring体系中日志框架

Spring框架

Spring Framework 4.X及之前的版本,都是使用的标准版JCL日志框架,该依赖由spring-core间接引入。Spring框架的日志信息都是使用JCL标准接口来进行输出。下面说下项目中常碰到的三种情况:

  • 项目中使用log4jcommons-logging原生支持和log4j的动态绑定,所以不需要任何配置即可将jcl的日志输出绑定到log4j上;
  • 项目中使用log4j2commons-logging原生并不支持和log4j2的动态绑定,但是log4j2本身提供了将jcl绑定到log4j2的依赖包:log4j-jcl.jar
  • 项目中使用slf4j:需要采用桥接模式jcl日志引入到SLF4J上,添加依赖包jcl-over-slf4j.jar,否则可能Spring框架的日志无法输出到日志文件中。

使用spring 4.X及之前版本的框架时一定要注意上面情况,否则很容易出现业务日志输出正常,但是spring框架本身日志没有输出的情况,导致一些错误无法察觉或者不利于排查。

spring5.0带来了commons-logging桥接模块的封装,它被叫做spring-jcl而不是标准版jcl,无需添加额外依赖包,可自动检测绑定到Log4j2SLF4J

SpringBoot框架

springboot-1.X - springboot-2.X:

1552014220124

SpringBoot框架可以看出,默认采用SLF4J+Logback组合的日志框架,通过桥接模式将其它日志框架桥接到SLF4J上。

SLF4J

SLF4J(Simple Logging Facade For Java)是一个为Java程序提供日志输出的统一接口,并不是一个具体的日志实现方案,就像我们经常使用的JDBC一样,只是了一些标准规范接口。因此,单独的SLF4J是不能工作的,它必须搭配其他具体的日志实现方案。

SLF4JLogback是同一个作者开发的,所以Logback天然与SLF4J适配,不需要引入额外适配库。

这里还有个比较有意思的事情,SLF4J项目提供了很多适配库、桥接库,唯独没有提供对Log4j2的适配库和桥接库,不过Apache Logging项目组自己开发了:log4j-slf4j-impllog4j-to-slf4j

JCL

Jakarta commons-logging简称JCL,是apache提供的一个通用日志门面接口,最后版本更新停留在2014年,且默认只能提供对Log4jJava Logging进行适配。

JCL已慢慢淡出人们的视线,一些历史遗留项目也开始慢慢由JCL转向SLF4J,如:Spring 5.0开始没有再依赖原生的JCL框架,SpringBoot默认采用SLF4J+LogbackSLF4J已经成为了Java日志组件的明星选手,可以完美替代JCL,使用JCL桥接库也能完美兼容一切使用JCL作为日志门面的类库,现在的新系统已经没有不使用SLF4J作为统一日志API接口层的理由了。

核心原理

SLF4JJCL对比,二者最大区别在于与日志服务的绑定机制。

SLF4J绑定原理

当项目中存在多个StaticLoggerBinder.class文件时,运行项目会出现以下日志:

1
2
3
4
5
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/C:/Users/jiangmitiao/.m2/repository/ch/qos/logback/logback-classic/1.1.3/logback-classic-1.1.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/C:/Users/jiangmitiao/.m2/repository/org/slf4j/slf4j-log4j12/1.7.12/slf4j-log4j12-1.7.12.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]

大致流程

LoggerFactory(SLF4J提供) –> StaticLoggerBinder.getSingleton().getLoggerFactory()(`类是适配库提供,ILoggerFactory接口是SLF4J定义的,实现类是适配库提供) --> ILoggerFactory.getLogger(String name)获取Logger实例--> 通过Logger`实例进行日志操作

总结

  • 获取实现ILoggerFactory接口的实例:StaticLoggerBinder.getSingleton().getLoggerFactory()
  • ILoggerFactory提供的getLogger()方法获取Logger实例,进行日志操作;
  • getLogger()方法核心逻辑:调用具体日志实现框架的日志工厂类getLogger()方法获取一个Logger实例,然后统一封装成org.slf4j.Logger的一个子类,采用的是设计模式中的包装模式

下面是slf4j-log4j12适配库中getLogger()方法实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Logger getLogger(String name) {
Logger slf4jLogger = loggerMap.get(name);
if (slf4jLogger != null) {
return slf4jLogger;
} else {
org.apache.log4j.Logger log4jLogger;
if (name.equalsIgnoreCase(Logger.ROOT_LOGGER_NAME))
log4jLogger = LogManager.getRootLogger();
else
log4jLogger = LogManager.getLogger(name);

Logger newInstance = new Log4jLoggerAdapter(log4jLogger);
Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
return oldInstance == null ? newInstance : oldInstance;
}
}

注意

SLF4J1.8版本起,开始使用SPI方式寻找日志实现框架,而不再采用之前通过寻找指定类StaticLoggerBinder的方式发现并绑定实现框架。

JCL绑定原理

JCL采用动态绑定机制,缺点是容易引发混乱,在一个复杂甚至混乱的依赖环境下,确定当前正在生效的日志服务是很费力的,特别是在程序开发和设计人员并不理解JCL的机制时。

JCL动态绑定的核心逻辑位于LogFactoryImpl类的discoverLogImplementation方法中如下代码块:

1
2
3
4
5
6
7
for(int i=0; i<classesToDiscover.length && result == null; ++i) {
/**
createLogFromClass()核心逻辑:通过Class.forName()加载适配器的类模板,
然后调用Constructor.newInstance()构建适配器类实例
*/
result = createLogFromClass(classesToDiscover[i], logCategory, true);
}

其中classesToDiscover数组的中定义了可以使用的适配器类,见下:

1
2
3
4
5
6
String[] classesToDiscover = {
"org.apache.commons.logging.impl.Log4JLogger",
"org.apache.commons.logging.impl.Jdk14Logger",
"org.apache.commons.logging.impl.Jdk13LumberjackLogger",
"org.apache.commons.logging.impl.SimpleLog"
};

总结

相较于JCL动态绑定机制SLF4J则简单得多,采用静态绑定机制

  • SLF4J则通过加载StaticLoggerBinder方式绑定到具体的日志实现框架,StaticLoggerBinder在每个适配库中都存在,如slf4j-log4j12slf4j-jdk14等,一个Java工程下只能引入一个SLF4J适配库
  • JCL则通过主动去寻找日志实现框架是否在当前环境中存在来判断是否进行绑定,且具有一定的绑定顺序,容易出现混乱,扩展性也不好;

所以SLF4J不仅对比JCL有性能上的优势,绑定机制设计上也要优于JCL,且SLF4J支持日志模板。

总结

对Java日志组件选型的建议

  • 统一日志API采用SLF4J,在模块中引入slf4j-api,需要绑定日志框架中引入logback-classic
  • 日志实现框架选型:如果最求高并发、高性能、日志量特别大的项目,可以采用Log4j2,否则都采用Logback
坚持原创技术分享,您的支持将鼓励我继续创作!