随笔-7  评论-23  文章-0  trackbacks-0
最近需要用到log4j动态定制Logger的场景,然后加上以前对于这个日志工具拿来就用而不知其原理的原因,所以决定花点时间看下它的源码,如果你还对log4j如何使用感到困惑,那么请首先简要浏览下它的官网http://logging.apache.org/log4j/

Log4j总体来说是一个可定制,支持同时多种形式输出日志,并且高度结构化的日志库。可定制,也就是既可以通过log4j.properties或者log4j.xml定义日志输出的级别(Level),形式(Appender)以及文本格式(Layout),也可以通过Logger类或者LogManager类取得Logger实例,并且设置日志输出级别(Level),形式(Appender)以及文本格式(Layout),可以说是相当的简便与灵活。下面一段代码简单地说明了后者的实现。
Logger MY_LOG = Logger.getLogger("MY_LOG");

//文件形式的输出方式实例化
DailyRollingFileAppender appender = new DailyRollingFileAppender();
appender.setName(name);
appender.setAppend(
true);
appender.setEncoding(
"GBK");
//文本的输出格式采用PatternLayout
appender.setLayout(new PatternLayout(pattern));
appender.setFile(
new File(getLogPath(), fileName).getAbsolutePath());
appender.activateOptions();

//将appender加入到MY_LOG的appender集合
MY_LOG.addAppender(appender);
//设定日志输出级别为INFO
MY_LOG.setLevel(Level.INFO);

Log4j初始化的代码是在LogManager的静态块里面,这个静态块无论如何都会实例化一个日志级别为DEBUGRootLogger,并且初始化一个以这个RootLogger为根节点的级联结构,然后检查有没有用户指定重写这个日志系统的初始化工作,如果没有,那么先去找log4j.xml,如果没有找到,那么再去找log4j.properties(也就是log4j.xml的优先级高于log4j.properties), 只有找到这两个配置文件的其中一个,再初始化文件里面内容,主要是一些配置文件中指定的Logger初始化,以及各个LoggerAppender列表设定,各个Appender的具体实例,Layout等。其实,没找到任何log4j配置文件也没什么关系,因为已经有RootLogger,日志系统骨架已经完成了,所以也可以通过如前面的代码添加Logger

//初始化以RootLogger为根的级联结构,rootLogger默认DEBUG级别
Hierarchy h = new Hierarchy(new RootLogger((Level) Level.DEBUG));
//有没指定log4j.configuration(内部使用,不知出于什么目的,未探究)
if(configurationOptionStr == null{
    
//加载classpath下log4j.xml
     url = Loader.getResource(DEFAULT_XML_CONFIGURATION_FILE);
   
if(url == null{
        
//如果log4j.xml没有,加载log4j.properties
        url = Loader.getResource(DEFAULT_CONFIGURATION_FILE);
    }

}
 else {
    
try {
    url 
= new URL(configurationOptionStr);
    }
 catch (MalformedURLException ex) {
    url 
= Loader.getResource(configurationOptionStr); 
    }

}
if(url != null{
    LogLog.debug(
"Using URL ["+url+"] for automatic log4j configuration."); 
    
//开始真正解析配置文件     
    OptionConverter.selectAndConfigure(url, configuratorClassName,LogManager.getLoggerRepository());
}
 else {
    LogLog.debug(
"Could not find resource:["+configurationOptionStr+"].");
}

简要介绍完Log4J的初始化后,我们有必要来看下从Logger myLogger =LogManager. getLogger(“MY_ LOG”), myLogger.debug(“some message”)结束之后,Log4j到底为我们做了些什么。


 
上面这幅序列图主要描述的过程就是从LoggerManager取得一个Logger的过程,其中最重要的操作在Hierarchy类中,这个类说白了就是存储Logger的仓库,其内部使用一个HashMap ht来存储LoggerProvisionNode

这里需要解释下ProvisionNode,这个类是一个Vector的实现,之前我们谈到过初始化的一开始,以RootLogger为根节点的级联结构(就是Hierarchy实例),那么这个ProvisionNode想当于这个级联结构中的树节点,比如我在定义了一个名字为a.b.cLogger,那么总共会生成”a”,”a.b”两个ProvisionNode,以及一个名字为”a.b.c”LoggerHierarchy并没有一个链表来维护他们之间的顺序,ProvisionNode会通过其本身就是向量的特性将属于它的Logger进行有序的排列,而Logger本身则通过parent属性记住他们的日志属性可以从哪里继承。

这样做的好处有两点,第一点就是只要名字相同,从LogManager中取出来的Logger就是同一个实例,第二点好处就是级联结构,可以定义出差异化的Logger,特别是其以a.b.c.d类似包结构的拆分日志节点,使得包级别的日志差异化输出更加的容易,并且这种特性提供了日志节点属性继承的功能。

举个例子,我们一般取得一个Logger实例是这样的, Logger log=LogManager.getLogger(Class class), Log4j处理方式为getLogger(class.getName()),这也就是说,这个Logger的名字是带包名的完全限定名字,所以如果我们通过名为a.b.c.aclass,a.b.c.d.bclass这么两个类取得Logger实例,然后定义名为a.b.c和a.b.c.d 2个Logger,那么总共会生成如下的节点 
           
            ProvisionNode: a,a.b
                    Logger: a.b.c(加入a,a.b 2个ProvisionNode中并且parent为RootLogger)
                             a.b.c.d(加入a,a.b 2个ProvisionNode中,并且parent为a.b.c)
                             a.b.c.aclass  (加入到a,a.b 2ProvisionNode中,并且parent为a.b.c), 
                             a.b.c.d.bclass(加入到a,a.b 2ProvisionNode中,并且parent为a.b.c.d)

其中ProvisionNode可能升级为Logger,当同名的Logger加入时,该ProvisionNode中所有以该ProvisionNode为父节点的子节点修改parent指向新的Logger.

final private void updateChildren(ProvisionNode pn, Logger logger) {
    
final int last = pn.size();
   
    
for(int i = 0; i < last; i++{
         Logger l 
= (Logger) pn.elementAt(i);
          
//有可能子节点的父节点指向更低一级的节点,比如孙节点。这个应该不难理解。
         
 if(!l.parent.name.startsWith(logger.name)) {
             logger.parent 
= l.parent;
             l.parent 
= logger;
        }

     }

}

这样Logger a.b.c除了自身的日志输出设置之外,还享受rootLogger的输出(输出级别,Appenders),Logger a.b.c.d同时享受rootLogger和a.b.c的设置,a.b.c.d.bclass享受rootLogger,a.b.c,a.b.c.d的日志输出设定,也就是可以分别定义一批Logger的输出级别和输出形式以及其他属性,当然也有控制开关控制这种继承。下图说明了一些问题。



取得Logger之后,随后就是需要按指定形式输出日志内容,首先需要在LoggerRepository中判定当前Level是否可以做日志输出,包括与全局的thresholdInt进行判定(相当于总开关,这个级别通不过直接返回,不做任何事情),通过后,再与其自身Logger日志级别比较,如果没有设定,查其父节点的日志级别,仍然没有设定,那么查父节点的父节点,直到查到有日志级别设定或者最终到RootLogger(其默认为Debug级别),比较通过后,就可以调用callDependers进行日志输出了。

日志级别为 OFF>FATAL>ERROR>WARN>INFO>DEBUG>ALL,只有输出级别大于等于Logger自身级别才能进行输出,比如 logger.debug,那么只有该Logger的级别(也可能是其先辈节点的日志级别)DEBUG或者ALL才能允许被输出。全局的thresholdInt如果不设定是保持在ALL级别。

一般Logger会通过AppenderAttachableImpl的实例来维护多个Appender,并且可以共享父节点的Appender List(包括RootLogger里面定义的appenders,所以我们一般在log4j.xml或者log4j.properties里面定义一个某个包下的Logger,然后挂有几个Appender,而程序中通过完全限定的类名(这个类属于前面指定的包)取得Logger,那么当这些Logger输出日志的时候,其本身并没有任何Appender,但是却通过先辈节点定义的Appenders得以输出。这里需要注意的是,如果在直系节点间定义相同的appender,似乎会多次重复输出,因为其会遍历自身以及所有先辈节点的Appender list并且逐一进行doAppend调用,而不会去排重,并且addAppender的时候也没有遍历先辈节点排重。


Appender持有一个Filter链表,doAppend的时候首先走一遍过滤器,结果有3种,Filter.DENY(直接拒绝返回),Filter.ACCEPT(接收输出请求,并且不再走之后的Filter),Filter.NEUTRAL(继续执行下一个Filter),顺利通过Filter链之后,进入真正的输出日志过程,这边以WriteAppender为例,首先将message通过持有的Layout进行格式化(format),然后调用输出流输出日志到目标文件(FileAppender)或者屏幕(ConsoleAppender)

至此,整个日志输出过程结束。

下面两幅图分别是Log4j的整体类图,不是非常完整,但是大概能够了解到整个结构。



总结下,
log4J出来已经很多年了,以前只是使用下,并没有去探究里面机制,但其某些机制还是相当不错的。文中可能出现一些错误,请各位能够指出。

posted on 2010-10-22 10:40 BucketLI 阅读(4247) 评论(1)  编辑  收藏

评论:
# re: Log4j代码随读 2012-05-15 11:28 | mabusyao
最后一张图,Appender并没有在Hierarchy中被使用到。倒是AppenderAttachableImpl和Appender之间应当有关联关系的。

问楼主个问题,Hierarchy里面的RendererMap是干什么用的?  回复  更多评论
  

只有注册用户登录后才能发表评论。


网站导航: