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

时间:2024-08-17 17:35:56

本文转自【码农翻身】

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

# 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帝国之消息队列](https://mp.weixin.qq.com/s?__biz=MzAxOTc0NzExNg==&mid=2665513507&idx=1&sn=d6db79c1ae03ba9260fb0fb77727bb54&chksm=80d67a60b7a1f376e7ad1e2c3276e8b565f045b1c7e21ef90926f69d99f969557737eb5d8128&scene=21#wechat_redirect)》,《[Java帝国之JMS的诞生](https://mp.weixin.qq.com/s?__biz=MzAxOTc0NzExNg==&mid=2665513515&idx=1&sn=380bb1cb56d4151fd3acc5aa86f1da9a&chksm=80d67a68b7a1f37e3d98fe4495eab4db097eedd695c99fbd8704cc0464595842c4da598b99e3&scene=21#wechat_redirect)》)

![](https://img-blog.****.net/20171023220049639?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamlhbmd6aGV4aQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)

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

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吧, 暗含了可以不断追加日志的意思。

![](https://img-blog.****.net/20171023220049639?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamlhbmd6aGV4aQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)

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

![](https://img-blog.****.net/20171023220551618?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamlhbmd6aGV4aQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)

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

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

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

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

![](https://img-blog.****.net/20171023220701264)

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

![](https://img-blog.****.net/20171023220811002)

小张迅速地画出了核心类的类图![](https://img-blog.****.net/20171023221105015)

![](https://img-blog.****.net/20171023220918480)

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

再接再厉, 把第四条需求也设计一下,日志要分级,这个简单, 定义一个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轴, 你看看,这三者是不是可以独立变化而不互相影响啊?”

![](https://img-blog.****.net/20171023221105015)

“我赛,果然如此,我可以任意扩展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。

![](https://img-blog.****.net/20171023221238826)

对于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/5ad1ccc86fb9a028c14ae528

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

> 我怀念的是 无话不说 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源码系列文章](https://link.juejin.im/?target=http%3A%2F%2Fkyfxbl.iteye.com%2Fblog%2F1160028)

# Java日志框架梳理-SLF4J+log4j

转自https://blog.****.net/xktxoo/article/details/76359366

  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程序员面试指南等干货资源)

微信公众号【Java技术江湖】一位阿里 Java 工程师的技术小站。(关注公众号后回复”Java“即可领取 Java基础、进阶、项目和架构师等免费学习资料,更有数据库、分布式、微服务等热门技术学习视频,内容丰富,兼顾原理和实践,另外也将赠送作者原创的Java学习指南、Java程序员面试指南等干货资源)

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