在Log4J中,每一个Logger有一个全局唯一的名称,所有Logger均以名称为键值存储在HashTable中,并且还根据名称组装成以RootLogger(名称为root的Logger)为根的一棵树,树的层次由Logger的Name来决定,其中以”.”分隔。
如按照以下顺序声明logger,则会形成图1的结构。
代码A
以L(name)的形式表示名称为name的Logger
上箭头表示(源Logger.parent = 目的Logger)。
图1
log4j基于Hierarchy来管理这棵树,记录所有Logger的HashTable就是在Hierarchy中,并由Hierarchy来维护。
(树中各个Logger是靠本身的parent字段连接起来的,而Hierarchy则负责更新这种关系)。
代码A中的主要接口是Hierarchy.getLogger(),源码片段1中截取了与图1有关的部分代码。这段代码的主要功能即是在HashTable中根据name查询对应的Logger。若未找到,则创建对应的Logger实例,添加到HashTable中,并更新Logger的parent。
源码片段1
Hierarchy.updateParents()的部分代码见源码片段2。
源码片段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节点,如:
代码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) 分支。
源码片段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中的一个元素。
图2
接下来,LoggerFactory.getLogger("x")的执行就和之前的不同了。因为使用名称x在HashTable中查询时,会查询到一个ProvisionNode,此时,代码会执行if (o instanceof ProvisionNode) 分支。
源码片段4对源码片段1进行了补充。
源码片段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自动回收。)
图3
这里新的接口是updateChildren(),这个接口的实现方式很简单,即是对ProvisionNode记录的Logger进行遍历,并更新parent。但是要注意红字部分的判断,这个判断是有必要的,也即并非所有的child都会在一次遍历中被迁移到logger下。下面的例2中会对此有简单的解释。
源码片段5
上述即是对log4j中日志的树形结构及ProvisionNode的介绍,下面,我们来看两个复杂些的例子,这两例不再详述。
例1
代码C
代码C对应的3幅图如下:
第一步,生成了两个ProvisionNode,注意,这两个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)
图5
第三步,生成L(x.y),并将P(x.y)中的L(x.y.z)迁移到L(x.y)下。(省略了P(x)的显示,仅为方便,其何时回收,我还并不知道)。
图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
代码D
第一步,生成了ProvisionNode,同例1中相同。
图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)。
图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节点被更新了,形成了如下图所示的最终结果。
图9