第十一章:Joran
Joran 代表寒冷的西北風,常常猛烈的吹在日列瓦湖上。位于西歐中部的日列瓦湖,表面上看起來比其它許多歐洲的湖泊都要小。但是它的平均深度有 153 米,異常的深。并且,它是西歐最大的淡水湖。
正如前幾章所示,logback 基于 Joran,一個成熟的,靈活的并且強大的配置框架。logback 提供的許多的功能,只能基于 Joran 來實現(xiàn)。這章將專注于 Joran 的基本設計以及一些明顯的特征。
Joran 實際上是一個通用的配置系統(tǒng),能夠被獨立用于日志記錄。為了強調這一點,我們需要說明的是 logback-core 模塊沒有 logger 的概念。所以,本章的大多數(shù)示例與 logger,appender,layout 無關。
本章節(jié)中的示例可以在 LOGBACK_HOME/logback-examples/src/main/java/chapters/onJoran/ 文件夾下被找到。
要安裝 Joran,只需要下載,然后將 logback-core-1.3.0-alpha4.jar 放到類路徑下。
歷史回顧
反射是 Java 語言一個強大的特性,使得聲明式的配置軟件系統(tǒng)變成可能。例如,EJB 許多重要的屬性都被配置在 ejb.xml 文件中。盡管 EJB 是用 Java 編寫的,但是它們的許多屬性都是通過 ejb.xml 來指定的。類似的,logback 也可以通過指定的 XML 格式的配置文件來進行設置。JDK 1.5 中的注解在 EJB 3.0 被大量使用用來替換之前 XML 文件中的許多指令。Joran 也會充分利用注解,但是使用的范圍少的多。由于 logback 配置的動態(tài)特性 (相比 EJB),Joran 使用注解相當有限。
在 logback 它爹 log4j 中, DOMConfigurator 類是 log4j 1.2.x 以及以后的版本的一部分。也能夠解析 XML 的配置文件。DOMConfigurator 的編寫方式強迫開發(fā)人員在配置文件的結構每次發(fā)生改變時,需要重新調整代碼。調整的代碼需要重新編譯并重新部署。同樣重要的是,DOMConfigurator 的代碼由循環(huán)組成,用于解析子元素,包含了許多 if/else 語句。這樣的代碼散發(fā)著冗余以及重復的味道。 commons-digester 告訴我們可以通過模式匹配規(guī)則來解析 XML 文件。在解析的時候,解析器會應用匹配了指定模式的規(guī)則。規(guī)則類通常比較小,并且具有專業(yè)性。因此,理解與維護相對簡單。
有了 DOMConfigurator 的經驗,我們開始開發(fā) Joran,一個在 logback 中使用的、強大的配置框架。Joran 受到了 commons-digester 項目很大的啟發(fā)。但是,它使用了一個稍微不同術語。在 commons-digester 中,規(guī)則可以看做由模式和規(guī)則組成,如同 Digester.addRule(String pattern, Rule rule) 方法展示的一樣。我們發(fā)現(xiàn)一個不必要的困惑是規(guī)則包含自身,但是不是遞歸,而是有不同的含義。在 Joran 中,規(guī)則由模式以及動作組成。當相應的模式被匹配時,會調用一個動作。模式與動作的這種關系是 Joran 的核心。值得注意的是,可以使用簡單的模式來處理復雜的匹配,或者更確切的是說是使用精確匹配以及通配符匹配。
SAX 還是 DOM ?
由于 SAX API 是基于事件的結構,所以基于 SAX 的工具不能很好的處理前向引用,也就是引用元素被定義晚于當前元素被處理。循環(huán)引用元素也有同樣的問題。通常,DOM API 允許用戶在所有的元素上進行搜索,并且可以向前跳轉。
這種額外的靈活性導致我們在開始的時候選擇 DOM API 作為 Joran 的解析器。在經過了一些實驗之后,我們發(fā)現(xiàn)當解析規(guī)則通過模式以及動作表達時,在解析 DOM 樹時處理相隔較遠的元素沒有意義。Joran 只需要 XML 文檔中連續(xù)且深度優(yōu)先順序的元素。
而且,在發(fā)生錯誤時,SAX API 提供元素的位置信息可以讓 Joran 去展示精確的行號與列號。位置信息在識別解析問題時非常方便。
非目標
考慮到它的高度動態(tài)特性,Joran API 沒有打算去解析包含幾千個元素的 XML 文檔。
模式 (Pattern)
Joran 的模式本質上就是一個字符串。有兩種形式的模式:exact 與 wildcard。模式 "a/b" 可以用來匹配嵌套在 <a> 元素中的 <b> 元素。因為 exact 匹配的設置,"a/b" 模式不會匹配其它的元素。
wildcard 可以用來進行后綴與前綴匹配。例如,"*/a" 可以用來匹配任何以 "a" 結尾的后綴,也就是 XML 文檔中任何 <a> 元素,但是不包含任何嵌套在 <a> 中的元素。"a/*" 將會匹配任何 <a> 開頭的元素,即任何嵌套在 <a> 中的元素。
動作
正如之前提到的,Joran 解析規(guī)則由相關聯(lián)的模式組成。動作繼承了 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 (作為字符串) 參數(shù)
*/
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;
}
所以,每個動作必須實現(xiàn) begin() 與 end() 方法。body() 方法的實現(xiàn)是可選的,因為 Action 提供了一個空的實現(xiàn)。
規(guī)則存儲
如前面提到的,根據(jù)匹配模式調用動作是 Joran 的核心概念。規(guī)則跟模式與動作相關聯(lián)。規(guī)則被存儲在 RuleStore 中。
根據(jù)之前提到的,Joran 建立在 SAX API 上。當 XML 文檔被解析時,每個元素會生成對應 start、body、end 的事件。當 Joran 的配置器接收到這些事件時,它會根據(jù)當前模式去規(guī)則存儲中查找對應的動作。例如,元素 B 的 start、body、end 事件的當前模式為 "A/B",內嵌在一個頂級元素 A 中。當 Joran 接收并處理 SAX 事件時,它會自動維護當前模式的數(shù)據(jù)結構。
當有幾個規(guī)則匹配到當前模式時,精確匹配會比后綴匹配優(yōu)先,后綴匹配會比前綴匹配優(yōu)先。對于詳細的實現(xiàn)細節(jié),請查看 SimpleRuleStore 類。
解析上下文
為了允許多個動作相互協(xié)作,在調用 begin 與 end 方法時會包含解析上下文,作為第一個參數(shù)傳遞。解析上下文包含對象棧,對象映射,錯誤列表以及 Joran 調用動作時的一個引用。請查看 InterpretationContext 類中詳細的字段列表。
動作可以通過對象棧獲取,入棧,出棧操作,或者通過對象映射來放置、獲取 key 來進行協(xié)作。動作可以在解析上下文的 StatusManager 上通過添加錯誤項來報告任何錯誤條件。
Hello world
這個章節(jié)中的第一個例子將會展示使用 Joran 所需要最小條件。這個例子由一個名為 HelloWorldAction 的動作組成。在調用它的 begin() 方法時會在控制臺打印 "Hello World"。配置文件由解析器負責解析。為了實現(xiàn)本章的目的,我們實現(xiàn)了一個非常的簡單的配置器 SimpleConfigurator。HelloWorld 應用會將下面這些結合在一起:
- 創(chuàng)建一個規(guī)則與
Context的映射 - 創(chuàng)建一個與 hello-world 模式相關的解析規(guī)則,以及對應的
HelloWorldAction實例 - 創(chuàng)建一個
SimpleConfigutator,解析之前提到的規(guī)則映射。 - 調用配置器的
doConfigure方法,解析 XML 文件 - 最后,將會收集上下文中的所有轉態(tài)信息。如果有的話,將會打印
hello.xml 包含一個 <hello-world> 元素,沒有任何其它的內置元素。詳細的內容請查看 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
強烈推薦你在規(guī)則存儲中添加新的規(guī)則,更改 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 中,我們聲明了各種解析規(guī)則 (模式與動作) 基于 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> 元素轉換為當前模式 "/computation"。因為在
Calculator1中我們?yōu)?"/computation" 模式關聯(lián)了一個ComputationAction1實例。ComputationAction1實例中的begin()方法將會被調用 - 開始事件對應的 <litera> 元素轉換為當前模式 "/computation/literal"。為 "/computation/literal" 關聯(lián)了一個
LiteralAction實例。LiteralAction實例中的begin()方法將會被調用。 - 同樣的,結束事件對應的 <computation> 元素將會觸發(fā)
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>
在之前的例子中,為了響應 <literal> 元素,LiteralAction 實例會將 value 屬性對應的整數(shù)放到解析上下文中的棧頂。在這個例子中,也就是 calculator2.xml,這個值是 7 與 3。為了響應 <add> 元素。AddAction 實例將會獲取之前放進去的兩個整數(shù),計算它們的和,然后再放進去。如,在解析上下文棧頂?shù)木褪?10 (= 7 + 3)。下個 literal 元素將會讓 LiteralAction 將會將整數(shù) 3 放入棧頂。為了響應 <multiply> 元素,MultiplyAction 將會獲取之前放入的兩個整數(shù) 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
默認動作
目前定義的隊則都是被顯示的動作調用。因為當前元素相關的模式/動作能夠在規(guī)則存儲中被找到。但是,在高度可以擴展的系統(tǒng)中,組件的數(shù)量跟類型都會非常多,因此對所有的模式都關聯(lián)一個具體的動作將會變得十分的蛋疼。
同時,甚至在高擴展的系統(tǒng)中,可以看到重復的規(guī)則關聯(lián)了不同的部分。如果我們可以識別這些規(guī)則,那么我們就可以在編譯時處理由子組件組成的未知組件。例如,Apache Ant 有能力在編譯時處理包含未知標簽的任務,僅僅通過檢查組件中的方法名是不是以 add 開頭,像 addFile 或者 addClassPath 之類的。當 Ant 在任務內遇到一個內置的標簽,它僅僅實例化一個匹配了任務類 add 方法的簽名的對象,并且將結果對象附加到父級上。
Joran 通過默認動作的形式來提供類似的功能。Joran 保留了一系列的默認動作,如果當前模式沒有具體的模式可以匹配時,它們將會被應用。但是,應用默認的動作可能并不總是合適的。在執(zhí)行默認的動作之前,Joran 會詢問指定的動作當前的情況是否合適。只有動作返回肯定的回答,Joran 的配置器才會調用默認的動作。注意,這個額外的步驟可能支持多個默認的動作,如果在給定的情況下,沒有合適的默認動作,也可能一個都不支持。
你可以創(chuàng)建并注冊一個自定義的默認動作。見下一個示例。該示例位于 logback-examples/src/main/java/chapters/onJoran/implicit 文件夾下。
PrintMe 應用將一個 NOPAction 實例與 "*/foo" 模式相關聯(lián),也就是與名字叫做 "foo" 的任何元素。正如它的名字所示, NOPAction 的 begin() 與 end() 方法都為空。PrintMe 應用仍然會在它的默認動作列表注冊一個 PrintMeImplicitAction 的實例。PrintMeImplicitAction 對任何 printme 屬性為 true 的元素有效。參見 PrintMeImplicitAction 的 isApplicable() 方法。PrintMeImplicitAction 的 begin() 方法會在控制臺打印當前元素的名字。
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" 實例相關聯(lián),NOPAction 的 begin() 與 end() 方法在 <foo> 元素上被調用。PrintMeImplicitAction 不會在任何 <foo> 元素上觸發(fā)。對于其它的元素,因為沒有明確的動作可以匹配,所以 PrintMeImplicitAction 的 isApplicable() 方法被調用。它只有在 printme 屬性設置為 true 的時候才會返回 true。也就是第一個 <xyz> 元素 (不是第二個) 與 <abc> 元素。第十行的第二個 <xyz> 元素,沒有可用的動作,所以生成了一個內部的錯誤信息。這個信息通過 PrintMe 的最后一行代碼 StatusPrinter.print 來進行輸出。這也解釋了上面的輸出。
在實踐中使用默認動作
logback-classic 與 logback-access 各自的 Joran 配置器只包含兩個默認的動作,叫做 NestedBasicPropertyIA 與 NestedComplexPropertyIA。
NestedBasicPropertyIA 適用于任何屬性的類型為原始類型 (或者 equivalent object type in the java.lang 包中的對象類型 ),枚舉類,或者其它遵循 "valuesOf" 約定的類型。這些屬性被稱之為基本 或者簡單 屬性。如果一個類它包含一個名為 valueOf() 的靜態(tài)方法,接受一個 java.lang.String 作為參數(shù)并且返回相關類型的實例,那么就說這個類遵循 "valueOf" 約定。目前,Level,Duration,以及 FileSize 類遵循這個約定。
NestedComplexPropertyIA 適用于 NestedBasicPropertyIA 不適用的情況,并且如果對象棧頂部的對象具有當前屬性名的 set 與 add 方法,那么該屬性名相當于當前元素的名稱。這些屬性可以反過來包含其它的組件。因此,這些屬性可以說是有點復雜。由于復雜屬性的存在,NestedComplexPropertyIA 將會為內部組件實例化一個合適的類,并且通過使用父組件以及內部元素名的 set/add 方法將其附加到父組件上 (在對象棧的頂部)。相應的類通過當前 (內置) 元素的 class 屬性來指定。但是,如果沒有指定 class 屬性,那么將會根據(jù)是否滿足以下其中之一的條件來使用默認的類名。
- 父對象屬性指定的類有內部的規(guī)則相關聯(lián)
- set 方法包含一個指定類的 @DefaultClass 屬性
- set 方法的參數(shù)類型是一個含有共有構造方法的實體類
默認類映射
在 logback-classic,有一些內部的規(guī)則將父 類/屬性 名映射為默認的類。這些規(guī)則如下表所示:
| 父類 | 屬性名 | 默認類 |
|---|---|---|
| ch.qos.logback.core.AppenderBase | encoder | ch.qos.logback.classic.encoder.PatternLayoutEncoder |
| ch.qos.logback.core.UnsynchronizedAppenderBase | encoder | ch.qos.logback.classic.encoder.PatternLayoutEncoder |
| ch.qos.logback.core.AppenderBase | layout | ch.qos.logback.classic.PatternLayout |
| ch.qos.logback.core.UnsynchronizedAppenderBase | layout | ch.qos.logback.classic.PatternLayout |
| ch.qos.logback.core.filter.EvaluatorFilter | evaluator | ch.qos.logback.classic.boolex.JaninoEventEvaluator |
在以后的版本中,這個列表可能會發(fā)生改變。最新的規(guī)則,請查看 logback-classic 中 JoranConfigurator 中的 addDefaultNestedComponentRegistryRules 方法。
屬性集
除了單個簡單屬性以及單個復雜屬性外,logback 的默認動作支持屬性集,它們可以是簡單的或者復雜的。指定的屬性通過 "add" 方法,而不是 set 方法。
動態(tài)添加新規(guī)則
Joran 包含一個允許 Joran 在解析 XML 文檔的過程中,動態(tài)解析新規(guī)則的動作。示例代碼,查看 logback-examples/src/main/java/chapters/onJoran/newRule/ 文件夾。在這個包中,NewRuleCalculator 僅僅設置兩個規(guī)則,一個用來處理最頂層的元素,一個用來學習新規(guī)則。下面是 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 屬性。然后實例化相應的動作類,并將模式/動作作為一條新規(guī)則添加到 Joran 的規(guī)則存儲中。
下面是如何在 xml 文件添加一條新規(guī)則:
<newRule pattern="*/computation/literal"
actionClass="chapters.onJoran.calculator.LiteralAction"/>
使用 newRule 聲明,我們可以看到 NewRuleCalculator 表現(xiàn)出跟之前看到的 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
跟之前的結果一致。
運行原文中的示例并不會輸出坐著所說的結果,需要去掉 <computation> 中的 <computation> 標簽才可以