Contents
  1. 1. Log4j 概述
  2. 2. Log4j 意义
  3. 3. Log4j Manual
    1. 3.1. Logger hierarchy
    2. 3.2. Appender and Layout
    3. 3.3. 范例
  4. 4. 源码分析
    1. 4.1. Logger.getLogger("com.foo.Bar")和Logger.getLogger("com.foo")
    2. 4.2. BasicConfigurator.configure()
    3. 4.3. logger.debug()
    4. 4.4. logger.setLevel(Level.INFO)、logger.debug("after set level")和barlogger.info("after set level")
  5. 5. 实践
  6. 6. 参考推荐

简单介绍log4j 1.x和官方Manual,本文的重点是对其意义和源码的分析,最后展示一般实践。

Log4j 概述

日志(logging)是开发周期中非常重要的组件,它能够提供程序运行时的精确内容,事后可保存下来研究。但是,日志也会降低程序速度,如果内容太详细,会令你瞎眼在茫茫日志语句中。Log4j的设计目标就是可靠性、快速和扩展性。

Log4j由三种组件组成——

  • Logger:日记信息生成器
  • Appender:日志信息的输出目标(output destination)
  • Layout:日志信息(logging request)的显示格式

Log4j 意义

The first and foremost advantage of any logging API over plain System.out.println resides in its ability to disable certain log statements while allowing others to print unhindered. —— log4j/1.2/manual
译:为什么使用logging API作为日志实现,而不是简单粗暴的System.out.println?首要好处就是可以控制哪些log statement输出哪些不输出。

我的理解是,log4j进行了两次分类:

  1. 最简单的是对log statement的等级分类,trace < debug < info < warn < error < fatal。比如输出日志时,若设阈值为info则只有level不低于info的才可以输出(因此log statement又被视为log request),那么trace和dubug的日志就被禁止了,就像“未成年人禁止进入网吧”一样,再配置日志的输出目标和输出格式后就可以打印出来。
    备注:如果要开发一个简单程序,并使用这些功能做日志,用log4j可能有点大材小用了吧。

  2. 对这些有level的log request再外包一层java class,称为Logger,可提供更丰富的日志控制机制。比如,为不同的Logger配置不同的输出目标和格式;下级Logger默认接受上级Logger的输出配置;Logger也有level,这个level用于控制其下的log request的输出阈值,即“只有level不低于info的才可以输出”一句中的info;实际经验中,还经常需要插入log request的java class的信息,结合反射机制,就可以一口气令Logger获得,精简代码。
    备注:在log4j的版本1.2之前,包装的是Category,而在1.2之后,Logger继承并且替代了它的位置。

除此之外,log4j能够将日志输出到多种、多个目标地,如控制台、文件、GUI组件、套接口服务器、NT的事件记录器、UNIX Syslog守护进程等,甚至异步输出,并且可以控制每一条日志的输出格式。


Log4j Manual

简单记录官方的Manual,只阐述结果,具体的解析看下一节会更加明白。

Logger hierarchy

Logger根据命名分层级(Named Hierarchy),子名用父名作为前缀,用点“.”分隔,类似package命名习惯,例如com.foo是com.foo.Bar的父亲,com是com.foo.Bar的祖先。

root Logger是所有Logger的祖先,有如下属性:

  • 总是存在的
  • 不可以通过名字获得

root Logger通过 Logger.getRootLogger 方法获得。其他的Logger通过 Logger.getLogger(name) 方法获得,并且传递同样的参数name总是返回同一个Logger实例的引用。

Logger的level相当于java class的成员变量一样,遵循继承机制(Level Inheritance)。用于控制其下各log request的是否能够输出,如果log request的level不小于产生它的Logger的level,则它是enabled,否则是disabled(Basic Selection Rule)。

Appender and Layout

调用 addAppender 方法可将Appender绑到Logger,指定Logger的一个输出目标。每一个enabled logging request都会被传递给它的Logger的所有Appenders和这个Logger的祖先的所有Appedenrs进行输出(Appenders Additivity)。如果将某个Logger $C$的additive设为false,相当于分了家,表示不需要祖先的Appenders,这个Logger $C$的子孙的additive默认仍是true,只向上继承到Logger $C$的Appenders。

Layout是类似String.format(..)一样的文本格式化,具体可见PatternLayout类。

范例

1
2
3
4
5
6
7
8
9
10
11
Logger barlogger = Logger.getLogger("com.foo.Bar");//子类在前也ok
Logger logger = Logger.getLogger("com.foo");

BasicConfigurator.configure();

logger.debug("before set level");//enabled

logger.setLevel(Level.INFO);//"com.foo.Bar"的logger也从"com.foo"继承info的level

logger.debug("after set level");//disabled
barlogger.info("after set level");//enabled

源码分析

针对上面的范例,可以从源码分析出Logger层次特征、输出目标Appenders和level控制是如何实现的,主要涉及的类和接口如下图所示。其中,Logger、LogManager、Hierarchy和Catagory最为重要,应用的主要涉及模式有工厂、代理、repository、观察者等。

Main.jpg

下面按语句来一步步跟踪解析,保持耐心!

Logger.getLogger("com.foo.Bar")Logger.getLogger("com.foo")

在Logger的静态方法getLogger(..)中,实际是去调用LogManager的静态方法,代码如下。

1
2
3
static public Logger getLogger(String name) {
return LogManager.getLogger(name);
}

在LogManager类中有static块,JVM在载入LogManager类后会先执行static块,简略代码如下。初始化Hierarchy,然后查找外设的配置信息。注意到root的level是debug等级的。

1
2
3
4
5
6
7
8
static {
// By default we use a DefaultRepositorySelector which always returns 'h'.
Hierarchy h = new Hierarchy(new RootLogger((Level) Level.DEBUG));
repositorySelector = new DefaultRepositorySelector(h);
/* 在CLASSPATH查找是否有配置信息 */
/* 在当前目录查找是否有log4j.xml配置文件 */
/* 在当前目录查找是否有log4j.properties配置文件 */
}

先关注到初始化Hierarchy结构这里,Hierarchy是一个实现了LoggerRepository接口的重要类,它的逻辑结构类似树,构造函数代码如下。主要的初始化工作包括root、level、ht等,ht是一个HashTable类型的成员变量,它是各个Logger实际存储的物理结构。
注意到Hierarchy是all等级的,任何都允许,虽然这在范例中并没有利用到,但是实际上Hierarchy也有阈值level控制程序中所有的Logger。

1
2
3
4
5
6
7
8
9
10
public Hierarchy(Logger root) {
ht = new Hashtable();
listeners = new Vector(1);
this.root = root;
// Enable all level levels by default.
setThreshold(Level.ALL);
this.root.setHierarchy(this);
rendererMap = new RendererMap();
defaultFactory = new DefaultCategoryFactory();
}

再回来看LogManager.getLogger(..),代码如下。绕了几圈后(因为抽象了repositorySelector),其实是调用Hierarchy实例的getLogger(..)方法。

1
2
3
4
public static Logger getLogger(final String name) {
// Delegate the actual manufacturing of the logger to the logger repository.
return getLoggerRepository().getLogger(name);
}

重头戏就是Hierarchy的getLogger(..)方法,代码如下。接下来分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public Logger getLogger(String name, LoggerFactory factory) {
CategoryKey key = new CategoryKey(name);
Logger logger;
synchronized (ht) {
Object o = ht.get(key);
if (o == null) {
logger = factory.makeNewLoggerInstance(name);
logger.setHierarchy(this);
ht.put(key, logger);
updateParents(logger);
return logger;
} else if (o instanceof Logger) {
return (Logger) o;
} else if (o instanceof ProvisionNode) {
// System.out.println("("+name+") ht.get(this) returned ProvisionNode");
logger = factory.makeNewLoggerInstance(name);
logger.setHierarchy(this);
ht.put(key, logger);
updateChildren((ProvisionNode) o, logger);
updateParents(logger);
return logger;
} else {
// It should be impossible to arrive here
return null; // but let's keep the compiler happy.
}
}
}

有三种情况:

  • o instanceof Logger
    如果HashTable已有这个name的logger就直接取出返回
  • o == null
    对完全陌生的”com.foo.Bar”,会创建Logger实例放入HashTable,并调用updateParents(logger)更新所有的祖先,代码如下。
    这个更新操作相当于建立”com.foo”和”com”的ProvisionNode类的伪结点,并插入HashTable,它们都只知道有一个孩子是”com.foo.Bar”,而”com.foo.Bar”的parent指向root。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    final private void updateParents(Logger cat) {
    String name = cat.name;
    int length = name.length();
    boolean parentFound = false;
    for (int i = name.lastIndexOf('.', length - 1); i >= 0; i = name
    .lastIndexOf('.', i - 1)) {
    String substr = name.substring(0, i);
    CategoryKey key = new CategoryKey(substr); // simple constructor
    Object o = ht.get(key);
    // Create a provision node for a future parent.
    if (o == null) {
    ProvisionNode pn = new ProvisionNode(cat);
    ht.put(key, pn);
    } else if (o instanceof Category) {
    parentFound = true;
    cat.parent = (Category) o;
    break; // no need to update the ancestors of the closest ancestor
    } else if (o instanceof ProvisionNode) {
    ((ProvisionNode) o).addElement(cat);
    } else {
    Exception e = new IllegalStateException("unexpected object type " + o.getClass() + " in ht.");
    e.printStackTrace();
    }
    }
    // If we could not find any existing parents, then link with root.
    if (!parentFound)
    cat.parent = root;
    }

执行执行Logger.getLogger(“com.foo.Bar”)结果如下。
1.png

  • o instanceof ProvisionNode
    随后执行Logger.getLogger(“com.foo”),再创建Logger实例放入HableTable并调用updateChildren((ProvisionNode) o, logger)和updateParents(logger)。
    第一个操作是更新子孙,代码如下。因为已有”com.foo”的ProvisionNode,它知道子孙是”com.foo.Bar”的Logger(且没有正确找到父亲),则”com.foo”的Logger会在”com.foo.Bar”的Logger这做一个相当于树结构的插入操作。
    第二个操作是更新祖先。会更新”com”的子孙为”com.foo.Bar”和”com.foo”,继续以ProvisionNode伪结点的方式存在。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    final private void updateChildren(ProvisionNode pn, Logger logger) {
    // System.out.println("updateChildren called for " + logger.name);
    final int last = pn.size();

    for (int i = 0; i < last; i++) {
    Logger l = (Logger) pn.elementAt(i);
    // System.out.println("Updating child " +p.name);

    // Unless this child already points to a correct (lower) parent,
    // make cat.parent point to l.parent and l.parent to cat.
    if (!l.parent.name.startsWith(logger.name)) {
    logger.parent = l.parent;
    l.parent = logger;
    }
    }
    }

然后执行Logger.getLogger(“com.foo”)的结果如下。
2.png

如果继续调用Logger.getLogger(“com”),更新子孙只需要在”com.foo”处直接插入,更新祖先指向root,结果如下。
3.png

BasicConfigurator.configure()

先看configure()的代码如下。把一个ConsoleAppender加给了root logger。

1
2
3
4
5
static public void configure() {
Logger root = Logger.getRootLogger();
root.addAppender(new ConsoleAppender(
new PatternLayout(PatternLayout.TTCC_CONVERSION_PATTERN)));
}

再看Logger类的addAppender方法(继承自Categeory类),代码如下。实际Logger类用AppenderAttachableImpl(接AppenderAttachabl的一种Vector实现)来作为Appender的容器,addAppender方法就是在扩充这个容器。

1
2
3
4
5
6
7
synchronized public void addAppender(Appender newAppender) {
if(aai == null) {
aai = new AppenderAttachableImpl();
}
aai.addAppender(newAppender);
repository.fireAddAppenderEvent(this, newAppender);
}

logger.debug()

5.png

调用Catagory类的debug()方法代码如下。要先判断Hierarchy的level阈值是否允许。

1
2
3
4
5
6
7
public void debug(Object message) {
if (repository.isDisabled(Level.DEBUG_INT))
return;
if (Level.DEBUG.isGreaterOrEqual(this.getEffectiveLevel())) {
forcedLog(FQCN, Level.DEBUG, message, null);
}
}

再判断level不为null的最近的祖先(含自己)是否允许。

1
2
3
4
5
6
7
public Level getEffectiveLevel() {
for (Category c = this; c != null; c = c.parent) {
if (c.level != null)
return c.level;
}
return null; // If reached will cause an NullPointerException.
}

由于默认Hierarchy的level为all,root的level为debug,debug>=debug,则可以顺利跳入forcelog(..),forcelog(..)再跳入callAppenders(..),其代码如下。方法appendLoopOnAppenders(..)就是向容器里的各个Appender通知LoggingEvent(设计模式中观察者模式),通知的过程是一个父结点回溯的过程,会通知所有的祖先。除非某个Logger的additive是false,则回溯过程会立刻终止跳出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void callAppenders(LoggingEvent event) {
int writes = 0;
for (Category c = this; c != null; c = c.parent) {
// Protected against simultaneous call to addAppender, removeAppender,...
synchronized (c) {
if (c.aai != null) {
writes += c.aai.appendLoopOnAppenders(event);
}
if (!c.additive) {
break;
}
}
}
if(writes == 0) {
repository.emitNoAppenderWarning(this);
}
}

logger.setLevel(Level.INFO)logger.debug("after set level")barlogger.info("after set level")

这三句实际在 3 中已经分析了,同样是受到Hierarchy和最近祖先(含自己)的level的影响。


实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.apache.log4j.Logger;
import org.apache.log4j.BasicConfigurator;
import com.foo.Bar;

public class MyApp {
static Logger logger = Logger.getLogger(MyApp.class);
public static void main(String[] args) {
BasicConfigurator.configure();
logger.info("Entering application.");
Bar bar = new Bar();
bar.doIt();
logger.info("Exiting application.");
}
}
1
2
3
4
5
6
7
8
9
package com.foo;
import org.apache.log4j.Logger;

public class Bar {
static Logger logger = Logger.getLogger(Bar.class);
public void doIt() {
logger.debug("Did it again!");
}
}

参考推荐

  1. 深入Log4J源码之SimpleLog
  2. 深入Log4J源码之Log4J Core
  3. 深入Log4J源码之Layout
  4. 深入Log4J源码之Appender
  5. 深入Log4J源码之LoggerRepository和Configurator
Contents
  1. 1. Log4j 概述
  2. 2. Log4j 意义
  3. 3. Log4j Manual
    1. 3.1. Logger hierarchy
    2. 3.2. Appender and Layout
    3. 3.3. 范例
  4. 4. 源码分析
    1. 4.1. Logger.getLogger("com.foo.Bar")和Logger.getLogger("com.foo")
    2. 4.2. BasicConfigurator.configure()
    3. 4.3. logger.debug()
    4. 4.4. logger.setLevel(Level.INFO)、logger.debug("after set level")和barlogger.info("after set level")
  5. 5. 实践
  6. 6. 参考推荐