Skip to content

Logback 手册 - 第十章:Joran

🏷️ Logback 手册


来源:https://logback.qos.ch/manual/onJoran.html
作者:Ceki Gülcü,Sébastien Pennec,Carl Harris
版权所有 © 2000-2022 QOS.ch Sarl

本文档采用 创作共享署名 - 非商业性使用 - 相同方式共享 2.5 许可证 授权。


答案,我的朋友,随风而去,答案就随风而去。

—BOB DYLAN,《自由转弯的鲍勃·迪伦》


该章节已过时,需要重新编写以适应 1.3 版本中正在发生的重大变化

Joran 代表着一股寒冷的西北风,偶尔在日内瓦湖上猛吹一阵。位于西欧中部的日内瓦湖的表面比许多其他欧洲湖泊要小。然而,它的平均深度达到 153 米,异常深邃,并且恰好是西欧最大的淡水储备。

正如前几章所示,logback 依赖于 Joran,这是一个成熟、灵活和强大的配置框架。logback 模块提供的许多功能都是基于 Joran 才能实现的。本章重点介绍 Joran 的基本设计和显著特点。

实际上,Joran 是一个通用的配置系统,可以独立于日志记录而使用。为了强调这一点,我们应该提到 logback-core 模块没有日志记录器的概念。因此,在本章中的大多数示例与日志记录器、追加器或布局无关。

本章中介绍的示例可以在 LOGBACK_HOME/logback-examples/​src/main/java/chapters/​onJoran/ 文件夹中找到。

要安装 Joran,只需 下载 logback,并将 logback-core-1.3.8.jar 添加到类路径中。

历史背景

反射是 Java 语言的一个强大特性,使得可以以声明方式配置软件系统。例如,许多 EJB 的重要属性是通过 ejb.xml 文件进行配置的。虽然 EJB 是用 Java 编写的,但它们的许多属性是在 ejb.xml 文件中指定的。同样,logback 的设置可以在配置文件中以 XML 格式表示。JDK 1.5 中提供的注解在 EJB 3.0 中广泛使用,取代了以前在 XML 文件中找到的许多指令。Joran 也使用了注解,但使用得较少。由于与 EJB 相比,logback 配置数据的动态性,Joran 对注解的使用相对较少。

在 log4j 中,logback 的前身,DOMConfigurator 类(作为 log4j 版本 1.2.x 及更高版本的一部分)也可以解析以 XML 编写的配置文件。DOMConfigurator 的编写方式迫使我们开发人员每次配置文件结构发生更改时都必须调整代码。修改后的代码必须重新编译和部署。同样重要的是,DOMConfigurator 的代码包含处理包含许多交错 if / else 语句的子元素的循环。人们不禁注意到,这段代码充满了冗余和重复。commons-digester 项目 已经向我们展示,可以使用模式匹配规则来解析 XML 文件。在解析时,digester 会应用与指定模式匹配的规则。规则类通常非常小而专用。因此,它们相对容易理解和维护。

基于对 DOMConfigurator 的经验,我们开始开发 Joran,这是一个强大的配置框架,用于 logback。Joran 在很大程度上受到了 commons-digester 项目的启发。然而,它使用稍微不同的术语。在 commons-digester 中,规则可以被看作由模式和规则组成,如 Digester.addRule(String pattern, Rule rule) 方法所示。我们认为将规则本身作为规则的一部分是不必要混乱的。在 Joran 中,规则由模式和操作组成。当发生与相应模式匹配的匹配时,将调用一个操作。模式和操作之间的这种关系是 Joran 的核心。非常显著的是,通过使用简单的模式,或者更准确地说是完全匹配和通配符匹配,可以处理相当复杂的需求。

SAX 还是 DOM?

由于基于事件的 SAX API 的体系结构,基于 SAX 的工具不能轻松处理前向引用,即对稍后定义的当前元素的引用。具有循环引用的元素同样存在问题。更一般地说,DOM API 允许用户对所有元素进行搜索并进行前向跳转。

这种额外的灵活性最初使我们选择 DOM API 作为 Joran 的底层解析 API。经过一些实验后,很快就明确了在解析 DOM 树时处理跳转到远程元素是没有意义的,尤其是当解释规则以模式和操作的形式表达时。Joran 只需要按顺序、深度优先的方式给出 XML 文档中的元素。

此外,SAX API 提供了元素位置信息,允许 Joran 显示出现错误的确切行和列号。位置信息在识别解析问题时非常有用。

非目标

由于其高度动态的特性,Joran API 不适用于解析具有成千上万个元素的非常大的 XML 文档。

模式

Joran 模式本质上是一个字符串。有两种类型的模式,即 完全匹配通配符。模式 "a/b" 可用于匹配嵌套在顶级 <a> 元素中的 <b> 元素。"a/b" 模式不会匹配任何其他元素,因此被称为 完全匹配

通配符可以用于匹配后缀或前缀。例如,模式 "*/a" 可用于匹配以 "a" 结尾的任何后缀,也就是 XML 文档中的任何 <a> 元素,但不包括嵌套在 <a> 内部的任何元素。模式 "a/*" 将匹配由 <a> 前缀的任何元素,也就是嵌套在 <a> 元素内部的任何元素。

操作

如上所述,Joran 解析规则由模式的关联组成。操作扩展了 Action 类,包括以下抽象方法。为简洁起见,省略了其他方法。

java
package ch.qos.logback.core.joran.action;

import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import ch.qos.logback.core.​joran.spi.InterpretationContext;

public abstract class Action extends ContextAwareBase {
  /**
   * 当解析器遇到与{@link ch.qos.logback.​core.joran.spi.Pattern Pattern}匹配的元素时调用。
   */
  public abstract void begin(InterpretationContext ic, String name,
      Attributes attributes) throws ActionException;

  /**
   * 用于传递包含在元素内部的正文(作为文本)。
   */
  public void body(InterpretationContext ic, String body)
      throws ActionException {
    // 空操作
  }

  /*
   * 当解析器遇到与{@link ch.qos.logback.​core.joran.spi.Pattern Pattern}匹配的 endElement 事件时调用。
   */
  public abstract void end(InterpretationContext ic, String name)
      throws ActionException;
}

因此,每个操作都必须实现 begin()end() 方法。body() 方法的实现是可选的,因为 Action 提供了一个空的实现。

RuleStore

如前所述,根据匹配模式调用操作是 Joran 中的一个核心概念。规则是模式和操作的关联。规则存储在 RuleStore 中。

正如上面提到的,Joran 是建立在 SAX API 之上的。当解析 XML 文档时,每个元素生成与每个元素的开始、内容和结束相对应的事件。当 Joran 配置器接收到这些事件时,它会尝试在其规则存储中找到与 当前模式 相对应的操作。例如,在顶级 A 元素内嵌套的 B 元素的开始、内容或结束事件的当前模式是 "A/B"。当前模式是由 Joran 自动维护的数据结构,随着它接收和处理 SAX 事件而更新。

当多个规则匹配当前模式时,精确匹配优先于后缀匹配,后缀匹配优先于前缀匹配。有关实现的详细信息,请参见 SimpleRuleStore 类。

Interpretation Context

为了使各种操作能够协作,开始和结束方法的调用将一个解释上下文作为第一个参数。解释上下文包括对象堆栈、对象映射、错误列表和对调用操作的 Joran 解释器的引用。请参见 InterpretationContext 类,了解解释上下文中包含的字段的确切列表。

操作可以通过从共享对象堆栈获取、推送或弹出对象,或者通过在共享对象映射中存储和获取带键的对象来进行协作。操作可以通过向解释上下文的 StatusManager 添加错误项来报告任何错误条件。

Hello World

本章的第一个示例演示了使用 Joran 所需的最小设置。该示例包含一个名为 HelloWorldAction 的简单操作,当调用其 begin() 方法时,它会在控制台上打印 "Hello World"。XML 文件的解析由配置器完成。为了本章的目的,我们开发了一个非常简单的配置器 SimpleConfiguratorHelloWorld 应用程序将所有这些部分结合在一起:

  • 它创建了一个规则映射和一个 Context
  • 它通过将 hello-world 模式与相应的 HelloWorldAction 实例关联来创建解析规则
  • 它创建了一个 SimpleConfigurator,将前面提到的规则映射传递给它
  • 然后,它调用配置器的 doConfigure 方法,将指定的 XML 文件作为参数传递
  • 最后一步是打印上下文中累积的状态消息(如果有的话)

hello.xml 文件包含一个没有其他嵌套元素的 <hello-world> 元素。有关确切内容,请参阅 logback-examples/​src/main/java/chapters/​onJoran/helloWorld/ 文件夹。

使用 hello.xml 文件运行 HelloWorld 应用程序将在控制台上打印 "Hello World"

bash
java chapters.onJoran.​helloWorld.HelloWorld src/main/java/chapters/​onJoran/helloWorld/​hello.xml

鼓励您在此示例中进行实验,通过向规则存储添加新规则、修改 XML 文档(hello.xml)和添加新操作。

Collaborating Actions

logback-examples/​src/main/java/joran/​calculator/ 目录包含了几个操作,这些操作通过共享对象堆栈相互协作,以完成简单的计算。

calculator1.xml 文件包含一个 computation 元素,其中嵌套一个 literal 元素。以下是其内容。

示例 10:第一个计算器示例(logback-examples/​src/main/​java/chapters/​onJoran/calculator/​calculator1.xml)

xml
<computation name="total">
  <literal value="3"/>
</computation>

Calculator1 应用程序中,我们声明了各种解析规则(模式和操作),它们共同协作根据 XML 文档的内容计算结果。

使用 calculator1.xml 运行 Calculator1 应用程序

bash
java chapters.onJoran.​calculator.Calculator1 src/main/java/chapters/​onJoran/calculator/​calculator1.xml

将打印:

txt
The computation named [total] resulted in the value 3

解析 calculator1.xml 文档(上面列出的)涉及以下步骤:

  • <computation> 元素对应的开始事件转换为当前模式 "/computation" 。由于在 Calculator1 应用程序中,我们将模式 "/computation"ComputationAction1 实例关联,因此会调用该 ComputationAction1 实例的 begin() 方法。

  • <literal> 元素对应的开始事件转换为当前模式 "/computation/literal" 。给定将 "/computation/literal" 模式与 LiteralAction 实例关联,因此会调用该 LiteralAction 实例的 begin() 方法。

  • 同样,与 <literal> 元素对应的结束事件触发同一个 LiteralAction 实例的 end() 方法的调用。

  • 类似地,与 </computation> 元素结束事件对应的事件触发 ComputationAction1end() 方法的调用。

有趣的是操作协作的方式。LiteralAction 读取一个字面值并将其推入由 InterpretationContext 维护的对象堆栈中。完成后,任何其他操作都可以弹出该值进行读取或修改。在这里,ComputationAction1 类的 end() 方法从堆栈中弹出值并打印它。

下一个示例,calculator2.xml 文件稍微复杂,但也更有趣。

示例 10: 计算器配置文件 (logback-examples/​src/main/java/chapters/​onJoran/calculator/​calculator2.xml)

xml
<computation name="toto">
  <literal value="7"/>
  <literal value="3"/>
  <add/>
  <literal value="3"/>
  <multiply/>
</computation>

与前面的示例类似,针对 <literal> 元素,适当的 LiteralAction 实例将在解释上下文的对象栈顶推送一个整数,该整数对应于 value 属性的值。在这个例子中,即 calculator2.xml,值为 7 和 3。针对 <add> 元素,适当的 AddAction 将弹出先前推送的两个整数,计算它们的和并将结果(即 10=7+3)推送到解释上下文的栈顶。下一个 literal 元素将导致 LiteralAction 将一个值为 3 的整数推送到栈顶。针对 <multiply> 元素,适当的 MultiplyAction 将弹出先前推送的两个整数,即 10 和 3,并计算它们的乘积。它将结果(即 30)推送到栈顶。最后,针对与 </computation> 标签相对应的结束事件,ComputationAction1 将打印栈顶的对象。因此,运行:

bash
java chapters.onJoran.​calculator.Calculator1 src/main/java/chapters/​onJoran/calculator/​calculator2.xml

将产生以下结果:

txt
The computation named [toto] resulted in the value 30

隐式操作

到目前为止定义的规则被称为显式操作,因为在规则存储中可以找到当前元素的模式 / 操作关联。然而,在高度可扩展的系统中,组件的数量和类型可能非常多,以至于为所有模式关联一个显式操作会变得非常繁琐。

同时,即使在高度可扩展的系统中,我们也可以观察到将各个部分联系在一起的重复规则。假设我们能够识别出这样的规则,我们就可以处理由编译时(logback)未知的子组件组成的组件。例如,Apache Ant 能够处理包含在编译时未知标签的任务,只需检查具有以 add 开头的方法的组件,如 addFileaddClassPath。当 Ant 在任务中遇到嵌入的标签时,它只需实例化与任务类的 add 方法签名匹配的对象,并将生成的对象附加到父级。

Joran 支持隐式操作的类似功能。Joran 保留了一个隐式操作列表,如果没有显式模式匹配当前模式,则应用这些隐式操作。但是,并非总是适用隐式操作。在执行隐式操作之前,Joran 会询问给定的隐式操作是否适用于当前情况。只有在操作回答肯定时,Joran 配置器才会调用(隐式)操作。请注意,这个额外的步骤使得支持多个隐式操作或者可能没有隐式操作成为可能,如果对于给定的情况没有适用的隐式操作。

您可以按照下一个示例中 logback-examples/src/main/java/chapters/onJoran/implicit 文件夹中的示例创建并注册自定义的隐式操作。

PrintMe 应用程序将 NOPAction 实例与模式 "*/foo" 关联起来,即任何名为 "foo" 的元素。正如其名称所示,NOPActionbegin()end() 方法为空。PrintMe 应用程序还在其隐式操作列表中注册了 PrintMeImplicitAction 的一个实例。PrintMeImplicitAction 适用于具有 printme 属性设置为 true 的任何元素。请参阅 PrintMeImplicitAction 中的 isApplicable() 方法。PrintMeImplicitActionbegin() 方法在控制台上打印当前元素的名称。

XML 文档 implicit1.xml 旨在说明隐式操作是如何起作用的。

示例 10:隐式规则的使用(logback-examples/src/main/java/chapters/onJoran/implicit/implicit1.xml)

xml
<foo>
  <xyz printme="true">
    <abc printme="true"/>
  </xyz>

  <xyz/>

  <foo printme="true"/>

</foo>

运行

bash
java chapters.onJoran.implicit.PrintMe src/main/java/chapters/​onJoran/implicit/​implicit1.xml

将产生以下结果:

txt
Element [xyz] asked to be printed.
Element [abc] asked to be printed.
20:33:43,750 |-ERROR in c.q.l.c.joran.spi.Interpreter@10:9 - no applicable action for [xyz], current pattern is [[foo][xyz]]

鉴于 NOPAction 实例明确关联了 "*/foo" 模式,将在 <foo> 元素上调用 NOPActionbegin()end() 方法。对于任何 <foo> 元素,都不会触发 PrintMeImplicitAction。对于其他元素,由于没有匹配的显式操作,将调用 PrintMeImplicitActionisApplicable() 方法。它只会对具有 printme 属性设置为 true 的元素返回 true,即第一个 <xyz> 元素(但不是第二个)和 <abc> 元素。对于第二个 <xyz> 元素,由于没有适用的操作,生成了一个内部错误消息。该消息由 StatusPrinter.print 调用打印,此为 PrintMe 应用程序中的最后一条语句。这解释了上面显示的输出(请参阅前面的段落)。

实践中的隐式操作

logback-classic 和 logback-access 的相应 Joran 配置器只包含两个隐式操作,即 NestedBasicPropertyIANestedComplexPropertyIA

NestedBasicPropertyIA 适用于属性类型为基本类型(或 java.lang 包中的等效对象类型)、枚举类型或符合 "valueOf" 约定的任何类型。这样的属性被称为 basicsimple 属性。如果一个类包含一个名为 valueOf() 的静态方法,该方法接受一个 java.lang.String 作为参数并返回该类型的实例,那么该类被认为符合 "valueOf" 约定。目前,LevelDurationFileSize 类遵循这个约定。

NestedComplexPropertyIA 操作适用于 NestedBasicPropertyIA 不适用的情况下,并且如果对象栈顶的对象具有与当前元素名称相等的属性的 setteradder 方法。请注意,这种属性本身可以包含其他组件。因此,这种属性被称为 complex 属性。在存在复杂属性的情况下,NestedComplexPropertyIA 将为嵌套组件实例化适当的类,并使用父组件的 setter / adder 方法和嵌套元素的名称将其附加到父组件(位于对象栈顶)。当前元素的类由(嵌套)当前元素的 class 属性指定。但是,如果缺少 class 属性,则如果满足以下任一条件,可以隐式推断出类名:

  1. 存在一个内部规则,将父对象的属性与指定的类关联起来。
  2. setter 方法包含一个 @DefaultClass 属性,指定了一个给定的类。
  3. setter 方法的参数类型是具有公共构造函数的具体类。

默认类映射

在 logback-classic 中,有一些内部规则将父类 / 属性名称对映射到默认类。这些规则列在下表中。

父类属性名称默认嵌套类
ch.qos.logback.core.AppenderBaseencoderch.qos.logback.classic.encoder.PatternLayoutEncoder
ch.qos.logback.core.UnsynchronizedAppenderBaseencoderch.qos.logback.classic.encoder.PatternLayoutEncoder
ch.qos.logback.core.AppenderBaselayoutch.qos.logback.classic.PatternLayout
ch.qos.logback.core.UnsynchronizedAppenderBaselayoutch.qos.logback.classic.PatternLayout
ch.qos.logback.core.filter.EvaluatorFilterevaluatorch.qos.logback.classic.boolex.JaninoEventEvaluator

这个列表可能会在未来的版本中发生变化。请参阅 logback-classic JoranConfiguratoraddDefaultNested​ComponentRegistryRules 方法以获取最新的规则。

在 logback-access 中,规则非常相似。在嵌套组件的默认类中,ch.qos.logback.classic 包被替换为 ch.qos.logback.access。请参阅 logback-access JoranConfiguratoraddDefault​NestedComponent​RegistryRules 方法以获取最新的规则。

属性集合

请注意,除了单个简单属性或单个复杂属性之外,logback 的隐式操作还支持属性的集合,无论是简单的还是复杂的。属性不是通过 setter 方法指定,而是通过一个 "adder" 方法指定。

动态添加规则

Joran 包含一个动作,允许 Joran 解释器在解释 XML 文档时动态学习新的规则。请参阅 logback-examples/​src/main/java/chapters/​onJoran/newRule/ 目录中的示例代码。在该包中,NewRuleCalculator 应用程序设置了两个规则,一个规则用于处理顶级元素,第二个规则用于学习新的规则。下面是 NewRuleCalculator 中的相关代码。

java
ruleMap.put(new Pattern("*/computation"), new ComputationAction1());
ruleStore.addRule(new Pattern("/computation/newRule"), new NewRuleAction());

NewRuleAction 是 logback-core 的一部分,工作方式与其他动作几乎相同。它有一个 begin()end() 方法,在解析器找到 newRule 元素时被调用。当调用时,begin() 方法查找 patternactionClass 属性。然后,它实例化相应的动作类,并将模式 / 动作关联作为新规则添加到 Joran 的规则存储中。

下面是如何在 XML 文件中声明新规则的示例:

xml
<newRule pattern="*/computation/literal"
          actionClass="chapters.​onJoran.calculator.​LiteralAction"/>

使用这样的 newRule 声明,我们可以使 NewRuleCalculator 的行为与之前看到的 Calculator1 应用程序相同。涉及计算的配置可以这样表示:

Example 10..: 使用动态添加规则的配置文件 (logback-examples/​src/main/java/chapters/​onJoran/newrule/​newRule.xml)

xml
<computation name="toto">
  <newRule pattern="*/computation/literal"
            actionClass="chapters.​onJoran.calculator.​LiteralAction"/>
  <newRule pattern="*/computation/add"
            actionClass="chapters.​onJoran.calculator.​AddAction"/>
  <newRule pattern="*/computation/multiply"
            actionClass="chapters.​onJoran.calculator.​MultiplyAction"/​>

  <computation>
    <literal value="7"/>
    <literal value="3"/>
    <add/>
  </computation>

  <literal value="3"/>
  <multiply/>
</computation>
bash
java java chapters.onJoran.​newRule.NewRuleCalculator src/main/java/chapters/​onJoran/newRule/​newRule.xml

输出为:

txt
The computation named [toto] resulted in the value 30

原始计算器示例 的输出相同。