• Java日志


            日志对于一个系统来说非常重要,查找异常信息、分析系统运行情况等都需要用到日志。所以无论是JDK还是第三方都提供了关于日志的相关工具,本文分别介绍以下几种工具,以及各种工具间的整合、原理。

    • JDK的java.util.logging包
    • 第三方日志工具(commons-logging/slf4j,log4j/logback)

    JDK的java.util.logging包

            JDK1.4引入了java.util.logging包,包中主要的类包括:Logger、LogManager、Handler、Formatter。首先看一段比较简单的示例代码:

    package me.likeyao.jdk.logger;
    
    import java.util.logging.Formatter;
    import java.util.logging.Handler;
    import java.util.logging.Level;
    import java.util.logging.LogRecord;
    import java.util.logging.Logger;
    
    public class JDKLoggerTest {
        public static void main(String[] args) {
            Logger logger = Logger.getLogger("logger");
            logger.info("hello world");
    
            Handler handler = new Handler() {
                @Override
                public void publish(LogRecord record) {
                }
                @Override
                public void flush() {
                }
                @Override
                public void close() throws SecurityException {
                }
            };
    
            handler.setFormatter(new Formatter() {
                @Override
                public String format(LogRecord record) {
                    return null;
                }
            });
    
            logger.setLevel(Level.INFO);
            logger.log(Level.FINEST, "hello world");
        }
    }

            通过Logger.getLogger(name)方法可以获取logger对象,logger对象有三个比较重要的概念:level、handler、formatter。level称为日志级别,在java.util.logging包中定义了java.util.logging.Level类,里面包含SEVERE/WARNING/INFO/CONFIG/FINE/FINER/FINEST(从高到低)7种日志级别。设置日志级别会过滤掉一部分日志,例如当日志级别设置为INFO级别时,CONFIG/FINE/FINER/FINEST级别的日志就会被忽略。handler解决的问题是日志输出到哪里,是到控制台(java.util.logging.ConsoleHandler),还是到文件(java.util.logging.FileHandler),或者是写到Socket中(java.util.logging.SocketHandler)。formatter定义了日志输出的格式,可以是XML(java.util.logging.XMLFormatter),也可以自己实现JSON格式的Fomatter。

    logger对象是如何生成的

            生成logger对象涉及到java.util.logging.LogManager类,LogManager中用到了单例模式,在static块中初始化了LoggerManager实例对象。生成logger对象的过程:

            从图中可以看到,logger对象是在LoggerManager中创建的。LoggerManager中有一个叫userContext的LoggerContext对象,userContext缓存了所有的logger对象(缓存在namedLoggers中),并维护了一套logger对象的父子结构。namedLoggers的定义:

    private final Hashtable<String,LoggerWeakRef> namedLoggers = new Hashtable<>();

    LoggerWeakRef继承自java.lang.ref.WeakReference,namedLoggers并不直接持有logger对象,当持有logger的对象被垃圾回收之后,只有一个weekreference指向logger,方便垃圾回收logger对象。

    JDK logger其他一些有意思的东西

    • 有一个Logger.getLogger方法是两参数的,public static Logger getLogger(String name, String resourceBundleName),第二个参数最终会变成java.util.ResourceBundle对象,可以用来做国际化。
    • java.util.logging.Handler可以设置java.util.logging.Filter更灵活的过滤日志。

    第三方日志工具(commons-logging/slf4j,log4j/logback)

            首先把四个工具分成了两组,commons-logging/slf4j和log4j/logback。log4j/logback功能与java.util.logging包类似,提供实际的日志功能。commons-logging/slf4j是门面,作用是统一日志操作,屏蔽底层不同日志组件的差异。

    commons-logging

            commons-logging是apache的项目,使用commons-logging的代码:

    package me.likeyao.java.logger;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    public class JCLTest {
        private static Log logger = LogFactory.getLog(JCLTest.class);
        public static void main(String[] args) throws Exception {
            logger.info("hello world");
        }
    }

    代码中用到了commons-logging日志对象(org.apache.commons.logging.Log),日志对象工厂(org.apache.commons.logging.LogFactory)。前面有说过commons-logging是一个统一操作的门面,不涉及具体的日志功能,那日志对象是怎么产生的?查看Log对象的继承关系:

    commons-logging分别为支持的日志工具提供了一个Log类的实现类,在列表中看到了Log4JLogger和Jdk14Logger等,意味着commons-logging可以log4j、java.util.logging组合使用。因为没有对应的logback实现,所以也就无法一起使用。下图解释commons-logging如何决定具体生成哪种Logger对象:

    LogFactory在static块中初始化了HashTable对象factories,以ClassLoader为key,LogFactory为value缓存了所有LogFactory对象。主要看一下LogFactoryImpl的discoverLogImplementation方法是如何发现底层使用的日志工具(省略了方法一部分内容):

    private static final String[] classesToDiscover = {
                LOGGING_IMPL_LOG4J_LOGGER, //org.apache.commons.logging.impl.Log4JLogger
                "org.apache.commons.logging.impl.Jdk14Logger",
                "org.apache.commons.logging.impl.Jdk13LumberjackLogger",
                "org.apache.commons.logging.impl.SimpleLog"
        };
    
    private Log discoverLogImplementation(String logCategory)
        ...
        Log result = null;
        //查看commons-logging.properties和System.getProperty是否配置了org.apache.commons.logging.log
        String specifiedLogClassName = findUserSpecifiedLogClassName();
        if (specifiedLogClassName != null) {
            ...
            //如果有配置,直接使用配置的类创建Log
            result = createLogFromClass(specifiedLogClassName,
                                        logCategory,
                                        true);
            ...
            return result;
        }
        ...
        //如果没有,遍历classesToDiscover数组,如果使用指定的ClassLoader Class.forName能加载到类,就创建Log对象
        for(int i=0; i<classesToDiscover.length && result == null; ++i) {
            result = createLogFromClass(classesToDiscover[i], logCategory, true);
        }
        if (result == null) {
            throw new LogConfigurationException
                        ("No suitable Log implementation");
        }
        return result;
    }

    整个初始化Log对象的流程:

    commons-logging中的classloader

    网上搜commons-logging,可以看到不少文章都是在讲关于classloader的问题,贴出比较详细的一篇的链接:《Taxonomy of class loader problems encountered when using Jakarta Commons Logging》。文章中用的commons-logging本版较老,这里用commons-logging-1.2+log4k-1.2.17模拟一种场景,看看新commons-logging有什么改变。代码:

    package me.likeyao.java.logger;
    
    import java.io.File;
    import java.net.URL;
    import java.net.URLClassLoader;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    public class JCLTest3 {
    
        public static void main(String[] args) throws Exception {
            //自定义classloader,模仿web容器classloader,自己加载优先
            ChildClassLoader childClassLoader = new ChildClassLoader(
                    new URL[] { new File("c:/tmpclass/commons-logging-1.2.jar").toURL(),
                                new File("C:/respository3/log4j/log4j/1.2.17/log4j-1.2.17.jar").toURL()});
            Thread.currentThread().setContextClassLoader(childClassLoader);
            Log log = LogFactory.getLog(JCLTest3.class);
            log.error("hello world");
        }
    
    }
    
    class ChildClassLoader extends URLClassLoader {
    
        public ChildClassLoader(URL[] urls) {
            super(urls, ClassLoader.getSystemClassLoader());
        }
    
        @Override
        protected Class<?> loadClass(String name, boolean resolve)
                throws ClassNotFoundException {
            synchronized (getClassLoadingLock(name)) {
                Class c = findLoadedClass(name);
                try{
                    c = findClass(name);
                    if (resolve) {
                        resolveClass(c);
                    }
                }catch(Exception e){
                    
                }
                if (c == null) {
                    c = super.loadClass(name, resolve);
                }
                return c;
            }
        }
    }

    命令行执行:java -cp .;commons-logging-1.2.jar me.likeyao.java.logger.JCLTest3。输出结果:执行没有抛异常,日志是通过JDK java.util.logging打出来了,而不是log4j。

    可以从代码中分析出为什么会产生这样的结果。这里用到了两种类加载器:系统类加载器(AppClassLoader)和自定义的ChildClassLoader,系统类加载器能加载到当前编译目录的class文件和commons-logging.jar,ChildClassLoader加载了commons-logging.jar和log4j.jar,线程上下文加载器被设置为ChildClassLoader。当运行java命令时,随着程序运行会使用系统类加载器加载Log类、LogFactory类,查看上面discoverLogImplementation方法源码,由于没有配置org.apache.commons.logging.log属性,具体使用哪个Log的实现类会通过遍历classesToDiscover数组决定。下面看一下classesToDiscover[0]时的情况:

    private Log createLogFromClass(String logAdapterClassName, //org.apache.commons.logging.impl.Log4JLogger
                                   String logCategory,
                                   boolean affectState)
        throws LogConfigurationException {
        ...
        //通过logAdapterClassName所制定的类创建的对象,方法返回的结果
        Log logAdapter = null;
        ...
        //获取classloader,这里会返回ChildClassLoader,即main方法中设置的线程上下文加载器
        ClassLoader currentCL = getBaseClassLoader();
        for(;;) {
            try {
                ...
                Class c;
                try {
                    //因为ChildClassLoader加载器可以获取到org.apache.commons.logging.impl.Log4JLogger类,正常得到c
                    c = Class.forName(logAdapterClassName, true, currentCL);
                } catch (ClassNotFoundException originalClassNotFoundException) {
                    ...
                }
                //创建org.apache.commons.logging.impl.Log4JLogger对象,这里o的classloader是ChildClassLoader
                constructor = c.getConstructor(logConstructorSignature);
                Object o = constructor.newInstance(params);
    
                // 注意下面的注释,因为Log和o的类加载器不一致,所以不进入if分支
                // Note that we do this test after trying to create an instance
                // [rather than testing Log.class.isAssignableFrom(c)] so that
                // we don't complain about Log hierarchy problems when the
                // adapter couldn't be instantiated anyway.
                if (o instanceof Log) {
                    logAdapterClass = c;
                    logAdapter = (Log) o;
                    break;
                }
    
                // Oops, we have a potential problem here. An adapter class
                // has been found and its underlying lib is present too, but
                // there are multiple Log interface classes available making it
                // impossible to cast to the type the caller wanted. We
                // certainly can't use this logger, but we need to know whether
                // to keep on discovering or terminate now.
                //
                // The handleFlawedHierarchy method will throw
                // LogConfigurationException if it regards this problem as
                // fatal, and just return if not.
                // 根据allowFlawedHierarchy参数判断是否抛出LogConfigurationException,如果不抛异常,只是简单return,进入下一个循环
                handleFlawedHierarchy(currentCL, c);
            } catch (NoClassDefFoundError e) {
                ...
                break;
            } catch (ExceptionInInitializerError e) {
                ...
                break;
            } catch (LogConfigurationException e) {
                // call to handleFlawedHierarchy above must have thrown
                // a LogConfigurationException, so just throw it on
                throw e;
            } catch (Throwable t) {
                ...
            }
    
            if (currentCL == null) {
                break;
            }
    
            // try the parent classloader
            // currentCL = currentCL.getParent();
            currentCL = getParentClassLoader(currentCL);
        }
        ...
        return logAdapter;
    }

    classesToDiscover[0]的调用中,logAdapter并没有指向org.apache.commons.logging.impl.Log4JLogger对象,而是返回了null。所以进入下一次循环classesToDiscover[1](org.apache.commons.logging.impl.Jdk14Logger),这一次因为Log类和org.apache.commons.logging.impl.Jdk14Logger类都是由系统类加载器加载,所以最终执行的结果日志由java.util.logging打出。commons-logging使用classloader来加载Resource和发现底层具体日志工具,在web容器或者OSGI这些需要用类加载器做隔离的情况下的确会出现一些问题。

    SLF4J/LOG4J

            另一种日志门面SLF4J,它采用“静态绑定”的方式避免了commons-logging中有关类加载的一些问题。下面的把SLF4J和LOG4J放到一起,首先还是比较简单的使用代码:

    package me.likeyao.slf4j.logger;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public class SLF4JTest {
        public static void main(String[] args) {
            Logger logger = LoggerFactory.getLogger(SLF4JTest.class);
            logger.error("hello world");
        }
    }

    使用方式上看与commons-logging类似,除了类名上有一些区别。下图的是SLF4J整合LOG4J时Logger初始化时序图:

    这里对几个类用了不同的颜色,蓝色代表类在slf4j-api包中,黄色代表类在slf4j-log4j12包中,红色代表类在log4j包中。相比commons-logging运行时通过classloader来发现底层日志工具,SLF4J是通过在不同的“桥梁包”里放置同名类org.slf4j.impl.StaticLoggerBinder来实现日志工具的绑定。例如,如果需要将SLF4J与java.util.logging整合,需要加入slf4j-jdk14-1.7.12,包中的StaticLoggerBinder类返回的loggerfactory是org.slf4j.impl.JDK14LoggerFactory;如果是SLF4J与log4j整合,需要加入slf4j-log4j12-1.7.12,包中的StaticLoggerBinder类返回的loggerfactory是org.slf4j.impl.Log4jLoggerFactory。这就是SLF4J的静态绑定。

            LOG4J是常用的日志工具,与java.util.logging(jul)非常像,一些概念也是共通的。例如logger都是父子结构的、jul的handler对应log4j的appender、formater对应layout、也都存在filter。为了理清log4j中各个类的关系,整理一份类图:

    整个类图的核心是org.apache.log4j.Category,但从1.2版本后log4j不会直接产生Category对象,而是Logger对象。Logger类继承自Category,扩展了一个日志级别:trace。当调用Logger对象的info/warn等方法时会生成一个LoggerEvent对象,AppenderAttachableImpl会遍历所有的appender调用doAppend方法。如果event没有被Filter过滤掉,那最终会经过Layout格式化,输出到appender指定的地方。LOG4J提供了很多appender供使用,这一点比JDK的hander强大很多。

            Appender的初始化在LogManager的static块中进行,最终解析发生在org.apache.log4j.xml.DOMConfigurator类中,logger、root、appender都在这里解析。

        if (tagName.equals(CATEGORY) || tagName.equals(LOGGER)) {
          parseCategory(currentElement);
        } else if (tagName.equals(ROOT_TAG)) {
          parseRoot(currentElement);
        } else if(tagName.equals(RENDERER_TAG)) {
          parseRenderer(currentElement);
        } else if(tagName.equals(THROWABLE_RENDERER_TAG)) {
            if (repository instanceof ThrowableRendererSupport) {
                ThrowableRenderer tr = parseThrowableRenderer(currentElement);
                if (tr != null) {
                    ((ThrowableRendererSupport) repository).setThrowableRenderer(tr);
                }
            }
        } else if (!(tagName.equals(APPENDER_TAG)
                || tagName.equals(CATEGORY_FACTORY_TAG)
                || tagName.equals(LOGGER_FACTORY_TAG))) {
            quietParseUnrecognizedElement(repository, currentElement, props);
        }

    LOGBACK

            logback是log4j的一个替代品,初始化和打日志代码流程相似。为什么要从log4j切换到logback,logback网站上已经给出了(Reasons to prefer logback over log4j)。要使用SLF4J+logback需要引入三个包:slf4j-api、logback-core、logback-classic。这里和log4j对比,介绍一下两种日志工具的父子结构。无论是java.util.logging还是log4j/logback都为logger对象提供了父子结构,这样做有什么好处?我觉得主要是这样logger对象会有一个树形的层次结构,底层的logger可以复用父logger中的一些配置,比如日志级别,appender等。从log4j和logback的配置文件中,也可以看出这一点,通过为某个包名的logger指定appender和日志级别,可以作用所有这个包下的logger。例如:

    <logger name="me.likeyao" additivity="false">     
        <level value="WARN" />     
        <appender-ref ref="CONSOLE" />     
    </logger>

            log4j和logback在处理父子结构时有一些差别,先看log4j的代码:

    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.
          }
    }

    log4j在创建logger时,如果父节点是一个logger,那只维护一个子logger对父logger的引用。如果父节点不存在,就创建一些ProvisionNode对象(这是一个Vector的子类),保存所有下面子的logger。举个例子:当获取名称为x.y.z的logger时,首先会创建logger(x.y.z);然后查找父logger x.y,如果x.y不存在,就创建一个ProvisionNode对象,把logger(x.y.z)放到ProvisionNode对象中;然后继续向上搜索名称为x的logger,就将logger(x.y.z)的parent设置为logger(x)。当调用logger(x.y.z)对象的info/warn等方法时,如果logger(x.y.z)没有设置日志级别和appender,就是沿着parent向上搜索,直到rootLogger为止。

            logback的结构稍微有一些区别,所有的节点都是Logger对象,对象中有指向parent和children的引用,并且创建节点时,会把parent的日志级别直接复制到自己对象中。

        // if the desired logger does not exist, them create all the loggers
        // in between as well (if they don't already exist)
        String childName;
        while (true) {
          int h = LoggerNameUtil.getSeparatorIndexOf(name, i);
          if (h == -1) {
            childName = name;
          } else {
            childName = name.substring(0, h);
          }
          // move i left of the last point
          i = h + 1;
          synchronized (logger) {
            childLogger = logger.getChildByName(childName);
            if (childLogger == null) {
              childLogger = logger.createChildByName(childName);
              loggerCache.put(childName, childLogger);
              incSize();
            }
          }
          logger = childLogger;
          if (h == -1) {
            return childLogger;
          }
        }

    总结

            SL4J/COMMONS-LOGGING、LOG4J、LOGBACK、JUL都是常用的Java日志工具,基本思想都是通过Factory生成logger对象,然后由LogManager管理/缓存logger对象,同时维护一个父子结构方便复用配置。logger对象包含三要素:日志级别、输出到哪里、格式化。

  • 相关阅读:
    Spyder | 关于报错No module named 'PyQt5.QtWebKitWidgets'
    Java基础(11) | 接口
    Java基础(10) | 抽象
    Java基础(9) | 继承
    Java基础(7) | String
    Java基础(6) | ArrayList
    CodeBlocks17.12配置GNU GCC + 汉化
    图片懒加载
    Mac安装Mysql 超详细(转载)
    剑指 Offer 03. 数组中重复的数字
  • 原文地址:https://www.cnblogs.com/bird-li/p/4696662.html
Copyright © 2020-2023  润新知