Log4j
是目前最为流行的 Java日志框架
之一, 1999年
发布首个版本, 2012年
发布最后一个版本, 2015年
正式宣布终止,官方也已不建议使用,并逐步被 Logback
和 Log4j2
等日志框架所替代,可是无法掩饰光辉历程,以及优良的设计理念。尽管 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.NDC
、 org.apache.log4j.MDC
Java Logging
是 Java
标准的日志框架,也称为 Java Logging API
,即 JSR 47
。从 Java 1.4
版本开始, Java Logging
成为 Java SE
的功能模块,其实现类存放在 java.util.logging
包下。
使用 Java Logging
最大好处是它属于 JDK内置
,不需要添加额外依赖,默认配置文件位于: jre/lib/logging.properties
,具体可以查看 LogManager
类 readConfiguration
方法,启动的时候可以通过设置 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
java.util.logging.LoggingMXBean
Logback
是 Log4j
创始人设计的又一个开源日志框架,可以看成 Log4j
的替代者,在架构和特征上有着相当提升。 Logback
当前分成三个模块:
logback-core
:其它两个模块的基础模块,提供一些关键的通用机制 logback-classic Log4j Log4j SLF4J API
logback-access logback-access Tomcat Jetty Servlet容器 Http access日志
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
上图是 logback
日志框架的输出日志的核心流程:
Logger
作为日志框架的代言人,程序开发通过 Logger
即可完成日志输出工作; Logger Filter Level LoggingEvent Appender
Appender Appender Filter
Logger
和 Appender
是日志框架比较核心组件, Logger
代表日志输入源,其配置样例见下:
Appender
代表日志输出源,其配置样例见下:
Logger
和 Appender
相互独立,都可以实现对日志过滤操作,同时可以实现多对多映射关系,在开发中可以对这些特性灵活应用。比如:生产中一个很常见的做法就是构建一个 Level=Error
的 Appender
,然后让所有的 Logger
都指向该 Appender
就可以实现汇聚系统中所有 Error
级别的日志,可以快速监测系统运行是否出现异常状况。
<appender>
节点被配置时,必须配置两个属性 name
和 class
。 name
指定 Appender
的名称,而 class
指定 Appender
具体的实现类。
UnsynchronizedAppenderBase
:非线程安全的Appender基类,即 public void doAppend(E eventObject)
没有使用 synchronized
关键字,而 AppenderBase
类中的 doAppend()
方法都使用了 synchronized
关键字: public synchronized void doAppend(E eventObject)
。
日志可以分配级别,包括: ALL
、 TRACE
、 DEBUG
、 INFO
、 WARN
、 ERROR
、 OFF
,其中 ALL
和 OFF
日志级别是用于 Appender
或 Logger
过滤使用。
TRACE(追踪)
:输出更细致的程序运行轨迹; DEBUG(调试)
:这个级别一般记录一些运行中的中间参数信息,只允许在开发环境开启,选择性在测试环境开启; INFO(信息)
:用来记录程序运行中的一些有用的信息,例如:程序运行开始、结束、耗时、重要参数等信息,需要注意有选择性的有意义的输出,到时候自己找问题看一堆日志却找不到关键日志就没有意义了; WARN(警告)
:一般用来记录一些用户输入参数错误; ERROR(错误)
:一般用来记录程序中发生的任何异常错误信息( Throwable
),或者是记录业务逻辑错误;
通过 LoggerFactory
获取 Logger
: Logger getLogger(String name)
, LoggerFactory
采用工厂设计模式,内部维护一个 Map
缓存所有生成的 Logger
实例信息: Map<String, Logger> loggerCache = new ConcurrentHashMap()
。
Logger
是有层次关系的,我们可一般性的理解为包名之间的父子继承关系。每个 Logger
通常以 class全限名称
为其名称。子 Logger
通常会从父 Logger
继承 Logger级别
、 Appender
等信息。
日志框架无论 Log4j
还是 Logback
,虽然它们功能完备,但是各自 API
相互独立,并且各自为政。当应用系统在团队协作开发时,由于工程师人员可能有所偏好,因此,可能导致一套系统同时出现多套日志框架情况。
其次,最流行的日志框架基本上基于实现类编程,而非接口编程,因此,暴露一些无关紧要的细节给用户,这种耦合性是没有必要的。
诸如此类的原因,开源社区提供 统一日志API
框架,最为流行的是:
apache commons-logging JCL log4j java logging
slf4j log4j log4j2 java logging logback
统一日志 API
,即日志门面接口层,直白点讲:提供了操作日志的接口,而具体实现交由 Logback
、 Log4j
等日志实现框架,这样就可以实现程序与具体日志框架间的解耦,对于底层日志框架的改变,并不影响到上层的业务代码,可以灵活切换日志框架。
现在日志框架众多: slf4j
、 jcl
、 jul
、 log4j
、 log4j2
、 logback
等,它们之间存在什么样的关系,我们在开发过程中又如何选取这些日志框架呢?
首先,看下Java日志体系:
通过上图可以概括日志体系大致分为三层:日志接口门面层、绑定/桥接层以及日志实现层。
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
已经足够系统使用进行日志输出了,为啥还多此一举弄个 日志接口门面层
和 绑定/桥接层
?看下图:
系统A
集成了 模块A
、 模块B
、 模块C
三个模块,但是这三个模块使用了不同的日志实现框架,现在 系统A
相当于同时存在了三个日志框架,那如何进行配置呢?每个框架都构建一个配置文件这种肯定是不行的,没法进行统一管理,日志较为混乱。
现在看下如何解决上述问题:
模块A
、 模块B
、 模块C
采用 slf4j
日志接口框架,而非具体日志实现类,具体使用哪种日志实现框架是由 系统A
配置决定的, 系统A
把 slf4j
绑定到 logback
,则统一采用 logback
日志框架, slf4j
绑定到 log4j
则统一采用 log4j
日志框架。 日志接口 --> 日志绑定 --> 日志实现
,日志接口和日志实现进行了解耦,模块只关注接口不关注实现,具体采用哪种实现是由其所在的系统环境决定,这样就可以实现日志的统一配置和管理。
对于上述解决方案,如果 模块A
、 模块B
、 模块C
是新开发统一采用 slf4j
日志接口框架没问题,但是对于旧系统,比如 模块B
、 模块C
都是很久之前开发的模块,分别采用了不同的日志实现框架,见下图:
如果 系统A
把 slf4j
绑定到 logback
日志框架上,但是 模块B
、 模块C
由于没有采用 slf4j
,绑定对于它们来说是无效的,这时候就要使用 桥接
。
桥接的大致结构如上图,通过桥接把 log4j
、 jdk log
等日志实现框架桥接到 slf4j
上,由于 slf4j
又被绑定到了 logback
上,则 模块B
和 模块C
最终会被 logback
纳管,而不是 log4j
和 jdk log
,同样可以实现日志统一配置管理。
以上就是项目开发中经常遇到的问题,以及绑定和桥接之间的区别。
spring
体系中日志框架
Spring Framework 4.X
及之前的版本,都是使用的 标准版JCL
日志框架,该依赖由 spring-core
间接引入。 Spring
框架的日志信息都是使用 JCL
标准接口来进行输出。下面说下项目中常碰到的三种情况:
log4j commons-logging log4j jcl log4j
log4j2 commons-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
,无需添加额外依赖包,可自动检测绑定到 Log4j2
、 SLF4J
。
SpringBoot框架
springboot-1.X
- springboot-2.X
:
从 SpringBoot
框架可以看出,默认采用 SLF4J+Logback
组合的日志框架,通过 桥接模式
将其它日志框架桥接到 SLF4J
上。
SLF4J(Simple Logging Facade For Java)
是一个为 Java
程序提供日志输出的统一接口,并不是一个具体的日志实现方案,就像我们经常使用的 JDBC
一样,只是了一些标准规范接口。因此,单独的 SLF4J
是不能工作的,它必须搭配其他具体的日志实现方案。
SLF4J
和 Logback
是同一个作者开发的,所以 Logback
天然与 SLF4J
适配,不需要引入额外适配库。
这里还有个比较有意思的事情, SLF4J
项目提供了很多适配库、桥接库,唯独没有提供对 Log4j2
的适配库和桥接库,不过 Apache Logging
项目组自己开发了: log4j-slf4j-impl
和 log4j-to-slf4j
。
Jakarta commons-logging
简称 JCL
,是 apache
提供的一个通用日志门面接口,最后版本更新停留在 2014年
,且默认只能提供对 Log4j
、 Java Logging
进行适配。
JCL
已慢慢淡出人们的视线,一些历史遗留项目也开始慢慢由 JCL
转向 SLF4J
,如: Spring 5.0
开始没有再依赖原生的 JCL
框架, SpringBoot
默认采用 SLF4J+Logback
。 SLF4J
已经成为了 Java日志组件
的明星选手,可以完美替代 JCL
,使用 JCL
桥接库也能完美兼容一切使用 JCL
作为日志门面的类库,现在的新系统已经没有不使用 SLF4J
作为 统一日志API接口层
的理由了。
SLF4J
和 JCL
对比,二者最大区别在于它们的绑定机制的不同,这也决定了为什么 JCL
会被慢慢的淘汰掉的根本原因。
1、 slf4j
定义好两个接口规范:
public interface LoggerFactoryBinder {
//获取一个ILoggerFactory实现类,采用工厂设计模式创建Logger
public ILoggerFactory getLoggerFactory();
public String getLoggerFactoryClassStr();
}
public interface ILoggerFactory {
public Logger getLogger(String name);
}
第一个接口 LoggerFactoryBinder
定义绑定类,如果日志框架需要和 slf4j
进行绑定,就要提供一个该接口实现类,并且名称是 StaticLoggerBinder
,这样,在 slf4j
模块中,使用 StaticLoggerBinder.getSingleton();
就可以获取到这个绑定类,进而通过 StaticLoggerBinder
绑定类的 getLoggerFactory()
获取到 Logger
生产工厂 ILoggerFactory
。
注意:这里的绑定机制利用到了类加载原理,如果存在多个绑定类 StaticLoggerBinder
,根据类路径的前后顺序,只有有一个会被加载进来,这个加载进来的就实现了绑定。
2、 ILoggerFactory
也是 slf4j
模块提供的一个接口,因为各个日志框架中 LoggerFactory
不统一,所以 slf4j
提供一个接口,让各个日志框架把自己的 LoggerFactory
包装成 ILoggerFactory
接口,这样 slf4j
模块下就可以统一使用。这里利用到的是设计模式中的:适配模式。系统间对接比较常用的一种设计模式,系统间接口不统一,通过适配模式实现一致。
3、可以看下, slf4j
和 log4j
绑定使用 slf4j-log4j12.jar
,这个模块下 StaticLoggerBinder
实现见下:
public class StaticLoggerBinder implements LoggerFactoryBinder {
private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
public static final StaticLoggerBinder getSingleton() {
return SINGLETON;
}
public static String REQUESTED_API_VERSION = "1.6.99";
private static final String loggerFactoryClassStr = Log4jLoggerFactory.class.getName();
private final ILoggerFactory loggerFactory;
private StaticLoggerBinder() {
loggerFactory = new Log4jLoggerFactory();
try {
@SuppressWarnings("unused")
Level level = Level.TRACE;
} catch (NoSuchFieldError nsfe) {
Util.report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version");
}
}
public ILoggerFactory getLoggerFactory() {
return loggerFactory;
}
public String getLoggerFactoryClassStr() {
return loggerFactoryClassStr;
}
}
4、 StaticLoggerBinder
:静态绑定,这个静态是相对于 JCL
所使用的动态绑定来说的,为什么说是静态的呢?因为你如果要绑定,需要在环境中添加绑定相关的jar,这样slf4j就可以加载到绑定包中的 StaticLoggerBinder
类实现绑定。
接口和实现类之间采用一种松耦合的设计,有利于灵活的扩展,但是在使用时有需要一种技术把它们关联起来,这是软件设计中比较常用到的设计思想, JDK 1.6
对此专门提供了一种技术: SPI
。 SLF4J
从 1.8版本
起,也开始使用 SPI
方式实现绑定,而不再采用通过寻找指定类 StaticLoggerBinder
的方式进行绑定。下面代码就是 slf4j-1.8
中使用 SPI
进行绑定核心代码:
private static List<SLF4JServiceProvider> findServiceProviders() {
ServiceLoader<SLF4JServiceProvider> serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class);
List<SLF4JServiceProvider> providerList = new ArrayList<SLF4JServiceProvider>();
for (SLF4JServiceProvider provider : serviceLoader) {
providerList.add(provider);
}
return providerList;
}
SLF4JServiceProvider
就是类似于上面的 LoggerFactoryBinder
接口,通过它可以获取到 ILoggerFactory
,这样其它日志框架和 slf4j
进行集成时只需要提供一个 SLF4JServiceProvider
接口的实现类即可,不再要求必须是像之前固定名称必须是: StaticLoggerBinder
,固定名称带来的一个问题是包路径也要一致,无形中存在侵入性,而使用 SPI
方式更加的灵活。比如我们常用到的 JDBC
也使用到 SPI
,感兴趣的可以多了解下,对系统设计还是比较实用的一种技术。
JCL
采用动态绑定机制,缺点是容易引发混乱,在一个复杂甚至混乱的依赖环境下,确定当前正在生效的日志服务是很费力的,特别是在程序开发和设计人员并不理解 JCL
的机制时。
JCL
动态绑定的核心逻辑位于 LogFactoryImpl
类的 discoverLogImplementation
方法中如下代码块:
for(int i=0; i<classesToDiscover.length && result == null; ++i) {
/**
createLogFromClass()核心逻辑:通过Class.forName()加载适配器的类模板,
然后调用Constructor.newInstance()构建适配器类实例
*/
result = createLogFromClass(classesToDiscover[i], logCategory, true);
}
其中 classesToDiscover
数组的中定义了可以使用的适配器类,见下:
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
模块中会有判断,当前项目中是否存在 Log4j
的 API
,如果有就直接和 Log4j
进行绑定;如果没有,则继续向下查找,是否存在 JDK Log
相关 API
,如果有就绑定;如果 JDK Log
也没有,则提供一个 SimpleLog
默认实现,该实现什么也不做,输出的日志直接会被丢弃,什么也看不到。
相较于 JCL
的 动态绑定机制
, SLF4J
则简单得多,采用 静态绑定机制
,可能你还没有很好理解这两者的本质区别,看下图:
JCL
框架自动检查当前环境中是否存在相关日志 API
,如果有就绑定,注意它内部有个固定的绑定顺序,这种所谓的动态绑定很容易出现问题,特别是系统较大可能会存在很多日志框架,就会出现混乱,不够灵活,这就导致了为啥 JCL
已经被慢慢淘汰掉。
而 slf4j
采用的静态绑定,不是直接和日志框架进行绑定,而是中间多了一个环节:绑定类,它就像一个开关一样,关键是可以进行控制,比如想和 log4j2
进行绑定,就添加 log4j-slf4j-impl.jar
,开关就会打开进行绑定。 slf4j
不管是采用 StaticLoggerBinder
还是后面采用的 SPI
,始终有个绑定类控制绑定关系。
对 Java
日志组件选型的建议
API SLF4J slf4j-api logback-classic
Log4j2
,否则都采用 Logback
SpringBoot 2.0 logback+slf4j
再一个就是对 slf4j
和 jcl
两种日志框架绑定机制的分析,学习了接口和实现类松耦合关系最后又是如何在运行时进行绑定,或许可以为我们以后的系统设计提供些思路,从而构建出更加灵活的、可扩展的应用。
长按识别关注, 持续输出原创