第十一章:Joran

Joran 代表寒冷的西北风,常常猛烈的吹在日列瓦湖上。位于西欧中部的日列瓦湖,表面上看起来比其它许多欧洲的湖泊都要小。但是它的平均深度有 153 米,异常的深。并且,它是西欧最大的淡水湖。

正如前几章所示,logback 基于 Joran,一个成熟的,灵活的并且强大的配置框架。logback 提供的许多的功能,只能基于 Joran 来实现。这章将专注于 Joran 的基本设计以及一些明显的特征。

Joran 实际上是一个通用的配置系统,能够被独立用于日志记录。为了强调这一点,我们需要说明的是 logback-core 模块没有 logger 的概念。所以,本章的大多数示例与 logger,appender,layout 无关。

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

要安装 Joran,只需要下载,然后将 logback-core-1.3.0-alpha4.jar 放到类路径下。

历史回顾

反射是 Java 语言一个强大的特性,使得声明式的配置软件系统变成可能。例如,EJB 许多重要的属性都被配置在 ejb.xml 文件中。尽管 EJB 是用 Java 编写的,但是它们的许多属性都是通过 ejb.xml 来指定的。类似的,logback 也可以通过指定的 XML 格式的配置文件来进行设置。JDK 1.5 中的注解在 EJB 3.0 被大量使用用来替换之前 XML 文件中的许多指令。Joran 也会充分利用注解,但是使用的范围少的多。由于 logback 配置的动态特性 (相比 EJB),Joran 使用注解相当有限。

在 logback 它爹 log4j 中, DOMConfigurator 类是 log4j 1.2.x 以及以后的版本的一部分。也能够解析 XML 的配置文件。DOMConfigurator 的编写方式强迫开发人员在配置文件的结构每次发生改变时,需要重新调整代码。调整的代码需要重新编译并重新部署。同样重要的是,DOMConfigurator 的代码由循环组成,用于解析子元素,包含了许多 if/else 语句。这样的代码散发着冗余以及重复的味道。 commons-digester 告诉我们可以通过模式匹配规则来解析 XML 文件。在解析的时候,解析器会应用匹配了指定模式的规则。规则类通常比较小,并且具有专业性。因此,理解与维护相对简单。

有了 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 的解析器。在经过了一些实验之后,我们发现当解析规则通过模式以及动作表达时,在解析 DOM 树时处理相隔较远的元素没有意义。Joran 只需要 XML 文档中连续且深度优先顺序的元素。

而且,在发生错误时,SAX API 提供元素的位置信息可以让 Joran 去展示精确的行号与列号。位置信息在识别解析问题时非常方便。

非目标

考虑到它的高度动态特性,Joran API 没有打算去解析包含几千个元素的 XML 文档。

模式 (Pattern)

Joran 的模式本质上就是一个字符串。有两种形式的模式:exactwildcard。模式 "a/b" 可以用来匹配嵌套在 <a> 元素中的 <b> 元素。因为 exact 匹配的设置,"a/b" 模式不会匹配其它的元素。

wildcard 可以用来进行后缀与前缀匹配。例如,"*/a" 可以用来匹配任何以 "a" 结尾的后缀,也就是 XML 文档中任何 <a> 元素,但是不包含任何嵌套在 <a> 中的元素。"a/*" 将会匹配任何 <a> 开头的元素,即任何嵌套在 <a> 中的元素。

动作

正如之前提到的,Joran 解析规则由相关联的模式组成。动作继承了 Action 类,包含了如下的抽象方法。为了简单起见,其它的方法被隐藏了。

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;

  /**
   * 传递包含元素的 body (作为字符串) 参数
   */
  public void body(InterpretationContext ic, String body)
      throws ActionException {
    // NOP
  }

  /*
   * 当解析器遇到最后一个元素匹配
   * {@link ch.qos.logback.core.joran.spi.Pattern Pattern}.
   */
  public abstract void end(InterpretationContext ic, String name)
      throws ActionException;
}

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

规则存储

如前面提到的,根据匹配模式调用动作是 Joran 的核心概念。规则跟模式与动作相关联。规则被存储在 RuleStore 中。

根据之前提到的,Joran 建立在 SAX API 上。当 XML 文档被解析时,每个元素会生成对应 start、body、end 的事件。当 Joran 的配置器接收到这些事件时,它会根据当前模式去规则存储中查找对应的动作。例如,元素 B 的 start、body、end 事件的当前模式为 "A/B",内嵌在一个顶级元素 A 中。当 Joran 接收并处理 SAX 事件时,它会自动维护当前模式的数据结构。

当有几个规则匹配到当前模式时,精确匹配会比后缀匹配优先,后缀匹配会比前缀匹配优先。对于详细的实现细节,请查看 SimpleRuleStore 类。

解析上下文

为了允许多个动作相互协作,在调用 begin 与 end 方法时会包含解析上下文,作为第一个参数传递。解析上下文包含对象栈,对象映射,错误列表以及 Joran 调用动作时的一个引用。请查看 InterpretationContext 类中详细的字段列表。

动作可以通过对象栈获取,入栈,出栈操作,或者通过对象映射来放置、获取 key 来进行协作。动作可以在解析上下文的 StatusManager 上通过添加错误项来报告任何错误条件。

Hello world

这个章节中的第一个例子将会展示使用 Joran 所需要最小条件。这个例子由一个名为 HelloWorldAction 的动作组成。在调用它的 begin() 方法时会在控制台打印 "Hello World"。配置文件由解析器负责解析。为了实现本章的目的,我们实现了一个非常的简单的配置器 SimpleConfiguratorHelloWorld 应用会将下面这些结合在一起:

  • 创建一个规则与 Context 的映射

  • 创建一个与 hello-world 模式相关的解析规则,以及对应的 HelloWorldAction 实例

  • 创建一个 SimpleConfigutator,解析之前提到的规则映射。

  • 调用配置器的 doConfigure 方法,解析 XML 文件

  • 最后,将会收集上下文中的所有转态信息。如果有的话,将会打印

hello.xml 包含一个 \ 元素,没有任何其它的内置元素。详细的内容请查看 logback-examples/src/main/java/chapters/onJoran/helloWorld/ 文件夹中的内容。

通过 hello.xml 运行 HelloWorld 应用将会在控制台输出 "Hello World"。

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

强烈推荐你在规则存储中添加新的规则,更改 XML 配置 (hello.xml),以及添加新的动作。

动作相互合作

logback-examples/src/main/java/joran/calculator/ 文件夹包含了几个动作,为了完成简单的计算,它们通过共同的对象栈相互合作。

calculator1.xml 文件包含一个 computation 元素,内嵌了一个 literal 元素。如下:

Example: calculator1.xml

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

在应用 Calculator1 中,我们声明了各种解析规则 (模式与动作) 基于 XML 文档的内容一起合作来计算一个结果。

通过 calculator1.xml 运行 Calculator

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

将会输出:

The computation named [total] resulted in the value 3

解析 calculator1.xml 文档包含如下的步骤:

  • 开始事件对应的 \ 元素转换为当前模式 "/computation"。因为在 Calculator1 中我们为 "/computation" 模式关联了一个 ComputationAction1 实例。ComputationAction1 实例中的 begin() 方法将会被调用

  • 开始事件对应的 \ 元素转换为当前模式 "/computation/literal"。为 "/computation/literal" 关联了一个 LiteralAction 实例。LiteralAction 实例中的 begin() 方法将会被调用。

  • 同样的,结束事件对应的 \ 元素将会触发 ComputationAction1 实例中的 end() 方法的调用。

有意思的是动作相互合作的方式。LiteralAction 读取到一个字面值,并将其放到对象栈中,由 InterpretationContext 来维护。一旦完成,其它的动作可以获取该值或者对其进行更改。ComputationAction1 类的 end() 方法从栈中获取值,并打印。

下一个例子中, calculator2.xml 有点复杂,但是更加有趣。

有趣个鸡儿

Example:calculator2.xml

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

在之前的例子中,为了响应 \ 元素,LiteralAction 实例会将 value 属性对应的整数放到解析上下文中的栈顶。在这个例子中,也就是 calculator2.xml,这个值是 7 与 3。为了响应 \ 元素。AddAction 实例将会获取之前放进去的两个整数,计算它们的和,然后再放进去。如,在解析上下文栈顶的就是 10 (= 7 + 3)。下个 literal 元素将会让 LiteralAction 将会将整数 3 放入栈顶。为了响应 \ 元素,MultiplyAction 将会获取之前放入的两个整数 10 与 3,然后计算它们的乘积。它会将结果 30 放入栈顶。最后,为了响应结束事件对应的 \</computation> 标签,ComputationAction1 将会打印堆栈顶部的结果,因此,运行:

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

将会输出:

The computation named [toto] resulted in the value 30

默认动作

目前定义的队则都是被显示的动作调用。因为当前元素相关的模式/动作能够在规则存储中被找到。但是,在高度可以扩展的系统中,组件的数量跟类型都会非常多,因此对所有的模式都关联一个具体的动作将会变得十分的蛋疼。

同时,甚至在高扩展的系统中,可以看到重复的规则关联了不同的部分。如果我们可以识别这些规则,那么我们就可以在编译时处理由子组件组成的未知组件。例如,Apache Ant 有能力在编译时处理包含未知标签的任务,仅仅通过检查组件中的方法名是不是以 add 开头,像 addFile 或者 addClassPath 之类的。当 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 的元素有效。参见 PrintMeImplicitActionisApplicable() 方法。PrintMeImplicitActionbegin() 方法会在控制台打印当前元素的名字。

implicit1.xml XML 文档说明了默认动作是如何起作用的。

Example: implicit1.xml

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

  <xyz/>

  <foo printme="true"/>

</foo>

运行:

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

输出:

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" 实例相关联,NOPActionbegin()end() 方法在 \ 元素上被调用。PrintMeImplicitAction 不会在任何 \ 元素上触发。对于其它的元素,因为没有明确的动作可以匹配,所以 PrintMeImplicitActionisApplicable() 方法被调用。它只有在 printme 属性设置为 true 的时候才会返回 true。也就是第一个 \ 元素 (不是第二个) 与 \ 元素。第十行的第二个 \ 元素,没有可用的动作,所以生成了一个内部的错误信息。这个信息通过 PrintMe 的最后一行代码 StatusPrinter.print 来进行输出。这也解释了上面的输出。

在实践中使用默认动作

logback-classic 与 logback-access 各自的 Joran 配置器只包含两个默认的动作,叫做 NestedBasicPropertyIANestedComplexPropertyIA

NestedBasicPropertyIA 适用于任何属性的类型为原始类型 (或者 equivalent object type in the java.lang 包中的对象类型 ),枚举类,或者其它遵循 "valuesOf" 约定的类型。这些属性被称之为基本 或者简单 属性。如果一个类它包含一个名为 valueOf() 的静态方法,接受一个 java.lang.String 作为参数并且返回相关类型的实例,那么就说这个类遵循 "valueOf" 约定。目前,LevelDuration,以及 FileSize 类遵循这个约定。

NestedComplexPropertyIA 适用于 NestedBasicPropertyIA 不适用的情况,并且如果对象栈顶部的对象具有当前属性名的 set 与 add 方法,那么该属性名相当于当前元素的名称。这些属性可以反过来包含其它的组件。因此,这些属性可以说是有点复杂。由于复杂属性的存在,NestedComplexPropertyIA 将会为内部组件实例化一个合适的类,并且通过使用父组件以及内部元素名的 set/add 方法将其附加到父组件上 (在对象栈的顶部)。相应的类通过当前 (内置) 元素的 class 属性来指定。但是,如果没有指定 class 属性,那么将会根据是否满足以下其中之一的条件来使用默认的类名。

  1. 父对象属性指定的类有内部的规则相关联

  2. set 方法包含一个指定类的 @DefaultClass 属性

  3. set 方法的参数类型是一个含有共有构造方法的实体类

默认类映射

在 logback-classic,有一些内部的规则将父 类/属性 名映射为默认的类。这些规则如下表所示:

在以后的版本中,这个列表可能会发生改变。最新的规则,请查看 logback-classic 中 JoranConfigurator 中的 addDefaultNestedComponentRegistryRules 方法。

属性集

除了单个简单属性以及单个复杂属性外,logback 的默认动作支持属性集,它们可以是简单的或者复杂的。指定的属性通过 "add" 方法,而不是 set 方法。

动态添加新规则

Joran 包含一个允许 Joran 在解析 XML 文档的过程中,动态解析新规则的动作。示例代码,查看 logback-examples/src/main/java/chapters/onJoran/newRule/ 文件夹。在这个包中,NewRuleCalculator 仅仅设置两个规则,一个用来处理最顶层的元素,一个用来学习新规则。下面是 NewRuleCalculator 中相关的代码:

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 文件添加一条新规则:

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

使用 newRule 声明,我们可以看到 NewRuleCalculator 表现出跟之前看到的 Calculator1 同样的结果。包括计算在内,我们可以按照如下的方式进行表示:

Example: newRule.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>
java chapters.onJoran.newRule.NewRuleCalculator src/main/java/chapters/onJoran/newRule/newRule.xml

作者原文中的命令写了两个 java

输出:

The computation named [toto] resulted in the value 30

之前的结果一致。

运行原文中的示例并不会输出坐着所说的结果,需要去掉 \ 中的 \ 标签才可以

Last updated