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
具体的实现类。
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框架
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
两种日志框架绑定机制的分析,学习了接口和实现类松耦合关系最后又是如何在运行时进行绑定,或许可以为我们以后的系统设计提供些思路,从而构建出更加灵活的、可扩展的应用。