源码阅读:log4j日志树形结构及ProvisionNode

时间:2021-02-16 06:48:31

在Log4J中,每一个Logger有一个全局唯一的名称,所有Logger均以名称为键值存储在HashTable中,并且还根据名称组装成以RootLogger(名称为root的Logger)为根的一棵树,树的层次由Logger的Name来决定,其中以”.”分隔。

如按照以下顺序声明logger,则会形成图1的结构。

源码阅读:log4j日志树形结构及ProvisionNode

代码A

以L(name)的形式表示名称为name的Logger

上箭头表示(源Logger.parent = 目的Logger)。

源码阅读:log4j日志树形结构及ProvisionNode

图1

 

log4j基于Hierarchy来管理这棵树,记录所有Logger的HashTable就是在Hierarchy中,并由Hierarchy来维护。

(树中各个Logger是靠本身的parent字段连接起来的,而Hierarchy则负责更新这种关系)。

代码A中的主要接口是Hierarchy.getLogger(),源码片段1中截取了与图1有关的部分代码。这段代码的主要功能即是在HashTable中根据name查询对应的Logger。若未找到,则创建对应的Logger实例,添加到HashTable中,并更新Logger的parent。

源码阅读:log4j日志树形结构及ProvisionNode

源码片段1

Hierarchy.updateParents()的部分代码见源码片段2。

源码阅读:log4j日志树形结构及ProvisionNode

源码片段2

在更新Logger的parent时,会将Logger的名称按照 ‘ . ‘ 号来划分,然后寻找前缀名称的Logger(源码片段2中的第一行注释解释的较清楚)。如果找不到有效parent,则将其parent设置为L(root)。

根据源码的逻辑,代码A的执行如下:

1)  getLogger(x)

在Hierarchy的HashTable中未查到L(x),新建L(x),并用名称x作为键值加入HashTable;

2)  updateParents(x)

因为L(x)不包含符号”.”,所以,找不到在名称层级上的parent,将L(x)的parent设置为L(root);

3)  getLogger(x.y)

在Hierarchy的HashTable中未查到L(x.y),新建L(x.y),并用名称x.y作为键值加入HashTable;

4)  updateParents(x.y)

根据x.y前缀x在HashTable中查询L(x),找到,设置L(x.y).parent = L(x);

5)  getLogger(x.y.z)

在Hierarchy的HashTable中未查到L(x.y.z),新建L(x.y.z),并用名称x.y.z作为键值加入HashTable;

6)  updateParents(x.y.z)

根据x.y.z前缀在HashTable查询L(x.y),找到,设置L(x.y.z).parent = L(x.y)。

 

以上6步便形成了图1的树。

 

 

log4j允许先声明parent节点,再声明child节点,如:

源码阅读:log4j日志树形结构及ProvisionNode

代码B

按照一般的思路:

1)  新建L(x,y),因为L(x)尚不存在,将其parent设置为root;

2)  新建L(x),因为其不包含”.”符号,将其parent设置为root。

现在的一个需求是,要将L(x,y)的parent重新设置为L(x)。但要指出的是,Logger类中并没有一个字段来维护其所有的child节点。图1中的那棵树,是完全依赖于Logger类中的parent字段生长起来的。我们无法通过root拿到L(x.y)。

(这种设计的考虑,可能是因为一个Logger只有一个parent,但却可能有很多个child,维护child的引用代价比较高,比如一个比较庞大的工程,一个package下可能会有很多类)。

         一种解决方案是遍历Hierarchy中的HashTable,找到所有L(x.*)形式的Logger。不过Hierarchy中HashTable的维护是为了便于查找特定日志,而遍历的操作则影响性能。(需要注意的是,在该例中似乎可以直接通过在HashTable中取出name=x.y的Logger,但,事实上在getLogger(“x”)时,程序是并不知道之前新建过何种Logger的)。

 

log4j的解决方案是设计了ProvisionNode类。

Provision类实际上就是一个Vector(通过继承Vector类来实现)。当childlogger先建立,而未能找到parent时,log4j会预先建立一个ProvisionNode,并将child logger添加到ProvisionNode中。当实际的parent logger建立时,再将所有的child logger从ProvisionNode转移到parent下。

下面将以P(name)的形式表示名称为name的ProvisionNode。

         “预先建立一个ProvisionNode”的功能主要是在Hierarchy.updateParents()中实现的。源码片段3在源码片段2的基础上进行了补充。此时,在HashTable中是找不到L(x.y)的parent L(x)的,将会执行 if (o == null) 分支。

源码阅读:log4j日志树形结构及ProvisionNode

源码片段3

代码B中,LoggerFactory.getLogger("x.y")的执行如下:

1)  新建L(x,y);

2)  在HashTable中找不到L(x),新建P(x),并根据x形成键值,将P(x)添加到HashTable中;

3)  将L(x,y)添加到P(x)中;

4)  L(x,y)没有找到有效的parent节点,将L(x,y)的parent设置为root。

以上四步形成如图2的结构。

图2中P(x)以嵌套的矩形框表示其Vector结构,内层的矩形框表示Vector中的一个元素。

 

源码阅读:log4j日志树形结构及ProvisionNode

图2

 

接下来,LoggerFactory.getLogger("x")的执行就和之前的不同了。因为使用名称x在HashTable中查询时,会查询到一个ProvisionNode,此时,代码会执行if (o instanceof ProvisionNode) 分支。

源码片段4对源码片段1进行了补充。

源码阅读:log4j日志树形结构及ProvisionNode

源码片段4

 

代码B中,LoggerFactory.getLogger("x")的执行如下:

1)  使用键值x在HashTable中查询时,会找到P(x)。由于P(x)并不是一个Logger,而是一个ProvisionNode,所以log4j会先新建一个L(x);

2)  将HashTable中键值x的位置更新为指向L(x);

3)  将ProvisionNode记录的child,迁移到新建的L(x)中;

4)  最后,还需要继续更新L(x)的parent,这里仍然为L(root)。

这四步则形成了图3的结构。

(图3中,P(x)成了一个不再被引用的对象,不知道何时会被java自动回收。)

源码阅读:log4j日志树形结构及ProvisionNode

图3

 

这里新的接口是updateChildren(),这个接口的实现方式很简单,即是对ProvisionNode记录的Logger进行遍历,并更新parent。但是要注意红字部分的判断,这个判断是有必要的,也即并非所有的child都会在一次遍历中被迁移到logger下。下面的例2中会对此有简单的解释。

源码阅读:log4j日志树形结构及ProvisionNode

源码片段5

 

上述即是对log4j中日志的树形结构及ProvisionNode的介绍,下面,我们来看两个复杂些的例子,这两例不再详述。

例1

源码阅读:log4j日志树形结构及ProvisionNode

代码C

代码C对应的3幅图如下:

第一步,生成了两个ProvisionNode,注意,这两个ProvisionNode是平级的。

源码阅读:log4j日志树形结构及ProvisionNode

图4

 

第二步,生成L(x),并将P(x)中的L(x.y.z)迁移到L(x)下。

对于updateChildren(),在child更新parent新前,满足:!l.parent.name.startsWith(logger.name)

l —— L(x.y.z)

l.parent —— L(root) (更新parent之前在第一步中的parent)

logger —— L(x)

源码阅读:log4j日志树形结构及ProvisionNode

图5

 

第三步,生成L(x.y),并将P(x.y)中的L(x.y.z)迁移到L(x.y)下。(省略了P(x)的显示,仅为方便,其何时回收,我还并不知道)。

源码阅读:log4j日志树形结构及ProvisionNode

图6

对于updateChildren(),在child更新parent前,满足:!l.parent.name.startsWith(logger.name)

l —— L(x.y.z)

l.parent —— L(x) (更新parent之前在第一步中的parent)

logger —— L(x.y)

 

例2

源码阅读:log4j日志树形结构及ProvisionNode

代码D

第一步,生成了ProvisionNode,同例1中相同。

源码阅读:log4j日志树形结构及ProvisionNode

图7

 

第二步,迁移P(x.y)中的L(x.y.z)到L(x.y)中,同时要注意的是,P(x)中新增了一个child L(x.y)。这是在源码片段3的最后一个分支if(o instanceof ProvisionNode)中实现的。此时,P(x)同时记录了L(x.y.z)和L(x.y)。

源码阅读:log4j日志树形结构及ProvisionNode

图8

 

第三步,迁移P(x)中的Logger到L(x)中。

对于L(x.y.z):

         l—— L(x.y.z)

         l.parent—— L(x.y) (更新parent前在第二步中的parent)

         logger—— L(x)

         此时,不满足!l.parent.name.startsWith(logger.name)条件。所以,不迁移。

对于L(x.y):

         l—— L(x.y)

         l.parent—— L(root)

         logger—— L(x)

         此时,满足!l.parent.name.startsWith(logger.name)条件。所以,迁移。

所以,最终只有L(x.y)的parent节点被更新了,形成了如下图所示的最终结果。

源码阅读:log4j日志树形结构及ProvisionNode

图9