• Log4j代码随读(转)


    最近需要用到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的静态块里面,这个静态块无论如何都会实例化一个日志级别为DEBUG的RootLogger,并且初始化一个以这个RootLogger为根节点的级联结构,然后检查有没有用户指定重写这个日志系统的初始化工作,如果没有,那么先去找log4j.xml,如果没有找到,那么再去找log4j.properties(也就是log4j.xml的优先级高于log4j.properties), 只有找到这两个配置文件的其中一个,再初始化文件里面内容,主要是一些配置文件中指定的Logger初始化,以及各个Logger的Appender列表设定,各个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来存储Logger和ProvisionNode。

    这里需要解释下ProvisionNode,这个类是一个Vector的实现,之前我们谈到过初始化的一开始,以RootLogger为根节点的级联结构(就是Hierarchy实例),那么这个ProvisionNode想当于这个级联结构中的树节点,比如我在定义了一个名字为a.b.c的Logger,那么总共会生成”a”,”a.b”两个ProvisionNode,以及一个名字为”a.b.c”的Logger。Hierarchy并没有一个链表来维护他们之间的顺序,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 2个ProvisionNode中,并且parent为a.b.c), 
                                 a.b.c.d.bclass(加入到a,a.b 2个ProvisionNode中,并且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出来已经很多年了,以前只是使用下,并没有去探究里面机制,但其某些机制还是相当不错的。文中可能出现一些错误,请各位能够指出。

  • 相关阅读:
    [原创]推荐三款强大且漂亮的统计图控件
    使用ATL设计组件(VC6.0)
    COM笔记引用计数
    远程调试
    记事本2
    C++中类的多态与虚函数的使用
    Eclipse常用快捷键
    怎么在Release下调试代码
    MSDN 无法显示网页
    COM笔记接口
  • 原文地址:https://www.cnblogs.com/SoulSpirit/p/3558100.html
Copyright © 2020-2023  润新知