Log4j:log4j.properties 配置解析

时间:2020-11-30 20:30:27

Log4j 三个主要组件

Loggers(记录器):记录日志的工具,程序中就是用它来记录我们想要的日志信息。

Appenders (输出源):日志输出到什么地方,可以是控制台、文件、流位置、数据库,等等。

Layouts(布局模式):日志需要记录哪些基本信息,用什么样的格式去记录展示这些信息。

一个 Logger 最少要有一个 Appender,一个 Appender 有一个 Layout。

Loggers

记录器在 Log4j 中就是 Logger 类的实例对象,下面是该类中定义的一些主要方法,也是程序中经常使用的:

package org.apache.log4j;

public class Logger {

    // Creation & retrieval methods:
public static Logger getRootLogger();
public static Logger getLogger(String name); // printing methods:
public void trace(Object message);
public void debug(Object message);
public void info(Object message);
public void warn(Object message);
public void error(Object message);
public void fatal(Object message); // generic printing method:
public void log(Level l, Object message);
}

在程序中使用也很简单,就是获取一个Logger实例,然后调用不同的方法获取不同类型的日志信息:

public class Main {

    // 获取rootLogger方法
public static Logger rootLogger = Logger.getRootLogger();
// 获取自定义Logger方法
public static Logger logger = Logger.getLogger(Main.class); public static void main(String[] args) {
logger.debug("debug log");// 记录一条debug级别的日志信息
logger.log(Level.DEBUG, "debug log");// 另一种写法
// trace info warn error fatal 都是类似的用法
} }

可以说在程序中,我们利用 Log4j 做的唯一一件事就是告诉 Log4j 我要记录什么类型的日志,我要记录的日志信息是什么。

至于其它事情:这些日志信息被记录成了什么样子?又被送到了什么地方?都是需要提前定义配置的,可以在 java 源文件中编码定义,也可以在 log4j.properties 文件中配置。最常用的是配置文件,这里就只说说 log4j.properties 配置。

下面是一个最基本的可以工作的 log4j.properties 文件,它会将日志信息输出到标准输出(控制台)。就根据这个例子来展开后面的解析:

# rootLogger
log4j.rootLogger=DEBUG, stdout # stdout appender
log4j.appender.stdout=org.apache.log4j.ConsoleAppender # stdout appender layout
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n

log4j 是配置前缀,每一行有效的配置都不能少,标志着这是属于 Log4j 的配置:

rootLogger 代表的是根 Logger 记录器,也是其它所有 Loggers 直接或间接的祖先,地位相当于 Java 类中的 java.lang.Object,基本配置格式:

log4j.rootLogger = [ level ] , appenderName1, appenderName2, …

[ level ] 定义的是日志级别LEVEL,也就是该 Logger 应该从哪个级别开始记录日志,日志本身从高到低共分八个级别:OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE、ALL,这些都定义在 org.apache.log4j.Level 类中。定义日志级别后,该 Logger 就只会记录高于或等于该级别的日志信息,低于该级别的日志信息不会被记录。常用的日志级别有四个:ERROR、WARN、INFO、DEBUG。

appenderName1, appenderName2, ... 指的是 Appenders(输出源) 的名字。

"log4j.rootLogger=DEBUG, stdout" 这句配置的意思就是 "根 Logger 记录 DEBUG 级别(包括)往上的日志,并将这些日志输出到 stdout 输出源"。

Appenders

在 Log4j 中,输出源类型就是 "org.apache.log4j.Appender" 接口的实现类,定义一个 Appender(输出源) 的格式是:

log4j.appender.appenderName=className

appenderName 是 Appender 的别名,自定义的,可以随便取,Logger 引用的就是这个名字。上面的例子中输出源名字是 stdout。

className 是 Appender 的全路径类名,Log4j 已经定义了很多的 Appender 类型,列举几个常用的 Appender 类型:

org.apache.log4j.ConsoleAppender(标准输出,控制台)
org.apache.log4j.FileAppender(文件)
org.apache.log4j.DailyRollingFileAppender(文件,每天一个)
org.apache.log4j.RollingFileAppender(文件,超出最大容量限制后另起一个文件)
org.apache.log4j.WriterAppender(以流格式发送日志信息到指定的地方)

上面的例子中,使用的就是 "org.apache.log4j.ConsoleAppender" 输出源。

到目前为止,定义了根 Logger 可以收集日志,定义了 Appender 可以把收集的日志存储到指定的地方,但是还差一样,就是日志的格式,也就是定义布局 Layout。

打个比喻:

就是我从河里抓了一条鲈鱼(日志),带回家(输出源),做成菜(布局模式),关键是要做什么菜呢?

红烧鱼?清蒸鱼?水煮鱼?酸辣鱼?(不同的布局模式)

是就用这一条干干净净的鱼做原汁原味的?还是再加点儿其它佐料做的更美味可口一些?(布局模式下更为具体的格式)

Layouts

在 Log4j 中,Layout 类型都是 "org.apache.log4j.Layout" 抽象类的子类

布局模式 Layout 是用来匹配输出源 Appender 的一个属性,也就是说,每一个输出源都要对应匹配一个布局模式,才能做出一道完整的"菜"。

Layout 配置格式:

log4j.appender.appenderName.layout=className

appenderName 是 Appender 的名字,对应某个已经定义了的 Appender。

className 是 Layout 的全路径类名,同样的,在 Log4j 中,定义了很多的 Layout 类型,常用的有:

org.apache.log4j.HTMLLayout(HTML表格布局)
org.apache.log4j.PatternLayout(类似正则,格式化输出,非常灵活,最常用的布局模式)
org.apache.log4j.SimpleLayout(简单布局,只含日志级别和日志信息字符串)
org.apache.log4j.TTCCLayout(该布局包含产生线程、日志级别、产生类、日志信息字符串等内容)

例子中使用的是 PatternLayout 布局模式,实际项目中该布局也是经常使用,所以着重说一下该布局模式的使用,其它布局原理类似,使用可参考 Log4j 源码和 API。

PatternLayout 仅是一种布局模式,定义了使用 PatternLayout 就好比是说我决定把那条鲈鱼做成"红烧鱼"了,但是红烧鱼也可以有不同的"色香味"啊。所以,还需要进一步指明该布局模式下日志的具体格式,配置如下:

log4j.appender.appenderName.layout.ConversionPattern=format

appenderName: Appender名称,对应某个已定义的 Appender。

ConversionPattern=format :就是 PatternLayout 布局模式下的具体格式。format 可选用格式:

%p:日志优先级,即 DEBUG,INFO,WARN,ERROR,FATAL。
%d:日志产生的日期或时间,默认格式为ISO8601,也可以在其后指定格式,如:%d{yyyy/MM/dd HH:mm:ss,SSS}。
%r:自应用程序启动到输出该日志信息耗费的毫秒数。
%t:产生该日志的线程名。
%l:输出日志事件的发生位置,相当于%c.%M(%F:%L)的组合,包括全路径类名、方法、文件名以及在代码中的行数。例如:test.TestLog4j.main(TestLog4j.java:10)。
%c:日志信息所属类目,通常就是所在类的全路径名称。
%M:产生日志信息的方法名。
%F:产生日志的文件名称。
%L::在代码中产生日志的行号。
%m::代码中自定义的具体日志信息字符串。
%n:输出一个回车换行符,Windows平台为"rn",Unix平台为"n"。
%x:输出和当前线程相关联的NDC(嵌套诊断环境),类似 java servlet 这样的多客户多线程的应用中会使用。
%%:输出一个"%"字符。 在%和格式字符之间还可以指定文本长度以及对齐方式:
%20M: 文本最小占用20个字符,默认右对齐。
%-20M: 文本最小占用20个字符,左对齐。
%.20M: 文本最大占用20个字符,超出部分字符从左边开始截断。

到这里,我们就可以完整的定义出上面作为示例的 log4j.properties 配置文件(再写一遍)。

# rootLogger
log4j.rootLogger=DEBUG, stdout # stdout appender
log4j.appender.stdout=org.apache.log4j.ConsoleAppender # stdout appender layout
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n

仔细观察这个例子,Log4j 定义"属性键"是有规律的:

log4j.appender.stdout 是一个 Appender;

log4j.appender.stdout.layout 是 log4j.appender.stdout 的一个属性;

log4j.appender.stdout.layout.ConversionPattern 是 log4j.appender.stdout.layout 的一个属性;

对应具体的"属性值",可以看到:

org.apache.log4j.ConsoleAppender 是一个 Appender;

org.apache.log4j.PatternLayout 是 org.apache.log4j.ConsoleAppender 的一个属性;

%-4r [%t] %-5p %c %x - %m%n 是 org.apache.log4j.PatternLayout 的具体值;

查看源码可以发现这种配置定义规律和 Log4j 的设计是紧密相连的。通过源码反过来理解 log4j.properties 文件,会是一番全新的感受。

Loggers 父子关系

上面说的都是根记录器 rootLogger,再来说说其它的 Logger。

新定义一个Logger,它的名字是"com",格式如下:

log4j.logger.com=WARN

程序中获取该Logger:

public static Logger logger = Logger.getLogger("com");

再定义一个Logger,它的名字是"com.foo",格式如下:

log4j.logger.com.foo=WARN

程序中获取该Logger:

public static Logger logger = Logger.getLogger("com.foo");

这样的两个 Logger 在名称上存在着嵌套包含关系,就像是 Java 中的包名定义。我们称这样的两个 Logger 是父子关系。

"com" 是 "com.foo" 的父亲,"com.foo" 是 "com" 的儿子。儿子默认会从父亲那里继承 Appender,这种父子关系是确定的,不可更改,但是继承特性只是默认的,也就是说可以更改,儿子可以选择不继承父亲的 Appender,定义新的使用。还有一个父子关系不要忽略了:rootLogger 是 "com" 的父亲。

修改一下最初的 log4j.properties 配置:

# rootLogger
log4j.rootLogger=DEBUG, stdout # stdout appender
log4j.appender.stdout=org.apache.log4j.ConsoleAppender # stdout appender layout
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n

# comLogger
log4j.logger.com=DEBUG # fooLogger
log4j.logger.com.foo=DEBUG

测试程序(后面会一直使用):

public class Main {

    public static Logger rootLogger = Logger.getRootLogger();
public static Logger comLogger = Logger.getLogger("com");
public static Logger fooLogger = Logger.getLogger("com.foo"); public static void main(String[] args) {
rootLogger.debug("root debug");
comLogger.debug("com debug");
fooLogger.debug("com.foo debug");
} }

结果是三条日志语句都会输出到控制台:

0    [main] DEBUG root  - root debug
1 [main] DEBUG com - com debug
1 [main] DEBUG com.foo - com.foo debug

在这个例子中,定义了三个 Logger,只定义了一个输出源 stdout,让 rootLogger 使用了 stdout,并没有让 comLogger 和 fooLogger 使用 stdout,但是通过结果可以看到三个 Logger 都使用了输出源 stdout。

可以得到结论:rootLogger 使用了输出源 stdout,comLogger 从 rootLogger 继承了 stdout,fooLogger 又从 comLogger 继承了 stdout。

这是默认情况下的继承特性,继承特性是可以取消的:

继承特性其实是由 Logger 的一个标识字段 additivity 控制的,它的默认值是 true,表示继承,将其改为 false 即表示不继承。看例子:

# rootLogger
log4j.rootLogger=DEBUG, stdout # stdout appender
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n # comLogger
log4j.logger.com=DEBUG, stdout2
log4j.additivity.com=false # stdout2 appender
log4j.appender.stdout2=org.apache.log4j.ConsoleAppender
log4j.appender.stdout2.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout2.layout.ConversionPattern=%m%n # fooLogger
log4j.logger.com.foo=DEBUG

为 comLogger 单独配置一个新输出源 stdout2,同样是往控制台输出,只是日志的具体格局更为简单一些。

还是刚才的测试程序,结果如下:

0    [main] DEBUG root  - root debug
com debug
com.foo debug

rootLogger 使用的输出源是 stdout,comLogger 使用了新的数据源 stdout2 并设置了 additivity=false,fooLogger 没有设置 additivity=false,所以从 comLogger 继承了 stdout2,输出了和 comLogger 一样格式的日志信息。

值得注意的是:

  1. 如果为某个 Logger 设置了 additivity=false,就要再为它指定至少一个 Appender,否则程序中一旦使用了该Logger,Log4j 就会报错,因为该 Logger 没有 Appender,日志信息无法输出。
  2. 如果有可能,Logger 只会从直系父亲那里继承 Appender,不会从爷爷那里间接继承。所以在上面的例子中,fooLogger 只继承了 comLogger 的 Appender,并没有继承 rootLogger 的 Appender。

Level 继承

Level 表示日志级别,在 "org.apache.log4j.Level" 中有定义。定义一个 Logger 的时候都要指明其记录的日志级别,如果没有明确指定,rootLogger 默认记录级别是  DEBUG,其它 Logger 会继承父 Logger 的 Level。

再看看修改过的例子:

# rootLogger
log4j.rootLogger=, stdout # stdout appender
log4j.appender.stdout=org.apache.log4j.ConsoleAppender # stdout appender layout
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n # comLogger
log4j.logger.com=INFO # fooLogger
log4j.logger.com.foo=

测试程序依旧是刚刚那个,输出结果:

0    [main] DEBUG root  - root debug

可以看到,只有 rootLogger 的 DEBUG 日志输出。

可以说明 rootLogger 默认的日志级别是 DEBUG。comLogger 定义的是 INFO 级别,测试程序中记录的是 debug 日志,所以没有输出,fooLogger 没有定义日志级别,于是从 comLogger 那里继承了 INFO 级别,所以也没有输出。

闲谈

rootLogger 其实就是 Log4j 指定了名称的一个 Logger,如果程序中不使用 rootLogger,在 log4j.properties 中也可以不定义 rootLogger。

如果定义了 rootLogger,还定义了其它 Logger,而其它的 Logger 又没有设置 additivity=false,那么因为继承特性,子 Logger 同时也会向 rootLogger 配置的 Appender 输出,这就存在输出重复日志的可能性。这不仅是 rootLogger 存在的问题,也是所有有父子关系的 Logger 都存在的问题。

Logger 可以指定记录日志的级别,Appender 也可以指定输出的日志级别,通过 Threshold 属性:

# stdout appender
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Threshold=INFO

Logger 和 Appender 都可以指定日志 LEVEL,好好利用这一点,说不定可以得到意想不到的效果。

Logger、Appender、Layout 是 Log4j 的三大基本元素,各自都有很多的属性可以配置。明白三者之间的关系和工作原理是最重要的,更为具体的使用还是参考 API、源码以及其它更好的资料吧。