混乱的java日志体系

2,669 阅读10分钟

日志组件是开发中最常用到的组件,但也是最容易被忽视的一个组件,我自己就遇到过很多次由于Log4j报错导致的应用无法启动的问题,以下做一个梳理,参考和借鉴了一些前辈的经验,并加入了一些自己的理解,相对容易看懂一些~

一、常见日志框架

目前常见的Java日志框架和facades(中文似乎不太好翻译)有一下几种:

  • 1、log4j

  • 2、logback

  • 3、SLF4J

  • 4、commons-logging

  • 5、j.u.l (即java.util.logging)

1-3是同一个作者(Ceki)所写。4被很多开源项目所用,5是Java原生库(以下用j.u.l简写来代替),但是在Java 1.4中才被引入。这么多日志库,了解他们的优劣和关系才能找到一款更加适配自己项目的框架。

二、框架间关系

如下图,common-logging与slf4j同属于日志的门面 (facade),下层可以对接具体的日志框架层。

common-logging:开发者可以使用它兼容j.u.l和log4j,相当于一个中间层,需要注意的是common-logging对j.u.l和log4j的配置兼容性并没有理想中那么好,更糟糕的是,在common-logging发布初期,使用common-logging可能会遇到类加载问题,导致

NoClassDefFoundError的错误出现;

slf4j:能够更加灵活的对接多个底层框架;

三、log4j简介

1、Logger(限定日志级别)

级别顺序为:DEBUG < INFO < WARN < ERROR < FATAL (制定当前日志的重要程度);

log4j的级别规则:只输出级别不低于设定级别的日志信息,例:loggers级别为INFO,则INFO、WARN、ERROR、FATAL级别的日志信息都会输出,而级别比INFO低的DEBUG则不会输出;

2、Appender(日志输出目的地)

Appender允许通过配置将日志输出到不同的地方,如控制台(Console)、文件(Files)等,可以根据天数或者文件大小产生新的文件,可以以流的形式发送到其他地方;

常用配置类:

org.apache.log4j.ConsoleAppender(控制台)

org.apache.log4j.FileAppender(文件)

org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)

org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)

3、Logout(日志输出格式)

用户可以根据需求格式化日志输出,log4j可以在Appenders后面附加Layouts来完成;

Layouts提供四种日志输出格式:根据HTML样式、自由指定样式、包含日志级别与信息的样式、包含日志时间/线程/类别等信息的样式;

org.apache.log4j.HTMLLayout(以HTML表格形式布局)

org.apache.log4j.PatternLayout(可以灵活地指定布局模式)

org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串)

org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等信息)

四、logger的组织结构

在这三个类中都通过Logger.getLogger(XXX.class);获取logger:


logger组织结构如下,父子关系通过 “.” 实现:


Loggers是被命名的实体,Logger的名称是大小写敏感的,并且遵循层次命名规则。命名为“com.mobile”的logger是命名为“com.mobile.log”的logger的父亲。同理,命名为“java”的logger是命名为“java.util”的logger的父亲,是命名为“java.util.Vector”的祖先。
root logger位于整个logger继承体系的最顶端,相比于普通logger它有两个特别之处:

1)、root logger总是存在。

2)、root logger不能通过名称获取。

可以通过调用Logger类的静态方法getRootLogger获取root logger对象。其它普通logger的实例可以通过Logger类的另一个静态方法getLogger获取。getLogger方法接受一个参数作为logger的名字。

五、level继承关系

logger节点的继承关系体现在level上,可以参见下面几个例子:

Example 1:这个例子中,只有root logger被分配了一个level值Proot,Proot会被其他的子logger继承:x、x.y、x.y.z


Example 2:这个例子中,所有的logger都被分配了一个level值,就不需要继承level了


Example 3:这个例子中,root、x和x.y.z三个logger分别被分配了Proot、Px和Pxyz三个level值,x.y这个logger从它的父亲那里继承level值;


Example 4:这个例子中,root和x两个logger分别被分配了Proot和Px这两个level值。x.y和x.y.z两个logger则从离自己最近的祖先x继承level值;


六、Appender继承关系

logger的additivity表示:子logger是否继承父logger的输出源 (Appender),即默认情况下子logger会继承父logger的Appender (子logger会在父logger的Appender里输出),当手动设置additivity flag为false,子logger只会在自己的Appender里输出,不会在父Appender里输出。

如下图展示:


七、slf4j两种使用方式

slf4j的使用有两种方式,一种是混合绑定(concrete-bindings), 另一种是桥接遗产(bridging-legacy).

1、混合绑定(concrete-bindings)

concrete-bindings模式指在新项目中即开发者直接使用sl4j的api来打印日志, 而底层绑定任意一种日志框架,如logback, log4j, j.u.l等.混合绑定根据实现原理,基本上有两种形式, 分别为有适配器(adapter)的绑定和无适配器的绑定.

有适配器的混合绑定是指底层没有实现slf4j的接口,而是通过适配器直接调用底层日志框架的Logger, 无适配器的绑定不需要调用其它日志框架的Logger, 其本身就实现了slf4j的全部接口.

几个混合绑定的包分别是:

  • slf4j-log4j12-1.7.21.jar(适配器, 绑定log4j, Logger由log4j-1.2.17.jar提供)

  • slf4j-jdk14-1.7.21.jar(适配器, 绑定l.u.l, Logger由JVM runtime, 即j.u.l库提供)

  • logback-classic-1.0.13.jar(无适配器, slf4j的一个native实现)

  • slf4j-simple-1.7.21.jar(无适配器,slf4j的简单实现, 仅打印INFO及更高级别的消息, 所有输出全部重定向到System.err, 适合小应用)

以上几种绑定可以无缝切换, 不需要改动内部代码. 无论哪种绑定,均依赖slf4j-api.jar.

此外, 适配器绑定需要一种具体的日志框架, 如log4j绑定slf4j-log4j12-1.7.21.jar依赖log4j.jar, j.u.l绑定slf4j-jdk14-1.7.21.jar依赖j.u.l(java runtime提供); 无适配器的直接实现, logback-classic依赖logback-core提供底层功能, slf4j-simple则不依赖其它库.

以上四种绑定的示例图如下:


关于适配器,正常使用slf4j从LoggerFactory.getLogger获取logger开始,在getLogger内部会先通过StaticLoggerBinder获取ILoggerFactory,StaticLoggerBinder则是存在具体的适配器包中的,我了解的一种实现是通过在适配器中的StaticLoggerBinder来绑定,举个例子,引用这四个slf4j-api.jar, log4j-core-2.3.jar, log4j-api-2.3.jar, log4j-slf4j-impl.jar(将slf4j转发到log4j2):


下面来分析两个典型绑定log4j (有适配器) 和logback (无适配器) 的用法.

1)log4j适配器绑定(slf4j-log4j12)
<!--pom.xml-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>${slf4j-log4j12.version}</version>
</dependency>


注意
: 添加上述适配器绑定配置后会自动拉下来两个依赖库, 分别是slf4j-api-1.7.21.jar和log4j-1.2.17.jar

基本逻辑: 用户层 <- 中间层 <- 底层基础日志框架层


2)slf4j绑定到logback-classic上
<!--pom.xml-->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>${logback-classic.version}</version>
</dependency>


注意
: 添加上述适配器绑定配置后会自动拉下来两个依赖库, 分别是slf4j-api-1.7.21.jar和logback-core-1.0.13.jar

logback-classic没有适配器层, 而是在logback-classic-1.0.13.jar的ch.qos.logback.classic.Logger直接实现了slf4j的org.slf4j.Logger, 并强依赖ch.qos.logback.core中的大量基础类:

import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.spi.LocationAwareLogger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.classic.util.LoggerNameUtil;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.spi.AppenderAttachable;
import ch.qos.logback.core.spi.AppenderAttachableImpl;
import ch.qos.logback.core.spi.FilterReply;

public final class Logger implements org.slf4j.Logger, LocationAwareLogger, AppenderAttachable<ILoggingEvent>, Serializable {}

绑定图:


2、桥接遗产(bridging-legacy)

桥接遗产用法主要针对历史遗留项目, 不论是用log4j写的, j.c.l写的,还是j.u.l写的, 都可以在不改动代码的情况下具有另外一种日志框架的能力。比如,你的项目使用java提供的原生日志库j.u.l写的, 使用slf4j的bridging-legacy模式,便可在不改动一行代码的情况下瞬间具有log4j的全部特性。说得更直白一些,就是你的项目代码可能是5年前写的, 当时由于没得选择, 用了一个比较垃圾的日志框架, 有各种缺陷和问题, 如不能按天存储, 不能控制大小, 支持的appender很少, 无法存入数据库等. 你很想对这个已完工并在线上运行的项目进行改造, 显然, 直接改代码, 把旧的日志框架替换掉是不现实的, 因为很有可能引入不可预期的bug。那么,如何在不修改代码的前提下, 替换掉旧的日志框架,引入更优秀且成熟的日志框架如如log4j和logback呢? slf4j的bridging-legacy模式便是为了解决这个痛点。

slf4j以slf4j-api为中间层, 将上层旧日志框架的消息转发到底层绑定的新日志框架上。


举例说明上述facade的使用, 以便理解。假如我有一个已完成的使用了旧日志框架commons-loggings的项目,现在想把它替换成log4j以获得更多更好的特性.
项目的maven旧配置如下:

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>${commons-logging.version}</version>
</dependency>


项目代码:

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class LogTest {
    private static Log logger = LogFactory.getLog(LogTest.class);

    public static void main(String[] args) throws InterruptedException {
        logger.info("hello,world");
    }
}


项目打印的基于commons-logging的日志显示在console上,具体如下:

十一月 20, 2017 5:52:00 下午 LogTest main
信息: hello,world


下面我们对项目改造, 将commongs-logging框架的日志转发到log4j上. 改造很简单, 我们将commongs-logging依赖删除, 替换为相应的facade(此处为jcl-over-slf4j.jar), 并在facade下面挂一个5.1的混合绑定即可.

具体来讲, 将commons-logging.jar替换成jcl-over-slf4j.jar, 并加入适配器slf4j-log412.jar(注意, 加入slf4j-log412.jar后会自动pull下来另外两个jar包), 所以实际最终只需添加facadejcl-over-slf4j.jar和混合绑定中相同的jar包slf4j-log412.jar即可.

改造后的maven配置:

<!--facade-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>${jcl-over-slf4j.version}</version>
</dependency>

<!--binding-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>${slf4j-log4j12.version}</version>
</dependency>


现在, 我们的旧项目在没有改一行代码的情况下具有了log4j的全部特性, 下面进行测试.
在resources/下新建一个log4j.properties文件, 对commongs-logging库的日志输出进行定制化:

# Root logger option
log4j.rootLogger=INFO, stdout, fout

# Redirect log messages to console
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.Threshold = INFO
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n

# add a FileAppender to the logger fout
log4j.appender.fout=org.apache.log4j.FileAppender
# create a log file
log4j.appender.fout.File=log-testing.log
log4j.appender.fout.layout=org.apache.log4j.PatternLayout
# use a more detailed message pattern
log4j.appender.fout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n


重新编译运行, console输出变为:

2017-11-20 19:26:15 INFO LogTest:11 - hello,world


同时在当前目录生成了一个日志文件:

% cat log-testing.log
INFO    2017-11-20 19:26:15,341    0    LogTest    [main]    hello,world


可见, 基于facade的日志框架桥接已经生效, 我们再不改动代码的前提下,让commons-logging日志框架具有了log4j12的全部特性.

八、log4j与log4j2比较


配置文件方式内容比较多,用得到时候可以详细查阅一下相关文档,还有log4j2相比log4j来讲,性能、代码可读性、支持日志参数化打印等方面都表现了很高的优越性。

九、日志组件可能遇到的问题

1、死循环

下图同一个颜色的两行表示不可共存的包,不要在工程中引入会形成循环的这两个包(可能不全,欢迎补充~):


2、日志重复输出

当log4j.xml中Appender:additivity设置为trueappender-ref配置了对应的appender 时,会出现重复打印的问题,光说不好理解,举个例子:

log4j.xml配置如下:


testlog.java测试类中:


结果是:在root.log和logtest.log里面分别打印了相同的日志进去


当<logger name="logTest" additivity="false">additivity置为false:root.log就没打日志了


3、slf4j的warning、error提示信息含义

1)、SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".

SLF4J: See www.slf4j.org/codes.html#… for further details.

报出错误的地方主要是slf4j的jar包,而故障码中“Failed to load 'org.slf4j.impl.StaticLoggerBinder'的意思则是“加载类文件org.slf4j.impl.StaticLoggerBinder时失败””,

Apache官方给出的解决方案是:

This error is reported when the org.slf4j.impl.StaticLoggerBinder class could not be loaded into memory. This happens when no appropriate SLF4J binding could be found on the class path. Placing one (and only one) of slf4j-nop.jar, slf4j-simple.jar, slf4j-log4j12.jar, slf4j-jdk14.jar or logback-classic.jar on the class path should solve the problem.

翻译来就是:这个错误当org.slf4j.impl.StaticLoggerBinder找不到的时候会报出,当类路径中没有找到合适的slf4j绑定时就会发生。可以放置如下jar包中的一个且有且一个jar包即可:slf4j-nop.jar、slf4j-simple.jar、slf4j-log4j12.jar、slf4j-jdk14.jar 或者 logback-classic.jar。

2)、multiple bindings were found on the class path.

slf4j是一个日志门面框架,它只能同时绑定有且一个底层日志框架,如果绑定了多个,slf4j就会有这个提示,并且会列出这些绑定的具体位置,当有多个绑定的时候,选择你想去绑定的那个,然后删掉其他的,比如:你绑定了

slf4j-simple-1.8.0-beta0.jar和

slf4j-nop-1.8.0-beta0.jar,
你最终想用的是
slf4j-nop-1.8.0-beta0.jar
,那么就删掉另外那个就好了。

我测试了一下,同时在slf4j-api.jar绑定了两个适配器:


真实的绑定是slf4j-log4j12.jar这个包里面的实现:


所以当绑定了两个包的时候,最后选择了哪个包里的实现方式,应该是按照classpath里面的顺序,这个顺序应该与classLoader的加载规则有关。

其他具体的信息可查阅:www.slf4j.org/codes.html#… for an explanation

十、参考

segmentfault.com/a/119000001…