Bootstrap

4.JoranConfigurator解析logbak.xml

一、前言

上一篇介绍了logback模块解析logback.mxl文件的入口, 我们可以手动指定logback.xml文件的位置, 也可以使用其它的名字, 本节我们继续讨论logback是如何解析logback.xml文件的。

二、源码解析

拿出我们上一节的继承图
在这里插入图片描述

其中ContextAwareContextAwareBase是有关日志上下文LoggerContext注入与打印启动日志的, 我们不介绍。

看到这个Aware结尾的有的同学可能会感觉到很有亲切感, 没错, spring中有很多这种Aware结尾的类, 例如ApplicationContextAwareEnvironmentAware等xxxAware, 它们都会提供一个setXxx的方法, 用来在框架启动的时候注入一个xxx对象。

GenericXMLConfigurator

public abstract class GenericXMLConfigurator extends ContextAwareBase {
    /**
     * SaxEvent解析器
     */
    protected SaxEventInterpreter saxEventInterpreter;

    /**
     * model解析器的上下文
     */
    protected ModelInterpretationContext modelInterpretationContext;
    
    /**
     * 配置文件节点路径和action的映射
     * 默认是SimpleRuleStore
     */
    private RuleStore ruleStore;
    
    public final void doConfigure(URL url) throws JoranException {
        InputStream in = null;
        try {
            // 1.给Context设置ConfigurationWatchList; 用于配置文件热更新
            informContextOfURLUsedForConfiguration(getContext(), url);
            URLConnection urlConnection = url.openConnection();
            // per http://jira.qos.ch/browse/LOGBACK-117  LBCORE-105
            // per http://jira.qos.ch/browse/LOGBACK-163  LBCORE-127
            urlConnection.setUseCaches(false);

            in = urlConnection.getInputStream();
            // 2.解析配置; url.toExternalForm():返回url表示的绝对路径的字符串形式
            doConfigure(in, url.toExternalForm());
        } catch (IOException ioe) {
            String errMsg = "Could not open URL [" + url + "].";
            // 打印解析异常日志
            addError(errMsg, ioe);
            throw new JoranException(errMsg, ioe);
        } finally {
            if (in != null) {
                try {
                    // 3.关闭流
                    in.close();
                } catch (IOException ioe) {
                    String errMsg = "Could not close input stream";
                    addError(errMsg, ioe);
                    throw new JoranException(errMsg, ioe);
                }
            }
        }
    }
}

这个方法就是开始解析logback.xml文件真正的入口了public final void doConfigure(URL url) throws JoranException

  1. informContextOfURLUsedForConfiguration方法用来设置动态热加载的配置文件, 也就是我们<configuration debug="true" scan="true" scanPeriod="10 second"> 这里动态刷新的默认文件
  2. doConfigure: 进一步解析

doConfigure(final InputSource inputSource)

直接从doConfigure跳过来看这个方法即可

public final void doConfigure(final InputSource inputSource) throws JoranException {
        // 发布配置开始事件
        context.fireConfigurationEvent(newConfigurationStartedEvent(this));
        long threshold = System.currentTimeMillis();

        // 1.解析日志配置文件; 例如logback.xml
        SaxEventRecorder recorder = populateSaxEventRecorder(inputSource);
        // 获取解析节点的结果; 每个节点都会解析成StartEvent, BodyEvent, EndEvent
        List<SaxEvent> saxEvents = recorder.getSaxEventList();
        if (saxEvents.isEmpty()) {
            addWarn("Empty sax event list");
            return;
        }
        // 2.根据xml节点的解析生成对应的model对象, top默认是configuration的model
        Model top = buildModelFromSaxEventList(recorder.getSaxEventList());
        if (top == null) {
            addError(ErrorCodes.EMPTY_MODEL_STACK);
            return;
        }
        // 3.语法检查
        sanityCheck(top);
        // 4.解析model节点(核心)
        processModel(top);

        // no exceptions at this level
        StatusUtil statusUtil = new StatusUtil(context);
        // 5.发布配置解析结束事件
        if (statusUtil.noXMLParsingErrorsOccurred(threshold)) {
            // xml解析无异常
            addInfo("Registering current configuration as safe fallback point");
            registerSafeConfiguration(top);
            context.fireConfigurationEvent(newConfigurationEndedSuccessfullyEvent(this));
        } else {
            // xml解析发生异常
            context.fireConfigurationEvent(newConfigurationEndedWithXMLParsingErrorsEvent(this));
        }


    }

方法小结

这里就是解析logback.xml的整个流程了, 编排了5个点

  1. populateSaxEventRecorder方法用来解析文件, 然后返回解析对象
  2. 根据解析logback.xml的结果生成对应的model
  3. 检查语法(不介绍)
  4. 解析model节点(核心)
  5. 发布解析完成事件(成功/失败)

logback.xml解析

populateSaxEventRecorder

public SaxEventRecorder populateSaxEventRecorder(final InputSource inputSource) throws JoranException {
    SaxEventRecorder recorder = new SaxEventRecorder(context);
    // sax解析配置文件, 每一个节点都会解析成SaxEvent, 分为StartEvent, BodyEvent, EndEvent
    recorder.recordEvents(inputSource);
    return recorder;
}

SaxEventRecorder

public class SaxEventRecorder extends DefaultHandler implements ContextAware {
    // 节点路径
    final ElementPath elementPath;
    // 解析节点得到的结果对象
    List<SaxEvent> saxEventList = new ArrayList<SaxEvent>();
    
    public void recordEvents(InputSource inputSource) throws JoranException {
        // 创建sax解析器
        SAXParser saxParser = buildSaxParser();
        try {
            // sax解析; 当前类SaxEventRecorder也是个DefaultHandler
            saxParser.parse(inputSource, this);
            return;
        } catch (xxxException ie) {
            // ...异常处理
        }
        throw new IllegalStateException("This point can never be reached");
    }
    // 解析节点的起始标签
    public void startElement(String namespaceURI, String localName, String qName, Attributes atts) {
        // ...
    }
    // 解析标签内容部分
    public void characters(char[] ch, int start, int length) {
        // ...
    }
    // 解析到标签结尾部分时触发
    public void endElement(String namespaceURI, String localName, String qName) {
     	// ...   
    }
}

方法小结

  1. SaxEventRecorder对象用来封装解析logback.xml的逻辑, 同时它也是一个DefaultHandler对象, 负责处理每个节点的具体解析逻辑
  2. 使用sax解析logback.xml文件
  3. 每个节点解析结果存放在实例变量saxEventList中

关于常见的xml解析框架的对比如下

特性DOM4JSAXJSOUP
解析方式基于树模型,加载整个文档到内存基于事件驱动,逐行解析类似 DOM 树模型,专注于 HTML/XML
性能性能较高,但文件过大时内存消耗明显性能最高,适合大文件解析性能适中,适合中小型 XML 或 HTML 文档
内存占用较高,依赖于内存加载整个文档最低,仅在解析时占用较少内存较高,但通常适合处理网页等较小文件
功能支持强大的 XPath 支持,支持修改和生成 XML只支持读取,不能修改文档支持解析和修改文档,HTML/XML 均适用
易用性较高,API 友好较低,需要手动处理事件逻辑非常高,简洁的 API,类似 jQuery 操作
修改能力支持动态修改不支持修改支持动态修改,灵活度高
适用场景适合处理中小型 XML 文件适合处理超大文件或流式读取适合处理 HTML/XML 文件,尤其是网页解析
依赖性需要引入额外依赖(如 dom4j jar 包)无需额外依赖,Java 内置支持需要引入 jsoup jar 包
  • DOM4J:功能全面,支持 XPath,适合需要频繁修改 XML 的场景,但处理超大文件时会占用大量内存。

  • SAX:性能最佳,占用内存最少,适合超大文件的解析,但使用复杂且无法修改文档内容。

  • JSOUP:偏向网页内容解析,API 简单易用,支持 XML 和 HTML 的解析和修改,适合中小型文件处理。

通过SaxEvent构建节点model

buildModelFromSaxEventList

// 通过节点的saxEvent构建节点的model
public Model buildModelFromSaxEventList(List<SaxEvent> saxEvents) throws JoranException {
    // 构建saxEvent解析器
    buildSaxEventInterpreter(saxEvents);
    // 解析节点, 解析节点的顺序是StartEvent, BodyEvent, EndEvent, 最终使用
    playSaxEvents();
    Model top = saxEventInterpreter.getSaxEventInterpretationContext().peekModel();
    return top;
}

// 构建saxEvent解析器, 并添加标签路径对应的action
protected void buildSaxEventInterpreter(List<SaxEvent> saxEvents) {
    // 1.创建ruleStore, 默认是SimpleRuleStore
    RuleStore rs = getRuleStore();
    // 2.将路径和对应的解析对象action绑定
    addElementSelectorAndActionAssociations(rs);
    // 3.构建saxEvent解析器
    this.saxEventInterpreter = new SaxEventInterpreter(context, rs, initialElementPath(), saxEvents);
    // 给saxEvent解析器上下文添加context
    SaxEventInterpretationContext interpretationContext = saxEventInterpreter.getSaxEventInterpretationContext();
    interpretationContext.setContext(context);

    // 4.给没有指定action的标签路径添加默认action; 这里是ImplicitModelAction, 用来解析ruleStore规则之外的标签, 给父标签对象添加属性用
    setImplicitRuleSupplier(saxEventInterpreter);
}

方法小结

  1. 创建ruleStore, 默认是SimpleRuleStore
  2. 将路径和对应的解析对象action绑定
  3. 构建saxEvent解析器
  4. 给没有指定action的标签路径添加默认action; 这里是ImplicitModelAction, 用来解析ruleStore规则之外的标签, 给父标签对象添加属性用

具体的解析逻辑在saxEvent解析器SaxEventInterpreter中, saxEvent经过action处理之后会得到对应标签节点的model对象

解析model节点

public void processModel(Model model) {
    // 1.创建ModelInterpretationContext并添加默认对象; 当一个标签没有指定class时, 会从这里尝试获取
    buildModelInterpretationContext();
    // configuration节点
    this.modelInterpretationContext.setTopModel(model);
    modelInterpretationContext.setConfiguratorHint(this);
    // 2.创建解析model的核心类
    DefaultProcessor defaultProcessor = new DefaultProcessor(context, this.modelInterpretationContext);
    // 3.将model与modelHandler和Analyser关联
    addModelHandlerAssociations(defaultProcessor);

    // disallow simultaneous configurations of the same context
    ReentrantLock configurationLock = context.getConfigurationLock();

    try {
        configurationLock.lock();
        // 开始解析model
        defaultProcessor.process(model);
    } finally {
        configurationLock.unlock();
    }
}

方法小结

  1. 构建model解析时的上下文ModelInterpretationContext, 并添加默认标签的class类(如下面的表格)
  2. 创建解析model的核心类DefaultProcessor
  3. 将model和对应的处理类(modelHandler)关联起来
  4. 使用DefaultProcessor解析model

默认标签的属性对应的类

通过buildModelInterpretationContext方法添加在ModelInterpretationContext.DefaultNestedComponentRegistry属性中

属性默认值
AppenderBaselayoutPatternLayout.class
UnsynchronizedAppenderBaselayoutPatternLayout.class
AppenderBaseencoderPatternLayoutEncoder
UnsynchronizedAppenderBaseencoderPatternLayoutEncoder
SSLComponentsslSSLConfiguration
SSLConfigurationparametersSSLParametersConfiguration
SSLConfigurationkeyStoreKeyStoreFactoryBean
SSLConfigurationtrustStoreKeyStoreFactoryBean
SSLConfigurationkeyManagerFactoryKeyManagerFactoryFactoryBean
SSLConfigurationtrustManagerFactoryTrustManagerFactoryFactoryBean
SSLConfigurationsecureRandomSecureRandomFactoryBean

例如下面的配置, 由于FileAppender是UnsynchronizedAppenderBase的子类, 并且encoder节点没有指定class, 而encoder是UnsynchronizedAppenderBase的一个属性, 所以这里取默认值PatternLayoutEncoder

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>logs/app.log</file>
    <!-- encoder不指定class的时候, 默认使用的是PatternLayoutEncoder -->
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n</pattern>
    </encoder>
</appender>

我们常用的也就appender标签下的这两个layout和encoder属性

各标签节点路径以及其对应的model和modelHandler如下表格

一般情况下我们看节点路径和action以及handler就行, 这个handler就是用来处理logback.xml中各个标签的类。
这里的节点路径就是我们logback.xml文件中所有可以定义的节点了。

标签节点路径解析路径节点的Actionaction解析之后生成的model解析model的Handler
configurationConfigurationActionConfigurationModelConfigurationModelHandlerFull
configuration/contextNameContextNameActionContextNameModelContextNameModelHandler
configuration/contextListenerLoggerContextListenerActionLoggerContextListenerModelLoggerContextListenerModelHandler
configuration/insertFromJNDIInsertFromJNDIActionInsertFromJNDIModelInsertFromJNDIModelHandler
configuration/loggerLoggerActionLoggerModelLoggerModelHandler
configuration/logger/levelLevelActionLevelModelLevelModelHandler
configuration/rootRootLoggerActionRootLoggerModelRootLoggerModelHandler
configuration/root/levelRootLoggerActionRootLoggerModelRootLoggerModelHandler
configuration/logger/appender-refAppenderRefActionAppenderRefModelAppenderRefModelHandler
configuration/root/appender-refAppenderRefActionAppenderRefModelAppenderRefModelHandler
configuration/includeIncludeActionIncludeModelIncludeModelHandler
configuration/propertiesConfiguratorPropertiesConfiguratorActionPropertiesConfiguratorModelPropertiesConfiguratorModelHandler
configuration/consolePluginConsolePluginAction
configuration/receiverReceiverActionReceiverModelReceiverModelHandler
*/variablePropertyActionPropertyModelPropertyModelHandler
*/propertyPropertyActionPropertyModelPropertyModelHandler
configuration/importImportActionImportModelImportModelHandler
configuration/timestampTimestampActionTimestampModelTimestampModelHandler
configuration/shutdownHookShutdownHookActionShutdownHookModelShutdownHookModelHandler
configuration/sequenceNumberGeneratorSequenceNumberGeneratorActionSequenceNumberGeneratorModelSequenceNumberGeneratorModelHandler
configuration/serializeModelSerializeModelActionSerializeModelModelSerializeModelModelHandler
configuration/defineDefinePropertyActionDefineModelDefineModelHandler
configuration/evaluatorEventEvaluatorActionEventEvaluatorModelEventEvaluatorModelHandler
configuration/conversionRuleConversionRuleActionConversionRuleModelConversionRuleModelHandler
configuration/statusListenerStatusListenerActionStatusListenerModelStatusListenerModelHandler
*/appenderAppenderActionAppenderModelAppenderModelHandler
configuration/appender/appender-refAppenderRefActionAppenderRefModelAppenderRefModelHandler
configuration/newRuleNewRuleAction
*/paramParamActionParamModelParamModelHandler
*/ifIfActionIfModelIfModelHandler
*/if/thenThenActionThenModelThenModelHandler
*/if/elseElseActionElseModelElseModelHandler
*/appender/siftSiftActionSiftModelSiftModelHandler
其它属性标签ImplicitModelActionImplicitModelImplicitModelHandler

DefaultProcessor解析model

public void process(Model model) {
	// 根节点为空, 直接异常
    if (model == null) {
        addError("Expecting non null model to process");
        return;
    }
    // 1.将LoggerContext添加到ModelInterpretationContext中, 这是第一个也是最底层的
    initialObjectPush();
    // 2.使用第一阶段过滤器过滤model, 将满足条件的model使用handler处理
    mainTraverse(model, getPhaseOneFilter());
    // 3.处理依赖
    analyseDependencies(model);
    // 4.使用第二阶段过滤器过滤model, 将满足条件的model使用handler处理
    traversalLoop(this::secondPhaseTraverse, model, getPhaseTwoFilter(), "phase 2");

    // 配置解析完成
    addInfo("End of configuration.");
    // 5.将LoggerContext从ModelInterpretationContext中弹出
    finalObjectPop();
}

方法小结

  1. 将日志上下文loggerContext放到model解析器中
  2. 使用第一阶段过滤器过滤model, 将满足条件的model使用handler处理, 不满足第一阶段过滤器的model有下面四个, 为什么这四个model这么特殊呢?? 因为它们需要依赖别的model(appender-ref标签)
  • LoggerModel
  • RootLoggerModel
  • AppenderModel
  • AppenderRefModel
  1. 确定依赖顺序
  2. 使用第二阶段过滤器过滤model, 将满足条件的model使用handler处理; 上面四个model将会在这里处理
  3. 弹出loggerContext节点

具体的model节点解析将会在下节挑出几个重点来介绍, 到这里, logback解析logback.xml文件的整体流程就介绍完了

关于一些细节以及JoranConfigurator和JoranConfiguratorBase中相关的内容没有详细介绍, 读者想要了解的更多建议看下源码

三、总结

  1. JoranConfigurator 是logback框架用来解析配置文件的核心类(logback.xml配置文件)
  2. logback.xml文件中每个节点都会被解析成一个saxEvent, 解析过程中借助解析器上下文SaxEventInterpretationContext保存相关信息
  3. RuleStore中映射了每个节点路径对应的action, action会创建对应节点的model
  4. DefaultProcessor中记录了每个model和modelHandler的映射关系
  5. DefaultProcessor借助上下文对象ModelInterpretationContext将model分为两个阶段使用modelHandler进行处理
  6. 最后将处理结果都放到了日志上下文LoggerContext中(这是我们打印日志的重要对象)
  7. 流程就是(eg. configuration/appender/encoder -> saxEvent -> action -> model -> modelHandler -> loggerContext)
;