第 11 章乔兰

**我的朋友,答案在风中飞舞,答案在风中.

—鲍勃·迪伦(BOB DYLAN)

本章已过时,需要重写以说明 1.3 中发生的大量更改。

乔兰(Joran)代表西北冷风,时不时地在日内瓦湖上吹大风。日内瓦湖位于西欧中部,比许多其他欧洲湖泊要小。但是,它的平均深度为 153 米,异常深,按体积计恰好是西欧最大的甜水储备。

如前几章所述,logback 依赖于 Joran,Joran 是一个成熟,灵活且功能强大的配置框架。只有 Joran 才能提供回溯模块提供的许多功能。本章重点介绍 Joran,其基本设计及其显着 Feature。

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

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

要安装 Joran,只需download logback 并将* logback-core-1.3.0-alpha5.jar *添加到您的 Classpath 中。

Historical perspective

反射是 Java 语言的强大功能,因此可以声明性地配置软件系统。例如,使用* ejb.xml 文件配置了 EJB 的许多重要属性。尽管 EJB 是用 Java 编写的,但它们的许多属性是在 ejb.xml *文件中指定的。同样,可以在配置文件中以 XML 格式表示logback设置。 JDK 1.5 中可用的 Comments 以及 EJB 3.0 中大量使用的 Comments 替换了以前在 XML 文件中发现的许多指令。乔兰(Joran)还使用 Comments,但范围更小。由于 logback 配置数据的动态特性(与 EJB 相比),Joran 对 Comments 的使用受到了很大的限制。

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

有了DOMConfigurator的经验,我们开始开发Joran,这是一个用于logback的强大配置框架。乔兰(Joran)在很大程度上受到了“Commons-学生计划”的启发。但是,它使用的术语略有不同。在 commers-digester 中,可以将规则视为由模式和规则组成,如Digester.addRule(String pattern, Rule rule)方法所示。我们发现使规则由自身构成(而不是递归地但具有不同的含义)会不必要地造成混淆。在 Joran 中,规则由模式和动作组成。当对应的模式发生匹配时,将调用一个动作。模式与行动之间的这种关系是乔兰的核心。非常明显的是,可以使用简单的模式来处理相当复杂的要求,或者更精确地使用完全匹配和通配符匹配。

SAX 或 DOM?

由于 SAX API 的基于事件的体系结构,因此基于 SAX 的工具无法轻松处理前向引用,即对元素的引用比在当前要处理的元素之后定义。具有循环引用的元素同样有问题。更一般而言,DOM API 允许用户对所有元素执行搜索并进行向前跳转。

这种额外的灵 Active 最初使我们选择 DOM API 作为 Joran 的基础解析 API。经过一些实验,很快就清楚了,当解释规则以模式和动作的形式表达时,在解析 DOM 树时处理跳转到远处的元素是没有意义的。 仅需要按 Sequences,深度优先的 Sequences 为 Joran 提供 XML 文档中的元素.

此外,SAX API 提供了元素位置信息,该信息使 Joran 可以显示发生错误的确切行号和列号。位置信息在识别解析问题时非常方便。

Non goals

鉴于其高度动态的特性,Joran API 不能用于解析具有成千上万个元素的超大型 XML 文档。

Pattern

Joran 模式本质上是一个字符串。有两种模式,* exact wildcard *。模式“ a/b”可用于匹配嵌套在顶层<a>元素中的<b>元素。 “ a/b”模式将不匹配任何其他元素,因此“精确”匹配指定。

通配符可用于匹配后缀或前缀。例如,“ /a”模式可用于匹配以“ a”结尾的任何后缀,即 XML 文档中的任何<a>元素,但不嵌套在<a>内的任何元素。 “ a/”模式将匹配以<a>前缀的任何元素,即<a>元素中嵌套的任何元素。

Actions

如上所述,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 {
  /**
   * Called when the parser encounters an element matching a
   * {@link ch.qos.logback.core.joran.spi.Pattern Pattern}.
   */
  public abstract void begin(InterpretationContext ic, String name,
      Attributes attributes) throws ActionException;

  /**
   * Called to pass the body (as text) contained within an element.
   */
  public void body(InterpretationContext ic, String body)
      throws ActionException {
    // NOP
  }

  /*
   * Called when the parser encounters an endElement event matching a
   * {@link ch.qos.logback.core.joran.spi.Pattern Pattern}.
   */
  public abstract void end(InterpretationContext ic, String name)
      throws ActionException;
}

因此,每个动作都必须实现begin()end()方法。由于Action提供的 empty/nop 实现,因此body()方法的实现是可选的。

RuleStore

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

如上所述,Joran 构建在 SAX API 之上。解析 XML 文档时,每个元素都会生成与每个元素的开始,正文和结尾相对应的事件。当 Joran 配置器收到这些事件时,它将尝试在其规则存储中查找与* current pattern 对应的操作。例如,嵌套在顶层 A 元素中的元素 B *的开始,主体或结束事件的当前模式是“ A/B”。当前模式是 Joran 在接收和处理 SAX 事件时自动维护的数据结构。

当多个规则与当前模式匹配时,精确匹配将覆盖后缀匹配,而后缀匹配将覆盖前缀匹配。有关实现的确切详细信息,请参见SimpleRuleStore类。

Interpretation context

为了允许各种动作进行协作,begin 和 end 方法的调用将解释上下文作为第一个参数。解释上下文包括一个对象堆栈,一个对象 Map,一个错误列表以及对调用该动作的 Joran 解释器的引用。请参阅InterpretationContext类以获取解释上下文中包含的字段的确切列表。

动作可以通过从公共对象堆栈中获取,推送或弹出对象,或通过在公共对象图上放置和获取键对象来进行协作。通过在解释上下文的StatusManager上添加错误项,动作可以报告任何错误情况。

Hello world

本章的第一个示例说明了使用 Joran 所需的最小管道设置。该示例由一个名为HelloWorldAction的琐碎动作组成,该动作在调用begin()方法时在控制台上显示“ Hello World”。 XML 文件的解析是由配置程序完成的。在本章中,我们开发了一个非常简单的配置器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)并添加新操作来进行讨论。

Collaborating actions

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

  • calculator1.xml *文件包含一个computation元素和一个嵌套的literal元素。这是其内容。

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

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

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

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

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

will print:

名为[total]的计算得出的值为 3

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

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

  • 对应于\ 元素的开始事件转换为当前模式“/computation/literal”。给定“/computation/literal”模式与LiteralAction实例的关联,将调用该LiteralAction实例的begin()方法。

  • 同样,与\ 元素对应的结束事件触发相同LiteralAction实例的end()方法的调用。

  • 类似地,对应于\ 元素结尾的事件触发对ComputationAction1相同实例的end()方法的调用。

这里有趣的是动作协作的方式。 LiteralAction读取 Literals 值并将其压入InterpretationContext所维护的对象堆栈中。完成后,任何其他操作都可以弹出该值以读取或修改它。在此,ComputationAction1类的end()方法从堆栈中弹出值并打印出来。

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

示例 10 .:计算器配置文件(logback-examples/src/main/java/chapters/onJoran/calculator/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),在解释上下文堆栈的顶部。下一个 Literals 元素将导致 LiteralAction 在堆栈顶部压入一个值为 3 的整数。响应\ 元素,适当的MultiplyAction将弹出两个先前压入的整数,即 10 和 3,并计算其乘积。它将结果推入堆栈顶部 30.最后,作为对与\ 标记对应的结束事件的响应,ComputationAction1 将在堆栈的顶部打印对象。因此,运行:

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

will yield

名为[toto]的计算得出值 30

Implicit actions

到目前为止定义的规则称为显式操作,因为可以在规则存储中找到当前元素的模式/操作关联。但是,在高度可扩展的系统中,组件的数量和类型可能非常大,以至于将显式操作与所有模式相关联非常繁琐。

同时,即使在高度可扩展的系统中,也可以观察到将各个部分链接在一起的循环规则。假设我们可以识别出这样的规则,我们可以处理由编译时未知的子组件组成的组件(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)

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

  <xyz/>

  <foo printme="true"/>

</foo>

Running

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

yields:

要求打印元素[xyz]。要求打印元素[abc]。 20:33:43,750 | -c.q.l.c.joran.spi.Interpreter @ 10:9 中的错误-[xyz]没有适用的操作,当前模式为[[foo] [xyz]]

假定NOPAction实例与“ */foo”模式显式关联,则在\ 元素上调用NOPActionbegin()end()方法。 PrintMeImplicitAction不会为\ 的任何元素触发。对于其他元素,由于没有匹配的显式操作,因此调用PrintMeImplicitActionisApplicable()方法。仅当将 printme 属性设置为 true 的元素(即第一个 元素(但不包括第二个)和\ 元素)返回 true。第 10 行的第二个\ 元素,没有适用的操作,生成内部错误消息。该消息由PrintMe应用程序中的最后一条语句StatusPrinter.print调用打印。这解释了上面显示的输出(请参见上一段)。

实践中的隐式动作

logback-classic 和 logback-access 的各个 Joran 配置器仅包括两个隐式操作,即NestedBasicPropertyIANestedComplexPropertyIA

NestedBasicPropertyIA适用于其类型为原始类型(或java.lang包中的等效对象类型),枚举类型或任何遵循“ valueOf”约定的类型的属性。这样的属性被称为* basic simple *。如果某个类包含名为valueOf()的静态方法(以java.lang.String作为参数,并返回所讨论类型的实例),则该类遵循“ valueOf”约定。目前,LevelDurationFileSize类遵循此约定。

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

  • 有一个内部规则将父对象的属性与指定的类相关联

  • setter 方法包含一个@DefaultClass 属性,用于指定给定的类

  • setter 方法的参数类型是具有公共构造函数的具体类

默认类 Map

在经典的 logback 中,有一些内部规则将父类/属性名称对 Map 到默认类。这些在下表中列出。

Parent classproperty name默认嵌套类
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

此列表在将来的版本中可能会更改。有关最新规则,请参见经典的 backback JoranConfiguratoraddDefaultNestedComponentRegistryRules方法。

在 logback-access 中,规则非常相似。在嵌套组件的默认类中,ch.qos.logback.classic 包被 ch.qos.logback.access 代替。有关最新规则,请参阅 logback-access JoranConfiguratoraddDefaultNestedComponentRegistryRules方法。

属性集合

请注意,除了单个简单属性或单个复杂属性之外,logback 的隐式操作还支持属性的集合,无论它们是简单的还是复杂的。该属性由“ adder”方法指定,而不是 setter 方法。

快速制定新规则

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()方法将查找 pattern actionClass *属性。然后,它实例化相应的动作类,并将模式/动作关联作为新规则添加到 Joran 的规则存储中。

这是可以在 xml 文件中声明新规则的方式:

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

使用这样的 newRule 声明,我们可以将NewRuleCalculator转换为类似于我们先前看到的Calculator1应用程序。涉及计算,可以这样表示:

示例 10 ..:动态使用新规则的配置文件(logback-examples/src/main/java/chapters/onJoran/newrule/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 Java Chapters.onJoran.newRule.NewRuleCalculator src/main/java/chapters/onJoran/newRule/newRule.xml

yields

名为[toto]的计算得出值 30

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