乐趣区

走进JavaWeb技术世界9Java日志系统的诞生与发展

微信公众号【黄小斜】大厂程序员,互联网行业新知,终身学习践行者。关注后回复「Java」、「Python」、「C++」、「大数据」、「机器学习」、「算法」、「AI」、「Android」、「前端」、「iOS」、「考研」、「BAT」、「校招」、「笔试」、「面试」、「面经」、「计算机基础」、「LeetCode」等关键字可以获取对应的免费学习资料。

                     

一个著名的日志系统是怎么设计出来的?

1 前言

Java 帝国在诞生之初就提供了集合、线程、IO、网络等常用功能,从 C 和 C ++ 领地那里吸引了大量程序员过来加盟,但是却有意无意地忽略了一个重要的功能:输出日志。

对于这一点,IO 大臣其实非常清楚,日志是个很重要的东西,因为程序运行起来以后,基本上就是一个黑盒子,如果程序的行为和预料的不一致,那就是出现 Bug 了,如何去定位这个 Bug 呢?   

臣民们能用的工具有两个,第一个就是单步调试,一步步地跟踪,查看代码中变量的值,这种办法费时费力,并且只能在程序员的机器上才能用。

第二种就是在特定的地方打印日志,通过日志的输出,帮助快速定位。尤其是当代码在生产环境上跑起来以后,日志信息更是必不可少,要不然出了状况两眼一抹黑,上哪儿找问题去?总不能让臣民们把自己变成一个线程进入系统来执行吧?

但是 IO 大臣也有自己的小算盘:日志嘛,用我的 System.out.println(…..) 不就可以了?!我还提供了 System.err.println 不是?

在 IO 大臣的阻挠下,从帝国的第一代国王到第三代国王,都没有在 JDK 中提供日志相关的工具包,臣民们只好忍受着去使用 System.out.println 去输出日志,把所有的信息都输出到控制台,让那里变成一堆垃圾。

2 张家村

 张家村的电子商务系统也不能幸免,自然也遇到了日志的问题。经验丰富的老村长已经烦透了 System.out.println 所输出的大量难于理解的无用信息,看着村民民整天苦逼地和这些 System.out 做斗争,他找来了小张,命令他设计一个通用的处理日志的系统。

小张在消息队列和 JMS 的设计上花了不少功夫,积累了丰富的经验,从那以后一直都是实现业务代码,一直都是 CRUD,张二妮整天笑话自己是 HTML 填空人员,这一回一定要让她看看自己的设计功力!

(传送门:《Java 帝国之消息队列》,《Java 帝国之 JMS 的诞生》)

老村长给小张下达的需求是这样的:

1.  日志消息除了能打印到控制台,还可以输出到文件,甚至可以通过邮件发送出去(例如生成环境出错的消息)

2. 日志内容应该可以做格式化,例如变成纯文本,XML, HTML 格式等等

3. 对于不同的 Java class,不同的 package,还有不同级别的日志,应该可以灵活地输出到不同的文件中。

例如对于 com.foo 这个 package,所有的日志都输出到 foo.log 文件中

对于 com.bar 这个 package,所有文件都输出到 bar. log 文件中

对于所有的 ERROR 级别的日志,都输出到  errors.log 文件中

4. 能对日志进行分级,有些日志纯属 debug,在本机或者测试环境使用,方便程序员的调试,生产环境完全不需要。有些日志是描述错误 (error) 的,在生产环境下出错的话必须要记录下来,帮助后续的分析。

小张仔细看了看,拍着胸脯对老村长说:“没问题,明天一定让您老看到结果。”

3 小张的设计

老村长走了以后,小张开始分析需求,祭出“面向对象设计大法”,试图从村长的需求中抽象出一点概念。

首先要记录日志,肯定需要一个类来表达日志的概念,这个类至少应该有两个属性,一个是时间戳,一个是消息本身,把它叫做 LoggingEvent 吧,记录日志就像记录一个事件嘛。

其次是日志可以输出到不同的地方,控制台、文件、邮件等等,这个可以抽象一下,不就是写到不同的目的地吗?可以叫做 LogDestination?

嗯,还是简单一点,叫做 Appender 吧,暗含了可以不断追加日志的意思。

至于第二条的日志内容可以格式化,完全可以比葫芦画瓢,定义一个 Formatter 接口去格式化消息。

对了,Appender 应该引用 Formatter,这样以来就可以对 LoggingEvent 记录格式化以后再发送。

第三条需求把小张给难住了,不同的 class, package 输出的目的地不同?“目的地”这个概念是由 Appender 来表达的,难道让不同的 class, package 和 Appender 关联?不不,不能这样!

还需要一个新的概念,这个概念是什么?

从用户角度想一下,村民们要想获取日志,必须得先获取个什么东西,这个东西是不是可以称为 Logger 啊?灵感的火花就闪了那么一下就被小张抓住了:获取 Logger 的时候要传入类名或者包名!

这样一来,不同的 class, package 就区分开了,然后让 Logger 和 Appender 关联,灵活地设置日志的目的地,并且一个 Logger 可以拥有多个 Appender,同一条日志消息可以输出到多个地方,完美!

小张迅速地画出了核心类的类图

还算漂亮,小张陶醉着自我欣赏了一下。

再接再厉,把第四条需求也设计一下,日志要分级,这个简单,定义一个 Priority 的类,里边定义 5 个常量 DEBUG, INFO, WARN, ERROR, FATAL,表示 5 个不同的级别就 OK 了。当然这我 5 个级别有高低之分,DEBUG 级别最低,FATAL 级别最高。

还可以给 Logger 增加一些辅助编程的方法,如 Logger.debug(….) , Logger.info(…)  , Logger.warn(…) 等等,这样村民们将来就可以轻松地输出各种级别的日志了。

等一下,老村长还说过“对于所有的 ERROR 级别的日志,都输出到  errors.log 文件中”类似这样的需求,好像给忽略了。

这也好办嘛,只要在 Appender 上增加一个属性,就叫做 Priority,如果用户要输出的日志是 DEBUG 级别,但是有个 FileAppender 的 Priority 是 ERROR 级别,那这个日志就不用在这个 FileAppender 中输出了,因为 ERROR 级别比 DEBUG 级别高嘛。

同理,在 Logger 类上也可以增加一个 Priority 的属性,用户可以去设置,如果一个 Logger 的 Priority 是 ERROR,而用户调用了这个 Logger 的 debug 方法,那这个 debug 的消息也不会输出。

小张全心全意地投入到设计当中,一看时间,都快半夜了,赶紧休息,明天向村长汇报去。

4 正交性

第二天,小张给老村长展示了自己设计的 LoggerEvent, Logger , Appender, Formatter, Priority 等类和接口,老村长捻着胡子满意地点点头:“不错不错,与上一次相比有巨大的进步。你知不知道我在需求中其实给了你引导?”

“引导?什么引导?”

“就是让你朝着正交的方向去努力啊”

“正交?”

‘“如果你把 Logger, Appender, Formatter 看成坐标系中的 X 轴,Y 轴,Z 轴,你看看,这三者是不是可以独立变化而不互相影响啊?”

“我赛,果然如此,我可以任意扩展 Appender 接口而影响不到 Logger 和 Formatter,无论有多少个 Logger 都影响不了 Appender 和 Formatter,这就是正交了?”

“是啊,当你从系统中提取出正交的概念的时候,那就威力无比了,因为变化被封装在了一个维度上,你可以把这些概念任意组合,而不会变成意大利面条似的代码。”

听到村长做了理论的升华,小张兴奋得直搓手。

“好吧,你把这个设计实现了吧,对了,你打算叫什么名字?”村长问道

“我打算把他叫做 Log4j , 意思是 Log for Java”

“不错,就这么定了吧”

5Log4j

小张又花了两个月的时间把 Log4j 开发了出来,由于 Log4j 有着良好的设计,优异的性能,不仅仅是张家村的人在用,Java 帝国的很多村镇、部落都爱上了它。

后来张家村把 Log4j 在 Apache 部落开源了,这下子吸引了无数的人无偿帮助测试它,扩展它,改进它,很快就成了帝国最流行的日志工具。

张家村建议帝国把 Log4j 纳入到 JDK 中,帝国那效率低下的官僚机构竟然拒绝了。消息传到了 IO 大臣的耳朵里,他不由的扼腕叹息:唉,失去了一次极好的招安机会啊。现在唯一的办法就是赶紧上奏皇上,在官方也提供一套,争取让臣民们使用官方版本。

到了第四代国王(JDK1.4),臣民们终于看到了帝国提供的 java.util.logging 包,也是用来记录日志的,并且其中的核心概念 Logger, Formatter, Handler 和 Log4j 非常相似,只是为时已晚,Log4j 早已深入人心了,不可撼动了。

6 尾声

Log4j 在 Apache 开源以后,小张也逐渐地有点落寞,他闲不住又写了一个工具,叫做 logback, 有了之前的经验,这 logback 比 log4j 还要快。

如今的日志世界有了很多的选择,除了 java.util.logging, log4j 之外,还有 logback,tinylog 等其他工具。

小张想了想,这么多日志工具,用户如果想切换了怎么办?不想用 log4j 了,能换到 logback 吗?

我还是提供一个抽象层吧,用户用这个抽象层的 API 来写日志,底层具体用什么日志工具不用关心,这样就可以移植了。

小张把这抽象层就叫做 Simple Logging Facade for Java,简称 SLF4J。

对于 Log4j , JDK logging, tinylog 等工具,需要一个适配层,把 SLF4J 的 API 转化成具体工具的调用接口。

由于 Logback 这个工具也是出自小张之手,直接实现了 SLF4J 的 API,所以连适配层都不需要了,用起来速度飞快,效率最高,SLFJ4+Logback 成为了很多人的最爱,大有超越 Apache Common Logging + Log4j 之势。

后记:本文主要想讲一下日志工具的历史和现状,尤其是 Log4j 核心的设计理念。

文中的小张其实就是 Ceki Gülcü,他开发了 Log4j , logback, 以及 slfj4,为 Java 的日志事业做出了卓越的贡献。

日志?聊一聊 slf4j 吧

转自 https://juejin.im/post/5ad1cc…

作为一个 Java 程序员,肯 https://juejin.im/post/5ad1cc…,无论项目大小,日志记录都是必须的;因为好的日志可以很容易的帮助我们定位一些生产问题。

我怀念的是 无话不说 System.out.println(“ 这里是重要的日志 ”);

我怀念的是 一起作梦 System.err.println(“ 这里是错误的日志 ”);

对于日常开发来说,其实 System.out.println 挺好用的,但是为什么在实际的开发应用中不使用这个来输出日志呢?

System.out.println()除了使用方便一点之外,其他感觉真的没有什么多大的用处。方便在哪儿呢?在 Eclipse 中你只需要输入 syso【IDEA 中输出 sout 即可】,然后按下代码提示键,这个方法就会自动出来了,相信这也是很多 Java 新手对它钟情的原因,因为我一开始也是这么干的,直到 …【此处省略泪水】。那缺点又在哪儿了呢?这个就太多了,比如日志打印不可控制、打印时间无法确定、不能添加过滤器、日志没有级别区分……

记得我最开始接触的是 log4j,log4j 作为 Apache 的一个开放源代码的项目,通过使用 log4j,我们可以控制日志信息输送的目的地是控制台、文件等我们期望它输出到的地方;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。重要的是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

确实,log4j 作为最先比较流行的日志框架,给我们在应用开发和维护带来了很大的便捷,那么为什么这样优秀的框架最后还是慢慢的走下“神坛”呢?而逐渐被 logback 代替呢?下面是摘自网上一位大佬对于这个问题的解释:

无论从设计上还是实现上,Logback 相对 log4j 而言有了相对多的改进。不过尽管难以一一细数,这里还是列举部分理由为什么选择 logback 而不是 log4j。牢记 logback 与 log4j 在概念上面是很相似的,它们都是有同一群开发者建立。

  • 更快的执行速度

    基于我们先前在 log4j 上的工作,logback 重写了内部的实现,在某些特定的场景上面,甚至可以比之前的速度快上 10 倍。在保证 logback 的组件更加快速的同时,同时所需的内存更加少。

  • 充分的测试

    Logback 历经了几年,数不清小时数的测试。尽管 log4j 也是测试过的,但是 Logback 的测试更加充分,跟 log4j 不在同一个级别。我们认为,这正是人们选择 Logback 而不是 log4j 的最重要的原因。人们都希望即使在恶劣的条件下,你的日记框架依然稳定而可靠。

slf4j log4j logback

slf4j:The Simple Logging Facade for Java 即 java 的简单日志门面

简答的讲就是 slf4j 是一系列的日志接口,slf4j 作为一个日志的抽象行为存在,但是并没有提供真正的实现。

slf4j 为各种日志框架提供了一个统一的界面,使用户可以用统一的接口记录日志,但是动态地决定真正的实现框架。logback,log4j,common-logging 等框架都实现了这些接口。

slf4j 源码分析

想了很久都不知道从哪里开头写比较合适,slf4j 包中总共 29 个类【1.7.21 版本】,不可能一一列举。所以就从我们熟知的这个语句来说。

private static final Logger logger =
LoggerFactory.getLogger(DemoTest.class);

上面这段代码其实就是我们平时在代码里面申明一个日志对象的常用方式。

先来看下 Logger 接口提供的方法;

package org.slf4j;

public interface Logger {
    // 根 Logger
    String ROOT_LOGGER_NAME = "ROOT";
    String getName();
    // 判断记录器 Trace 跟踪是否激活;Trace 跟踪激活后一般会打印比较详细的信息。boolean isTraceEnabled();
    //trace 级别
    void trace(String var1);
    void trace(String var1, Object var2);
    void trace(String var1, Object var2, Object var3);
    void trace(String var1, Object... var2);
    void trace(String var1, Throwable var2);
    boolean isTraceEnabled(Marker var1);
    void trace(Marker var1, String var2);
    void trace(Marker var1, String var2, Object var3);
    void trace(Marker var1, String var2, Object var3, Object var4);
    void trace(Marker var1, String var2, Object... var3);
    void trace(Marker var1, String var2, Throwable var3);
    // 进行预先判断,提升系统性能
    boolean isDebugEnabled();
    void debug(String var1);
    void debug(String var1, Object var2);
    void debug(String var1, Object var2, Object var3);
    void debug(String var1, Object... var2);
    void debug(String var1, Throwable var2);
    boolean isDebugEnabled(Marker var1);
    void debug(Marker var1, String var2);
    void debug(Marker var1, String var2, Object var3);
    void debug(Marker var1, String var2, Object var3, Object var4);
    void debug(Marker var1, String var2, Object... var3);
    void debug(Marker var1, String var2, Throwable var3);
    //info 级别
    boolean isInfoEnabled();
    void info(String var1);
    void info(String var1, Object var2);
    void info(String var1, Object var2, Object var3);
    void info(String var1, Object... var2);
    void info(String var1, Throwable var2);
    boolean isInfoEnabled(Marker var1);
    void info(Marker var1, String var2);
    void info(Marker var1, String var2, Object var3);
    void info(Marker var1, String var2, Object var3, Object var4);
    void info(Marker var1, String var2, Object... var3);
    void info(Marker var1, String var2, Throwable var3);
    //warn 级别
    boolean isWarnEnabled();
    void warn(String var1);
    void warn(String var1, Object var2);
    void warn(String var1, Object... var2);
    void warn(String var1, Object var2, Object var3);
    void warn(String var1, Throwable var2);
    boolean isWarnEnabled(Marker var1);
    void warn(Marker var1, String var2);
    void warn(Marker var1, String var2, Object var3);
    void warn(Marker var1, String var2, Object var3, Object var4);
    void warn(Marker var1, String var2, Object... var3);
    void warn(Marker var1, String var2, Throwable var3);
    //error 级别
    boolean isErrorEnabled();
    void error(String var1);
    void error(String var1, Object var2);
    void error(String var1, Object var2, Object var3);
    void error(String var1, Object... var2);
    void error(String var1, Throwable var2);
    boolean isErrorEnabled(Marker var1);
    void error(Marker var1, String var2);
    void error(Marker var1, String var2, Object var3);
    void error(Marker var1, String var2, Object var3, Object var4);
    void error(Marker var1, String var2, Object... var3);
    void error(Marker var1, String var2, Throwable var3);
}

isXXXXEnabled

以 isDebugEnabled 来说明下这个 isXXXXEnabled 方法的作用。

logger.debug(“the debug msg is ” +doMethod());

假设我们的日志级别设置为 info, 那这句话不会输出日志,但这个方法还是会调用(预判断作用)。要调用这个方法,必须提供参数。doMethod()方法返回的结果就是参数的一部分。doMethod()要执行 n 秒钟,n 秒钟后,进入到 debug()方法里;

但是日志级别为 info。结果是日志虽然没有输出,却花费了 n 秒钟来构造参数。很显然这里得不偿失的。尽管实际应用中几乎不可能有这种花 n 秒钟来构造这样一个参数的情况,但如果并发数大的话,这样写还是会影响系统的性能的。这个时候,就应该写成:

if(logger.isDebugEnabled()){logger.debug("the debug msg is" + doMethod());
} 

接下来说 LoggerFactory 这个类;先从 getLogger 这个方法为入口来看下:

public static Logger getLogger(String name) {ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
}

public static Logger getLogger(Class<?> clazz) {Logger logger = getLogger(clazz.getName());
    if (DETECT_LOGGER_NAME_MISMATCH) {Class<?> autoComputedCallingClass = Util.getCallingClass();
        if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(), autoComputedCallingClass.getName()));
            Util.report("See http://www.slf4j.org/codes.html#loggerNameMismatch for an explanation");
        }
    }

    return logger;
}

getLogger 方法提供了两种重载方式,一种是传入一个 name,用于标注当前日志的名字。另外一个是提供一个 Class 对象,其实里面也是通过 clazz.getName()来作为日志的名称。

从上面的代码中可以比较明显的看到 ILoggerFactory 这个接口。

package org.slf4j;

public interface ILoggerFactory {Logger getLogger(String var1);
}

ILoggerFactory 这个接口实际上就是为不同接入的日志实现提供了统一的顶层类型;每个日志框架都需要实现 ILoggerFactory 接口,来说明自己是怎么提供 Logger 的。像 log4j、logback 能够提供父子层级关系的 Logger,就是在 ILoggerFactory 的实现类里实现的。同时,它们也需要实现 Logger 接口,以完成记录日志。

logback 中的实现

public class LoggerContext extends ContextBase implements
ILoggerFactory, LifeCycle

上面提到过,对于不同的日志框架的实现都实现了 ILoggerFactory 接口。

 @Override
public final Logger getLogger(final String name) {if (name == null) {throw new IllegalArgumentException("name argument cannot be null");
    }

    // if we are asking for the root logger, then let us return it without
    // wasting time
    if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) {return root;}

    int i = 0;
    Logger logger = root;

    // check if the desired logger exists, if it does, return it
    // without further ado.
    Logger childLogger = (Logger) loggerCache.get(name);
    // if we have the child, then let us return it without wasting time
    if (childLogger != null) {return childLogger;}

    // if the desired logger does not exist, them create all the loggers
    // in between as well (if they don't already exist)
    String childName;
    while (true) {int h = LoggerNameUtil.getSeparatorIndexOf(name, i);
        if (h == -1) {childName = name;} else {childName = name.substring(0, h);
        }
        // move i left of the last point
        i = h + 1;
        synchronized (logger) {childLogger = logger.getChildByName(childName);
            if (childLogger == null) {childLogger = logger.createChildByName(childName);
                loggerCache.put(childName, childLogger);
                incSize();}
        }
        logger = childLogger;
        if (h == -1) {return childLogger;}
    }
}

关于 logback 的源码可以参考这个系列的文章:logback 源码系列文章

Java 日志框架梳理-SLF4J+log4j

转自 https://blog.csdn.net/xktxoo/…

log4j 的配置文件是用来设置纪录器的级别、存放位置和布局的,可以通过 Java 属性文件(key=value)格式设置或 XML 格式设置。log4j 配置文件元素简介:

Logger

Logger 是一个允许应用纪录日志的对象,开发者不必考虑输出位置。应用可将具体需要打印的信息通过一个 Object 传递。Logger 是命名了的实体,每个 Logger 相互独立,它们的名字大小写敏感且遵循层次化命名规则:

如果 logger 的名称带上一个点号后是另外一个 logger 的名称的前缀,则前者被称为后者的祖先。如果 logger 与其后代 logger 之间没有其他祖先,则前者就被称为子 logger 之父。

根 logger(rootLogger)位于 logger 等级的最顶端,它是每个层次等级的共同始祖。获取根 logger 的方式:

Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
  • 1

logger 可被分配级别,如果 logger 没有被分配级别,那它将从没有分配级别的最近祖先继承级别,直至根 logger,默认情况下,根 logger 级别为 DEBUG。

logger 可通过 additivity 标识设置其 Appender 的叠加性,定义如下:

logger A 记录语句的输出会发送给 A 及其祖先的全部 Appender,如果 logger A 的某个祖先 B 设置叠加性标识为 false,则 A 的输出会发送给 A 与 B(包含 B)之间的所有 Appender,但不会发送给 B 的任何祖先 Appender

Appender

每个 Appender 可独立配置纪录日志的设备,可以是:控制台、文件、数据库、消息系统等。log4j 提供的 Appender 具体有如下几种:

类型 描述
org.apache.log4j.ConsoleAppender 控制台
org.apache.log4j.FileAppender 文件
org.apache.log4j.DailyRollingFileAppender 每天产生一个日志文件
org.apache.log4j.RollingFileAppender 文件大小到达指定尺寸的时候产生一个新的文件
org.apache.log4j.WriterAppender 将日志信息以流格式发送到任意指定的地方

Layout

Layout 也被称为 Formatters,负责对日志时间中的数据进行转换和格式化,Layout 决定了数据在一条日志记录中的最终形式。log4j 提供的 Layout 有如下几种:

类型 描述
org.apache.log4j.HTMLLayout 以 HTML 表格形式布局
org.apache.log4j.PatternLayout 可以灵活地指定布局模式
org.apache.log4j.SimpleLayout 包含日志信息的级别和信息字符串
org.apache.log4j.TTCCLayout 包含日志产生的时间、线程、类别等等信息

log4j 采用类似 C 语言中 printf 的打印格式化日志信息,打印参数如下

类型 描述
%m 输出代码中制定的消息
%p 输出日志级别
%r 输出自应用启动到出处该日志记录耗费的毫秒数
%c 输出触发该日志事件的类
%t 输出触发该日志事件的线程
%d 输出日志事件发生的时间,如:%-d{yyyy-MM-dd HH:mm:ss}
%l 输出日志发生的位置,包括类信息、线程、行数

Level

每个打印日志都可以单独指定日志级别,通过配置文件来控制输出级别。log4j 提供的日志级别如下:

类型 描述
ALL 最低级别,用于打开所有日志记录
TRACE 指定粒度比 DEBUG 更细的事件
DEBUG 指定细粒度信息事件,对调试应用程序有帮助
INFO 指定粗粒度信息事件,突出强调程序运行过程
WARN 指定具有潜在危害的情况
ERROR 指定错误事件,程序仍然允许运行
FATAL 指定非常严重的错误事件,可能导致应用程序终止
OFF 最高等级,用于关闭日志记录
    • *

SLF4J+log4j 实践

log4j 适配器绑定可添加如下 pom 依赖,添加配置后会自动拉下来两个依赖包,分别是 slf4j-api-1.7.25 和 log4j-1.2.17。

<dependency>
    <groupId>org.slf4j</groupId>
    slf4j-log4j12
    <version>1.7.25</version>
</dependency>

log4j 必须指定配置文件或默认配置。如果程序中引入了以上包,在没有编写配置文件,且没有设置默认配置器时打印日志,log4j 会报如下错误:

public class Test {public static void main(String [] args) {//BasicConfigurator.configure();
        Logger logger  = LoggerFactory.getLogger(Test.class);
        logger.debug("this is {} debug log", Test.class.getName());
        logger.error("this is {} error log", Test.class.getName());
    }
}

结果:log4j:WARN No appenders could be found for logger (com.xiaofan.test.Test).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.

通过 BasicConfigurator.configure() 可指定 log4j 默认配置器,该配置默认生成 rootLogger,并添加一个控制台 Appender,源码如下:

static public void configure() {Logger root = Logger.getRootLogger();
    root.addAppender(new ConsoleAppender(new PatternLayout(PatternLayout.TTCC_CONVERSION_PATTERN)));
  }

打印日志结果如下:

0 [main] DEBUG com.xiaofan.test.Test  -  this is com.xiaofan.test.Test debug log
2 [main] ERROR com.xiaofan.test.Test  -  this is com.xiaofan.test.Test error log

编写 log4j 配置文件有两种方式:Java 属性文件(key=value)和 XML 形式。

  • log4j.properties 

    属性配置文件范例如下:

### set log levels ###
log4j.rootLogger = debug,stdout,D,E

### 输出到控制台 ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern =  %-d{yyyy-MM-dd HH:mm:ss} %p [%c] %m%n

### 输出到日志文件 ###
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
log4j.appender.D.File = logs/log.log
log4j.appender.D.Append = true
log4j.appender.D.Threshold = DEBUG 
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss}  [%t:%r] - [%p]  %m%n

### 保存异常信息到单独文件 ###
log4j.appender.E = org.apache.log4j.DailyRollingFileAppender
log4j.appender.E.File = logs/error.log
log4j.appender.E.Append = true
log4j.appender.E.Threshold = ERROR
log4j.appender.E.layout = org.apache.log4j.PatternLayout
log4j.appender.E.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss}  [%t:%r] - [%p]  %m%n

### logger ### 
# 设置后 com.xiaofan 路径下的日志将只输出 ERROR 日志
#log4j.logger.com.xiaofan=ERROR

将 log4j.properties 文件放到工程的 resources 文件夹下,如果程序没有显示指定其他配置文件,log4j 会默认加载 log4j.properties 文件作为配置文件。可通过 PropertyConfigurator.configure()显示指定外部配置文件。

测试代码如下:

package com.xiaofan.test;

import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.PropertyConfigurator;
import org.apache.log4j.xml.DOMConfigurator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Created by Jerry on 17/7/24.
 */
public class Test {public static void main(String [] args) {PropertyConfigurator.configure(Test.class.getResource("/log4j.properties"));
        Logger logger  = LoggerFactory.getLogger(Test.class);
        logger.debug("this is {} debug log", Test.class.getName());
        logger.error("this is {} error log", Test.class.getName());
    }
}

结果:console:
    2017-07-27 09:44:50 DEBUG [com.xiaofan.test.Test]  this is com.xiaofan.test.Test debug log
    2017-07-27 09:44:50 ERROR [com.xiaofan.test.Test]  this is com.xiaofan.test.Test error log
/logs/error.log:
    2017-07-27 09:48:27  [main:1] - [ERROR]   this is com.xiaofan.test.Test error log
/logs/log.log:
    2017-07-27 09:48:27  [main:0] - [DEBUG]   this is com.xiaofan.test.Test debug log
    2017-07-27 09:48:27  [main:1] - [ERROR]   this is com.xiaofan.test.Test error log
  • log4j.xml

    XML 配置文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">

<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/' >

    
        <layout >
            <param name="ConversionPattern"
                   value="[%d{yyyy-MM-dd HH:mm:ss,SSS\} %-5p] %c{2\} [%t] - %m%n" />
        </layout>
        <filter >
            <param name="levelMin" value="debug" />
            <param name="levelMax" value="error" />
            <param name="AcceptOnMatch" value="true" />
        </filter>
    

    
        <param name="File" value="logs/error.log" />
        <param name="Append" value="true" />
        <param name="threshold" value="error" />
        <param name="MaxBackupIndex" value="10" />
        <layout >
            <param name="ConversionPattern" value="%p (%c:%L)- %m%n" />
        </layout>
    

    
        <param name="File" value="logs/log.log" />
        <param name="DatePattern" value="'.'yyyy-MM-dd'.log'" />
        <layout >
            <param name="ConversionPattern"
                   value="[%d{MMdd HH:mm:ss SSS\} %-5p] [%t] %c{3\} - %m%n" />
        </layout>
    

    <logger name="mylogger" additivity="true">
        <level value="debug" />
        
    </logger>

    <root>
        <priority value ="debug"/>
        
        
        
    </root>

</log4j:configuration>

将 log4j.xml 文件放到工程的 resources 文件夹下,如果程序没有显示指定其他配置文件,log4j 会默认加载 log4j.xml 文件作为配置文件。可通过 DOMConfigurator.configure()显示制定外部配置文件。

测试代码如下:

public class Test {public static void main(String [] args) {DOMConfigurator.configure(Test.class.getResource("/log4j.xml"));
        Logger logger  = LoggerFactory.getLogger(Test.class);
        logger.debug("this is {} debug log", Test.class.getName());
        logger.error("this is {} error log", Test.class.getName());
    }
}

结果:控制台:[2017-07-27 12:43:20,474 DEBUG] test.Test [main] -  this is com.xiaofan.test.Test debug log
    [2017-07-27 12:43:20,484 ERROR] test.Test [main] -  this is com.xiaofan.test.Test error log
/logs/error.log
    ERROR (com.xiaofan.test.Test:18)-  this is com.xiaofan.test.Test error log
/logs/log.log
    [0727 12:50:04 829 DEBUG] [main] xiaofan.test.Test -  this is com.xiaofan.test.Test debug log
    [0727 12:50:04 830 ERROR] [main] xiaofan.test.Test -  this is com.xiaofan.test.Test error log

 一位阿里 Java 工程师的技术小站。作者黄小斜,专注 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点 Docker、ELK,同时也分享技术干货和学习经验,致力于 Java 全栈开发!(关注公众号后回复”Java“即可领取 Java 基础、进阶、项目和架构师等免费学习资料,更有数据库、分布式、微服务等热门技术学习视频,内容丰富,兼顾原理和实践,另外也将赠送作者原创的 Java 学习指南、Java 程序员面试指南等干货资源)

退出移动版