第11章 Joran
答えは風に吹かれている。答えは風に吹かれている。
—BOB DYLAN, The Freewheelin' Bob Dylan
Joranとは、ジュネーブ湖で一年中吹きすさんでいる冷たい北西風のことです。ジュネーブ湖は西ヨーロッパの中央からやや右側にある湖で、ヨーロッパにいくつもある他の湖と比べるとずっと狭い湖です。しかし、平均水深は153メートルと非常に深く、西ヨーロッパ最大の淡水湖として知られています。
これまでの章で説明したように、logbackはJoran設定フレームワークの成熟した、柔軟で、強力な機能を頼りにしています。logbackのモジュールが提供する機能の大部分は、Joran無しでは実現できません。この章では、Joranの設計の根幹部分と、顕著な特徴に焦点を当てていきます。
Joranはロギングとは完全に無関係な、汎用設定システムです。この点を明らかにするため、logback-coreモジュールにはロガーに関わる要素が一切存在しないことに触れておかなければなりません。また、本章に登場するほとんどの例に、ロガーもアペンダーもレイアウトも出てこないこともそれを後押ししています。
この章で使っている例はLOGBACK_HOME/logback-examples/src/main/java/chapters/onJoranに配置されています。
Joranのインストールは簡単です。logbackをダウンロードして、クラスパスにlogback-core-1.1.2.jarを追加するだけです。
歴史的な観点
リフレクションは、宣言的にソフトウェアシステムを設定できるようにしてくれる、Java言語の強力な機能です。たとえば、EJBの重要なプロパティの多くはejb.xmlで設定します。EJBがJavaで実装されているとしても、それらのプロパティのほとんどがejb.xmlで指定されるのです。同じように、logbackの設定もXML形式の設定ファイルで指定します。JDK1.5から導入されたアノテーションは、以前ならXMLで設定されていたものを置き換えるためにEJB3.0で多用されています。Joranもアノテーションは利用しますが、ほんの少しだけです。EJBに比べてlogbackの設定には動的な要素が多いので、Joranでアノテーションを活用できる範囲が限られてしまうのです。
logbackの前身のlog4jでは、DOMConfigurator
(log4j1.2.x以降に含まれています)がXML形式の設定ファイルをパースするために使われていました。DOMConfigurator
は、設定ファイルの構造を変えるたびに、コードを微調整しなければならない作りになっていました。修正したコードは、再コンパイルして再デプロイしなければなりません。同じくらい重要なのが、DOMConfigurator
のコードは、たくさんのまばらなif/else文を含む子要素をループするような作りになっていたことです。たいして役に立ちませんでしたし、冗長であちこちに重複があるコードでした。このとき、Apace commons-digesterではパターンマッチ規則に基づいたXMLのパースができていました。digesterは、パース時に指定したパターンにマッチしたら、こちらも指定されたルールを適用します。たいていのルールクラスは小さくて、1つのことしかやっていませんでした。そのため、理解するのも保守するのも比較的簡単でした。
私たちはDOMConfigurator
の経験を武器にしてlogbackで使うための設定フレームワークJoran
の開発を始めました。Joranはcommons-digesterに強く影響を受けています。にもかかわらず、使っている用語が若干異なります。たとえば、commons-digester だとルールとパターンは一緒に使うものでした。DigesterのaddRule(String pattern, Rule rule)
メソッドがいい例です。私たちは、ルールがルールによって構成されている(再帰的という意味ではなく別の意味で)と、不必要な混乱を招くことに気づきました。そこで、Joranではルールがパターンとアクションで構成されるものとしました。アクションとは、対応するパターンがマッチしたときに行われる操作です。パターンとアクションの関係が、Joranの中核を為していると言ってもおかしくありません。それが顕著にあらわれているのが、単純なパターンを使って非常に複雑な要件を満たすことができるというところです。具体的には正確なマッチングとワイルドカードのマッチングによって実現されています。
SAXかDOMか
SAXのAPIはイベントベースのアーキテクチャなので、SAXをベースにしたツールで前方参照を扱うのは決して簡単なことではありません。前方参照とは、現在の要素よりも後で定義される要素を参照することです。同様に循環参照も手強い相手です。一般的な話ですが、DOM APIなら全要素を検索対象にできるし、前方の要素にジャンプすることもできるのです。
こういった柔軟性についてのあれこれを鑑みて、Joranでは当初DOM APIを使ってパースしていました。試行錯誤の後、パターンとアクションの形式で表現されたルールを解釈する上で、DOMツリーをパースしてる途中で離れた要素にジャンプできても意味が無いことが明らかになりました。Joranに必要だったのは、XMLドキュメントの要素を、深さ優先で逐次的に走査していくことだけだったのです。
また、SAX APIには要素の位置を取得するものがあったので、Joranは問題のあった行番号と列番号を表示できるようにもなりました。位置情報があれば、パースエラーの起きている場所を簡単に特定することができます。
対象外事項
高度な可変性を求められていることもあり、JoranのAPIは数千要素にもなる巨大なXMLドキュメントを扱うようには設計されていません。
パターン
Joranのパターンとは基本的に文字列です。正確なパターンとワイルドカードパターンの2種類があります。パターン"a/b" は、最上位要素a
にネストされた要素b
にマッチします。他の要素にはマッチしないことから、これは正確なパターンだと言えます。
ワイルドカードは、接尾辞または接頭辞をマッチさせるときに使われます。たとえばパターン"*/a" は接尾辞が"a"であるものすべてにマッチします。つまり、XMLドキュメント中で要素a
をネストしているあらゆる要素がマッチするのです。パターン"a/*"は接頭辞が"a"なので、要素a
がネストしているあらゆる要素がマッチすることになります。
アクション
前に述べたように、Joranはパターンに関連付けられたルールをパースします。アクションは、Action
クラスを継承したもので、次のような抽象メソッドで肉付けします。他のメソッドは簡潔にするために省略されています。
package ch.qos.logback.core.joran.action; import org.xml.sax.Attributes; import ch.qos.logback.core.joran.spi.ExecutionContext; public abstract class Action { /** * 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()
メソッドを実装しなければなりません。body()
メソッドを実装するかどうかは選択可能ですが。Action
クラスは空実装を提供しているからです。
RuleStore
前述のように、パターンマッチと関連するアクションの実行がJoranの中心的な考え方です。ルールはパターンとアクションを関連付けるものです。そしてRuleStoreに保存されます。
前述したとり、JoranはSAX APIを使っています。SAX APIとは、XMLドキュメントのそれぞれの要素についてstart/body/endというイベントを生成しながらパースを進めていくものです。Joranコンフィギュレーターは、イベントを受け付けると今のパターンに対応したアクションをルールストアから探してきます。たとえば、最上位要素Aにネストされた要素Bのstart/body/endイベントなら、今のパターンとは"A/B"になります。今のパターンとは、JoranがSAXイベントを受け付けながら自動的に調整するデータ構造なのです。
今のパターンにいくつかのルールがマッチするときは、正確なマッチが接尾辞マッチより優先されます。そして接尾辞マッチは接頭辞マッチより優先されます。実装の詳細についてはSimpleRuleStoreのを参照してください。
解釈コンテキスト
いろいろなアクションが協調して動作させるため、beginメソッドとendメソッドの一つ目の引数に解釈コンテキストが渡されます。解釈コンテキストには、オブジェクトスタック、オブジェクトマップ、エラーリスト、アクションを呼び出したJoranインタプリタへの参照が含まれています。解釈コンテキストの完全なフィールドが知りたければInterpretationContext
を見てください。
アクションは、共通のオブジェクトスタックに対するフェッチ、プッシュ、ポップといった操作や、共通のオブジェクトマップに対してキーと共にオブジェクトをプットしたりフェッチしたりすることで、他のアクションと共同作業をすることができます。また、解釈コンテキストのStatusManager
にエラーを追加することで、問題が起きたことを報告することができます。
こんにちは
最初に、Joranを使うために必要最低限の構成を見てもらいます。HelloWorldAction
は、begin()
でコンソールに"Hello World"と出力するだけの小さなアクションです。XMLファイルはコンフィギュレーターでパースします。この章の説明用に、非常に小さくて単純なSimpleConfigurator
というコンフィギュレーターを用意しました。HelloWorld
アプリケーションはこれらの部品を全部使用します。
- ルールマップと
Context
を用意します HelloWorldAction
とhello-worldパターンを関連付けて、パースルールを用意しますSimpleConfigutator
を用意して、ルールマップを渡します- XMLファイルを引数として、コンフィギュレーターの
doConfigure()
メソッドを呼び出します - 最後に、もしあれば解釈コンテキストに蓄積されたステータスメッセージを出力します
hello.xmlには何もネストしていない1つの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
ルールストアに新しいルールを追加したり、XMLドキュメントを変更してみたり、新しいアクションを追加するなど、いろいろと試してみたくなったでしょう?
アクションの協調
共通のオブジェクトスタックを通じて協調して簡単な計算をするアクションが、logback-examples/src/main/java/joran/calculator/ディレクトリに入っています。
calculator1.xmlを見ると、literal要素
をネストしたcomputation要素
があるので見てみましょう。
例10:Calculatorの設定例(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
次のように出力されます。
The computation named [total] resulted in the value 3
上記のcalculator1.xmlは次のように解釈されます。
- computation要素のstartイベントが、"/computation" パターンとみなされます。
Calculator1
アプリケーションは"/computation"パターンとComputationAction1
を関連付けています。ですので、ComputationAction1
のインスタンスのbegin()
メソッドが実行されます。 literal要素のstartイベントが、"/computation/literal" パターンとみなされます。"/computation/literal" パターンは
LiteralAction
と関連付けられています。ですので、LiteralAction
のインスタンスのbegin()
メソッドが実行されます。また、literal要素のendイベントより、
LiteralAction
のインスタンスのend()
メソッドを実行されます。同様に、computation要素のendイベントより、
ComputationAction1
のインスタンスのend()
メソッドが実行されます。
ここで注目して欲しいのは、アクションがどのように協調しているのかということです。LiteralAction
は設定ファイルからリテラル値を読み取り、InterpretationContext
の保持しているオブジェクトスタックに登録します。オブジェクトスタックに登録された値は、他のアクションから読み書きすることができるようになります。ここでは、 ComputationAction1
のend()
メソッドが、オブジェクトスタックから値をポップして、出力しています。
次にcalculator2.xmlを見てみましょう。前の例よりも少し複雑で、面白いことをしています。
例10:Calculatorの設定例(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>
前の例と同じく、literal要素に対応するLiteralAction
は、解釈コンテキストのオブジェクトスタックに整数値を登録します。ここ(calculator2.xml)ではまず7と3が登録されています。add要素にはAddAction
が関連付けられています。これはオブジェクトスタックから二回整数値をポップして、それらを加算した結果をオブジェクトスタックにプッシュするものです。次のliteral要素に対応するLiteralActionは、オブジェクトスタックの一番上に整数値3をプッシュします。multiply要素にはMultiplyAction
が関連付けられています。これはオブジェクトスタックから二回整数値をポップして、それらをかけあわせた結果をオブジェクトスタックにプッシュするものです。ここでは、計算結果の30がオブジェクトスタックの一番上にプッシュされることになります。一番最後に、computation要素に関連付けられたComputationAction1(のend()メソッド)によって、オブジェクトスタックの一番上の値が出力されます。実行してみましょう。
java chapters.onJoran.calculator.Calculator1 src/main/java/chapters/onJoran/calculator/calculator2.xml
そうすると、次のように出力されます。
The computation named [toto] resulted in the value 30
暗黙的なアクション
ここまでに紹介してきたルールの定義は明示的なアクションと呼ばれます。現在のxml要素に対応するパターンとアクションのペアを、ルールストアから1つだけ取り出すことができるからです。しかし、高度な拡張性を備えたシステムにおいて、コンポーネントの種類は膨大な数になります。それゆえに、すべてのパターンに明示的なアクションを関連付けるのはとても面倒なことになるのです。
そうは言っても、高度な拡張性を備えたシステムなら、ルールに付随するコンポーネントそれぞれに結びついたルールを、循環して見つけることができます。そのようにルールを発見できるとすると、logbackの設定ファイルをパースする時点では未知のコンポーネントが含まれるコンポーネントを扱うことができるようになります。たとえば、Apacne Ant では、addFile
やaddClassPath
といったコンポーネントを発見するメソッドを使うことで、設定ファイルをパースする時点では未知のタグを含んだタスクを扱えるようになっています。Antはタスクの処理中に未知のタグを発見すると、タグ名に基づいたクラスのオブジェクトを生成し、タスクの実装クラスに宣言されているaddXメソッドを呼び出して、親のオブジェクトに登録します。
Joranは暗黙的なアクションとして同じような機能を実現しています。現在のパターンが明示的なアクションにマッチしなかった時のために、暗黙的なアクションの一覧を保持するようになっています。しかし、暗黙的なアクションを適用することが必ずしも適切ではない場合があります。そこで、Joranは暗黙的なアクションを実行する前に、現在の状況が妥当であるかどうかを確認するようになっています。Joranからの確認に対して、これから実行されようとするアクションが肯定を返すときだけ、アクションを呼び出します。この例外対応によって、複数の暗黙的なアクションを備えつつ、適切なアクションが無ければ何もしないことの両方のケースに対応できるのです。
暗黙的なアクションの作成例がlogback-examples/src/main/java/chapters/onJoran/implicitにあります。
PrintMe
アプリケーションでは、"/*/foo" パターンとNOPAction
アクションを関連付けています。このパターンは任意のfoo要素にマッチします。NOPAction
のbegin()
メソッドとend()
メソッドは、名前のとおり空っぽです。PrintMe
アプリケーションは、暗黙的なアクションの一覧にPrintMeImplicitActionも登録しています。PrintMeImplicitAction
は、printme属性にtrueを指定されたあらゆる要素について適用可能なアクションです。PrintMeImplicitAction
のisApplicable()
メソッドを見ておいてください。PrintMeImplicitAction
のbegin()
メソッドは、現在の要素の名前をコンソールに出力します。
暗黙的なアクションがどのように振る舞うのか、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>
実行してみましょう。
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"パターンに関連付けられているので、foo要素についてNOPAction
のbegin()
メソッドとend()
メソッドが実行されているはずです。そのため、foo要素についてはPrintMeImplicitAction
は呼び出されないのです。他の明示的なアクションがマッチしない要素について、PrintMeImplicitAction{\0}の
メソッドが呼び出されます。isApplicable()メソッドは、printme属性にtrueが指定されている場合にだけtrueを返します。したがって、最初のxyz要素とabc要素についてはtrueを返します。10行目の二つ目のxyz要素には適用可能なアクションがないので、内部エラーメッセージが出力されます。エラーメッセージはisApplicable()
StatusPrinter
のprint()メソッドが出力しています。
暗黙的なアクションの実装
logback-classic モジュールと logback-access モジュールのそれぞれの Joran コンフィギュレーターには、暗黙的なアクションが2つだけ含まれています。NestedBasicPropertyIA
とNestedComplexPropertyIA
です。
NestedBasicPropertyIA
は、プリミティブ型あるいはそのラッパークラス、列挙型、"valueOf" 規約に則った任意のクラスのプロパティに適用可能なアクションです。このアクションが適用可能なプロパティは基本型あるいは単純型と呼ばれています。"valueOf" 規約に則るというのは、java.lang.String
を引数とする静的メソッドvalueOf()
によってインスタンス化できる、ということです。Level
やDuration
、FileSize
がこの規約に従っています。
NestedComplexPropertyIA
は、NestedBasicPropertyIA
が適用できない場合に、かつ、オブジェクトスタックの一番上のオブジェクトに、現在の要素名に対応するセッターあるいはアダーメソッドがある場合に適用可能なアクションです。このアクションが適用可能なプロパティは、更に他のコンポーネントを内包することがあるので注意しましょう。このアクションが適用可能なプロパティは複雑型と呼ばれています。NestedComplexPropertyIA
が複雑型のプロパティを見つけると、ネストされたコンポーネントに対応する適切なクラスをインスタンス化して、親のコンポーネント(オブジェクトスタックの一番上のオブジェクト)に設定します。class属性でインスタンス化するクラスを指定することができます。class属性が指定されていない場合、次のいずれかを条件に従ってクラス名が決定されます。
- 親のオブジェクトのプロパティのクラスを決定する内部的なルールに基づいて決められたクラス名
- セッターメソッドの@DefaultClassアノテーションに指定あされ
- セッターメソッドの引数が公開コンストラクタを持つ具象クラスならそのクラス名
デフォルトのクラスマッピング
logback-classic では、親のクラスとプロパティ名に対応するデフォルトのクラスを規定した内部的なルールがあります。表にまとめました。
親クラス | プロパティ名 | ネストするコンポーネントのデフォルトクラス |
---|---|---|
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 |
これらは将来的に変更されるかもしれません。最新のルールについては、logback-classic モジュールのJoranConfiguratorのaddDefaultNestedComponentRegistryRules()
メソッドを参照してください。
logback-accessモジュールのルールも似たようなものです。ネストするコンポーネントのデフォルトクラスについては、パッケージ名を ch.qos.logback.classic から ch.qos.logback.access に読み替えてください。最新のルールについては、logback-access モジュールのJoranConfiguratorのaddDefaultNestedComponentRegistryRules()
メソッドを参照してください。
コレクション型のプロパティ
logbackの暗黙的なアクションは、単独の基本型プロパティ、複雑型プロパティに加えて、コレクション型のプロパティにも対応しています。ただし、セッターメソッドの代わりに、アダーメソッドを用意する必要があります。
その場で新しいルールを定義する
Joranには、XMLドキュメントを解釈している途中でも、Joranコンフィギュレーターに新しいルールを教えるためのアクションが含まれています。サンプルコードがlogback-examples/src/main/java/chapters/onJoran/newRuleディレクトリにあります。NewRuleCalculator
アプリケーションは、二つのルールを定義しています。1つは最上位要素を処理するためのもので、二つ目は動的に新しいルールを定義するためのものです。NewRuleCalculator
の関連するコードをピックアップしました。
ruleMap.put(new Pattern("*/computation"), new ComputationAction1()); ruleStore.addRule(new Pattern("/computation/newRule"), new NewRuleAction());
NewRuleAction
はlogback-coreの一部で、他のアクションと同じように動作します。パーサーがnewRule要素を見つけるたびに、このアクションのbegin()
メソッドとend()
メソッドが呼び出されます。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 chapters.onJoran.newRule.NewRuleCalculator src/main/java/chapters/onJoran/newRule/newRule.xml
次のように出力されます。
The computation named [toto] resulted in the value 30
これは元のCalculator1の出力とまったく同じです。