混乱的java日志体系

日志组件是开发中最经常使用到的组件,但也是最容易被忽视的一个组件,我本身就遇到过不少次因为Log4j报错致使的应用没法启动的问题,如下作一个梳理,参考和借鉴了一些前辈的经验,并加入了一些本身的理解,相对容易看懂一些~html

1、常见日志框架

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

  • 一、log4j数据库

  • 二、logbackapache

  • 三、SLF4Jsegmentfault

  • 四、commons-loggingapi

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

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

2、框架间关系

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

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

NoClassDefFoundError的错误出现;

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

3、log4j简介

一、Logger(限定日志级别)

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

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

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

Appender容许经过配置将日志输出到不一样的地方,如控制台(Console)、文件(Files)等,能够根据天数或者文件大小产生新的文件,能够以流的形式发送到其余地方;

经常使用配置类:

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

org.apache.log4j.FileAppender(文件)

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

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

三、Logout(日志输出格式)

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

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

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

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

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

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

4、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的名字。

5、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值;


6、Appender继承关系

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

以下图展现:


7、slf4j两种使用方式

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

一、混合绑定(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 {}复制代码

绑定图:


二、桥接遗产(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的所有特性.

8、log4j与log4j2比较


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

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

一、死循环

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


二、日志重复输出

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

log4j.xml配置以下:


testlog.java测试类中:


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


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


三、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

10、参考

segmentfault.com/a/119000001…

相关文章
相关标签/搜索