• java 日志体系(四)log4j 源码分析


    java 日志体系(四)log4j 源码分析

    logback、log4j2、jul 都是在 log4j 的基础上扩展的,其实现的逻辑都差不多,下面以 log4j 为例剖析一下日志框架的基本组件。

    一、总体架构

    log4j 使用如下:

    @Test
    public void test() {
        Log log = LogFactory.getLog(JclTest.class);
        log.info("jcl log");
    }
    

    log.info 时调用的时序图如下:

    log4j 时序图

    在 log4j 的配置文件,我们可以看到其三个最重要的组件:

    1. Logger 每个 logger 可以单独配置
    2. Appender 每个 appender 可以将日志输出到它想要的任何地方(文件、数据库、消息等等)
    3. Layout 日志格式布局

    这三个组件的关系如下:

    Log4J 类图

    Log4j API(核心)

    • 日志对象(org.apache.log4j.Logger):供程序员输出日志信息
    • 日志附加器(org.apache.log4j.Appender):把格式化好的日志信息输出到指定的地方去
      • ConsoleAppender - 目的地为控制台的 Appender
      • FileAppender - 目的地为文件的 Appender
      • RollingFileAppender - 目的地为大小受限的文件的 Appender
    • 日志格式布局(org.apache.log4j.Layout):用来把程序员的 message 格式化成字符串
      • PatternLayout - 用指定的 pattern 格式化 message的 Layout
    • 日志过滤器(org.apache.log4j.spi.Filter)
    • 日志事件(org.apache.log4j.LoggingEvent)
    • 日志级别(org.apache.log4j.Level)
    • 日志管理器(org.apache.log4j.LogManager)
    • 日志仓储(org.apache.log4j.spi.LoggerRepository)
    • 日志配置器(org.apache.log4j.spi.Configurator)
    • 日志诊断上下文(org.apache.log4j.NDC、org.apache.log4j.MDC)

    二、日志管理器(org.apache.log4j.LogManager)

    主要职责:

    • 初始化默认 log4j 配置
    • 维护日志仓储(org.apache.log4j.spi.LoggerRepository)
    • 获取日志对象(org.apache.log4j.Logger)

    LogManager

    2.1 初始化默认 log4j 配置

    LogManager 的静态代码块加载配置文件。

    static {
        // 1. 初始化默认的日志仓库 Hierarchy(实现了 LoggerRepository 接口) 
        //    DefaultRepositorySelector#getLoggerRepository 简单的封装了 LoggerRepository
        Hierarchy h = new Hierarchy(new RootLogger((Level) Level.DEBUG));
        repositorySelector = new DefaultRepositorySelector(h);
    
        // 2. DEFAULT_CONFIGURATION_KEY=log4j.configuration 配置文件
        //    CONFIGURATOR_CLASS_KEY=log4j.configuratorClass 配置文件解析器,
        //    分 DOMConfigurator 和 PropertyConfigurator 两类
        String configurationOptionStr = OptionConverter.getSystemProperty(
                DEFAULT_CONFIGURATION_KEY, null);
        String configuratorClassName = OptionConverter.getSystemProperty(
                CONFIGURATOR_CLASS_KEY, null);
    
        // 3. 根据配置文件路径加载资源文件
        URL url = null;
        if (configurationOptionStr == null) {
            url = Loader.getResource(DEFAULT_XML_CONFIGURATION_FILE);
            if (url == null) {
                url = Loader.getResource(DEFAULT_CONFIGURATION_FILE);
            }
        } else {
            try {
                url = new URL(configurationOptionStr);
            } catch (MalformedURLException ex) {
                // so, resource is not a URL:
                // attempt to get the resource from the class path
                url = Loader.getResource(configurationOptionStr);
            }
        }
    
        // 4. Configurator 解析配置文件
        if (url != null) {
            try {
                OptionConverter.selectAndConfigure(url, configuratorClassName,
                        LogManager.getLoggerRepository());
            } catch (NoClassDefFoundError e) {
                LogLog.warn("Error during default initialization", e);
            }
        } 
    }
    

    2.2 日志仓储(org.apache.log4j.spi.LoggerRepository)

    主要职责:

    • 管理日志级别阈值(org.apache.log4j.Level)
    • 管理日志对象(org.apache.log4j.Logger)

    LoggerRepository 的主要方法是 getLogger(name),创建一个日志对象。

    // ht 通过 key/value 的形式保存了所有的 logger,其中 key 为类的全路径,value 为 logger
    // logger 有父子关系,每个 logger 的父节点为前一个包名,如果父节点不存在则一直向上查找,直到 rootLogger
    // 如果其父节点不存在,使用 ProvisionNode 先进行占位,ProvisionNode 保存有其全部的子节点
    // 即 com.github.binarylei.log4j.Log4jTest1 的父节点为 com.github.binarylei.log4j,直到 rootLogger 为止
    Hashtable ht;
    
    public Logger getLogger(String name, LoggerFactory factory) {
        CategoryKey key = new CategoryKey(name);
        Logger logger;
    
        synchronized (ht) {
            Object o = ht.get(key);
            // 1. 日志仓库中没有创建一个
            if (o == null) {
                logger = factory.makeNewLoggerInstance(name);
                logger.setHierarchy(this);
                ht.put(key, logger);
                updateParents(logger);
                return logger;
            // 2. 存在直接返回
            } else if (o instanceof Logger) {
                return (Logger) o;
            // 3. ProvisionNode 占位用
            } 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);
                // ProvisionNode 中的是子节点元素,logger 为当前的父节点
                updateChildren((ProvisionNode) o, logger);
                updateParents(logger);
                return logger;
            } else {
                // It should be impossible to arrive here
                return null;
            }
        }
    }
    

    其中有两个相对比较重要的方法,updateParents 和 updateChildren

    // 轮询父节点,如果存在则直接指定其父节点
    // 如果不存在则创建一个 ProvisionNode 用于占位,并设置 ProvisionNode 的子节点
    final private void updateParents(Logger cat) {
        String name = cat.name;
        int length = name.length();
        boolean parentFound = false;
    
        // if name = "w.x.y.z", loop thourgh "w.x.y", "w.x" and "w", but not "w.x.y.z"
        // 轮询父节点
        for (int i = name.lastIndexOf('.', length - 1); i >= 0;
             i = name.lastIndexOf('.', i - 1)) {
            String substr = name.substring(0, i);
            CategoryKey key = new CategoryKey(substr); // simple constructor
            Object o = ht.get(key);
            // 1. 不存在父节点,创建一个 ProvisionNode 用于占位,设置其子节点为 cat
            if (o == null) {
                ProvisionNode pn = new ProvisionNode(cat);
                ht.put(key, pn);
            // 2. 存在父节点则指定当前 logger 的父节点
            } else if (o instanceof Category) {
                parentFound = true;
                cat.parent = (Category) o;
                break; // no need to update the ancestors of the closest ancestor
            // 3. 如果是 ProvisionNode 直接添加其子节点
            } else if (o instanceof ) {
                ((ProvisionNode) o).addElement(cat);
            } else {
                Exception e = new IllegalStateException("unexpected object type " +
                        o.getClass() + " in ht.");
                e.printStackTrace();
            }
        }
        // If we could not find any existing parents, then link with root.
        if (!parentFound)
            cat.parent = root;
    }
    
    // ProvisionNode 保存有当前 logger 的所有子节点
    // 创建 logger 时如果找不到父节点则默认为 root,即 l.parent.name=root
    // 如果 l.parent 已经是正确的父节点则忽略,否则就需要更新其父节点
    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;
            }
        }
    }
    

    三、日志对象(org.apache.log4j.Logger)

    Logger 继承自 org.apache.log4j.Priority。Logger 日志级别: OFF、FATAL、ERROR、INFO、DEBUG、TRACE、ALL。

    Logger 最终要的方法是输出日志,持有 Appender 才能输出日志。

    3.1 Logger 管理 Appender

    AppenderAttachableImpl 用来管理所有的 Appender,对 logger 上的所有 Appender 进行增删改查,当前还一个最重要的方法 appendLoopOnAppenders 用于输出日志。

    AppenderAttachableImpl aai;
    public synchronized void addAppender(Appender newAppender) {
        if (aai == null) {
            aai = new AppenderAttachableImpl();
        }
        aai.addAppender(newAppender);
        repository.fireAddAppenderEvent(this, newAppender);
    }
    

    3.2 Logger 日志输出

    public void info(Object message) {
        if (repository.isDisabled(Level.INFO_INT))
            return;
        if (Level.INFO.isGreaterOrEqual(this.getEffectiveLevel()))
            forcedLog(FQCN, Level.INFO, message, null);
    }
    protected void forcedLog(String fqcn, Priority level, Object message, Throwable t) {
    	callAppenders(new LoggingEvent(fqcn, this, level, message, t));
    }
    

    callAppenders 最终调用 appender.doAppend(event) 进行日志输出。

    public void callAppenders(LoggingEvent event) {
        int writes = 0;
    
        for (Category c = this; c != null; c = c.parent) {
            // Protected against simultaneous call to addAppender, removeAppender,...
            synchronized (c) {
            	// 1. 日志输出
                if (c.aai != null) {
                    writes += c.aai.appendLoopOnAppenders(event);
                }
                // 2. 如果 logger.additive=false 则不会将日志向上传递给父节点 logger
                //    也就是说 additive=false 时日志不会重复输出,默认为 true
                //    类似 spring 子容器的事件传递给父容器
                if (!c.additive) {
                    break;
                }
            }
        }
        // 没有日志输出
        if (writes == 0) {
            repository.emitNoAppenderWarning(this);
        }
    }
    
    // AppenderAttachableImpl#appendLoopOnAppenders 用于日志输出
    public int appendLoopOnAppenders(LoggingEvent event) {
        int size = 0;
        Appender appender;
    
        if (appenderList != null) {
            size = appenderList.size();
            for (int i = 0; i < size; i++) {
                appender = (Appender) appenderList.elementAt(i);
                // 真正输出日志
                appender.doAppend(event);
            }
        }
        return size;
    }
    

    3.3 日志事件(org.apache.log4j.LoggingEvent)

    日志事件是用于承载日志信息的对象,其中包括:日志名称、日志内容、日志级别、异常信息(可选)、当前线程名称、时间戳、嵌套诊断上下文(NDC)、映射诊断上下文(MDC)。

    四、日志附加器(org.apache.log4j.Appender)

    日志附加器是日志事件(org.apache.log4j.LoggingEvent)具体输出的介质,如:控制台、文件系统、网络套接字等。

    日志附加器(org.apache.log4j.Appender)关联零个或多个日志过滤器(org.apache.log4j.Filter),这些过滤器形成过滤链。

    主要职责:

    • 附加日志事件(org.apache.log4j.LoggingEvent)
    • 关联日志布局(org.apache.log4j.Layout)
    • 关联日志过滤器(org.apache.log4j.Filter)
    • 关联错误处理器(org.apache.log4j.spi.ErrorHandler)

    相关组件的关系如下,Append 持有 Layout、Filter、ErrorHandler。

    Appender 组件关系

    4.1 Appender 主要流程

    log info流程

    注意 logger#info 调用 doAppend 时加 synchronized 锁了,所以是线程安全的,但了同时造成多线程时效率低下。所以才有了后来的 log4j2 和 logback 的出现。

    public synchronized void doAppend(LoggingEvent event) {
      	// 1. 日志级别判断
        if (!isAsSevereAsThreshold(event.getLevel())) {
            return;
        }
    
      	// 2. Filter 过滤
        Filter f = this.headFilter;
        FILTER_LOOP:
        while (f != null) {
            switch (f.decide(event)) {
            	// 1. 日志事件跳过日志附加器的执行
                case Filter.DENY:
                    return;
            	// 2. 日志附加器立即执行日志事件
                case Filter.ACCEPT:
                    break FILTER_LOOP;
            	// 3. 跳过当前过滤器,让下一个过滤器决策
                case Filter.NEUTRAL:
                    f = f.getNext();
            }
        }
        // 3. 子类实现,日志输出
        this.append(event);
    }
    

    doAppend 做日志过滤,是否进行日志输出,真实的日志输出则直接委托给了 append 方法。append -> subAppend -> qw.write,QuietWriter 增加了对日志输出错误时的 ErrorHandler 处理。

    public void append(LoggingEvent event) {
    	subAppend(event);
    }
    protected void subAppend(LoggingEvent event) {
        this.qw.write(this.layout.format(event));
    
        if (layout.ignoresThrowable()) {
            String[] s = event.getThrowableStrRep();
            if (s != null) {
                int len = s.length;
                for (int i = 0; i < len; i++) {
                    this.qw.write(s[i]);
                    this.qw.write(Layout.LINE_SEP);
                }
            }
        }
    
        if (shouldFlush(event)) {
            this.qw.flush();
        }
    }
    

    4.2 日志过滤器(org.apache.log4j.spi.Filter)

    日志过滤器用于决策当前日志事件(org.apache.log4j.spi.LoggingEvent)是否需要在执行所关联的日志附加器(org.apache.log4j.Appender)中执行。

    决策结果有三种:

    • DENY:日志事件跳过日志附加器的执行
    • ACCEPT:日志附加器立即执行日志事件
    • NEUTRAL:跳过当前过滤器,让下一个过滤器决策
    public void addFilter(Filter newFilter) {
        if (headFilter == null) {
            headFilter = tailFilter = newFilter;
        } else {
            tailFilter.setNext(newFilter);
            tailFilter = newFilter;
        }
    }
    

    4.3 Appender 类继承关系

    Appender 类图

    • ConsoleAppender - 目的地为控制台的 Appender
    • FileAppender - 目的地为文件的 Appender
    • RollingFileAppender - 目的地为大小受限的文件的 Appender

    WriterAppender 不关心日志到底写到那个流中,子类调用 createWriter 来创建一个具体的 Writer,这个 Writer 最终会被 QuietWriter 进行包装。

    // WriterAppender#createWriter
    protected OutputStreamWriter createWriter(OutputStream os) {
        OutputStreamWriter retval = null;
    
        String enc = getEncoding();
        if (enc != null) {
            try {
                retval = new OutputStreamWriter(os, enc);
            } catch (IOException e) {
            }
        }
        if (retval == null) {
            retval = new OutputStreamWriter(os);
        }
        return retval;
    }
    

    4.3.1 FileAppender

    FileAppender 通过 setFile 方法创建一个 QuietWriter 进行文件定入。

    public synchronized void setFile(String fileName, boolean append, boolean bufferedIO, int bufferSize)
                throws IOException {
        if (bufferedIO) {
            setImmediateFlush(false);
        }
    
        reset();
        FileOutputStream ostream = null;
        try {
            ostream = new FileOutputStream(fileName, append);
        } catch (FileNotFoundException ex) {
         	...
        }
        Writer fw = createWriter(ostream);
        if (bufferedIO) {
            fw = new BufferedWriter(fw, bufferSize);
        }
        this.setQWForFiles(fw);
        this.fileName = fileName;
        this.fileAppend = append;
        this.bufferedIO = bufferedIO;
        this.bufferSize = bufferSize;
        writeHeader();
        LogLog.debug("setFile ended");
    }
    

    4.3.2 RollingFileAppender 文件大小滚动

    RollingFileAppender 根据文件大小进行滚动,有一个重要的属性 maxFileSize 控制文件大小。RollingFileAppender#subAppend 每次写日志时都会判断是否达到回滚的条件。

    protected void subAppend(LoggingEvent event) {
        super.subAppend(event);
        if (fileName != null && qw != null) {
            long size = ((CountingQuietWriter) qw).getCount();
            if (size >= maxFileSize && size >= nextRollover) {
            	// 滚动生成新的日志文件
                rollOver();
            }
        }
    }
    

    4.3.3 DailyRollingFileAppender 时间滚动

    DailyRollingFileAppender(根据时间滚动) 和 RollingFileAppender(根据文件大小滚动) 差不多,只是回滚的条件不一样吧了。DailyRollingFileAppender 有一个重要的属性 datePattern = "'.'yyyy-MM-dd" 用于控制多长时间滚动一次,具体配制规则见类注释。

    protected void subAppend(LoggingEvent event) {
        long n = System.currentTimeMillis();
        if (n >= nextCheck) {
            now.setTime(n);
            // 计算一次滚动的时间
            nextCheck = rc.getNextCheckMillis(now);
            try {
                rollOver();
            } catch (IOException ioe) {
                ...
            }
        }
        super.subAppend(event);
    }
    

    五、日志格式布局(org.apache.log4j.Layout)

    日志格式布局用于格式化日志事件(org.apache.log4j.spi.LoggingEvent)为可读性的文本内容。

    Layout 最重要的方法是 format,将 LoggingEvent 转换成可读性的文本内容。

    5.1 SimpleLayout

    public String format(LoggingEvent event) {
    	sbuf.setLength(0);
    	sbuf.append(event.getLevel().toString());
    	sbuf.append(" - ");
    	sbuf.append(event.getRenderedMessage());
    	sbuf.append(LINE_SEP);
    	return sbuf.toString();
    }
    

    5.2 PatternLayout

    PatternLayout 可以自定义 LoggingEvent 输出格式,如 "%r [%t] %p %c %x - %m%n",初始化时会将 pattern 解析为 PatternConverter,PatternConverter 是一个链式结构。PatternLayout 自定义规则详见 PatternLayout 类注释。

    public final static String DEFAULT_CONVERSION_PATTERN = "%m%n";
    private StringBuffer sbuf = new StringBuffer(BUF_SIZE);
    private String pattern;
    private PatternConverter head;
    
    public PatternLayout(String pattern) {
        this.pattern = pattern;
        head = createPatternParser((pattern == null) ? DEFAULT_CONVERSION_PATTERN :
                pattern).parse();
    }
    protected PatternParser createPatternParser(String pattern) {
        return new PatternParser(pattern);
    }
    

    LoggingEvent 格式化时调用 PatternConverter#format 方法,PatternConverter 具体格式化的实现以后有时间再看一下。

    public String format(LoggingEvent event) {
        // Reset working stringbuffer
        if (sbuf.capacity() > MAX_CAPACITY) {
            sbuf = new StringBuffer(BUF_SIZE);
        } else {
            sbuf.setLength(0);
        }
    
        PatternConverter c = head;
        while (c != null) {
            c.format(sbuf, event);
            c = c.next;
        }
        return sbuf.toString();
    }
    

    六、日志配置器(org.apache.log4j.spi.Configurator)

    日志配置器提供外部配置文件配置 log4j 行为的 API,log4j 内建了两种实现:

    • Properties 文件方式(org.apache.log4j.PropertyConfigurator)
    • XML 文件方式(org.apache.log4j.xml.DOMConfigurator)

    每天用心记录一点点。内容也许不重要,但习惯很重要!

  • 相关阅读:
    单例模式
    建造者模式
    工厂方法模式
    原型模式
    适配器模式
    桥接模式
    装饰模式
    组合模式
    多线程的学习与GDI的学习
    我们复习.Net的这些日子里
  • 原文地址:https://www.cnblogs.com/binarylei/p/10788315.html
Copyright © 2020-2023  润新知