Logbackフレームワークにおいて、logback.xmlファイルの解析はJoranConfiguratorクラスが担っている。本記事では、設定ファイルが読み込まれてから内部データ構造に変換されるまでの処理フローを詳しく見ていく。
クラス階層と設計思想
設定処理に関わるクラスは以下のような継承関係を持っている。
ContextAware/ContextAwareBase: ログコンテキスト(LoggerContext)へのアクセスとステータスログの出力を担当GenericXMLConfigurator: XML設定の抽象基底クラス
Spring FrameworkにおけるApplicationContextAwareやEnvironmentAwareのように、接尾辞「Aware」を持つクラスは外部依存性の注入ポイントとして機能する。
設定読み込みの起点:doConfigure
GenericXMLConfiguratorクラスのdoConfigure(URL url)メソッドが解析処理の実質的なエントリーポイントとなる。
public abstract class GenericXMLConfigurator extends ContextAwareBase {
protected SaxEventInterpreter eventInterpreter;
protected ModelInterpretationContext interpretationContext;
private RuleStore ruleStore;
public final void doConfigure(URL configUrl) throws JoranException {
InputStream inputStream = null;
try {
// 設定ファイルの監視リストへ登録(ホットリロード用)
informContextOfURLUsedForConfiguration(getContext(), configUrl);
URLConnection connection = configUrl.openConnection();
connection.setUseCaches(false);
inputStream = connection.getInputStream();
// 解析処理の実行
doConfigure(inputStream, configUrl.toExternalForm());
} catch (IOException ex) {
addError("設定URLを開けませんでした: " + configUrl, ex);
throw new JoranException("設定読み込みエラー", ex);
} finally {
closeQuietly(inputStream);
}
}
}
SAXイベントによるXML解析
LogbackはSAX(Simple API for XML)パーサーを使用して設定ファイルを解析する。各XMLノードはSaxEventオブジェクトのシーケンスとして表現される。
public final void doConfigure(InputSource inputSource) throws JoranException {
context.fireConfigurationEvent(newConfigurationStartedEvent(this));
long startTime = System.currentTimeMillis();
// SAXイベントの記録
SaxEventRecorder recorder = populateSaxEventRecorder(inputSource);
List<SaxEvent> events = recorder.getSaxEventList();
if (events.isEmpty()) {
addWarn("SAXイベントリストが空です");
return;
}
// SAXイベントからModelを構築
Model rootModel = buildModelFromSaxEventList(events);
if (rootModel == null) {
addError(ErrorCodes.EMPTY_MODEL_STACK);
return;
}
// 構文チェック
sanityCheck(rootModel);
// Modelの処理
processModel(rootModel);
// 完了イベントの発行
StatusUtil statusUtil = new StatusUtil(context);
if (statusUtil.noXMLParsingErrorsOccurred(startTime)) {
registerSafeConfiguration(rootModel);
context.fireConfigurationEvent(newConfigurationEndedSuccessfullyEvent(this));
} else {
context.fireConfigurationEvent(newConfigurationEndedWithXMLParsingErrorsEvent(this));
}
}
SaxEventRecorderの実装
SaxEventRecorderはDefaultHandlerを実装し、SAXパーサーのコールバックを受け取る。
public class SaxEventRecorder extends DefaultHandler implements ContextAware {
final ElementPath currentPath;
List<SaxEvent> recordedEvents = new ArrayList<>();
public void recordEvents(InputSource source) throws JoranException {
SAXParser parser = buildSaxParser();
parser.parse(source, this);
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attrs) {
// 開始タグの処理
currentPath.push(qName);
recordedEvents.add(new StartEvent(currentPath, attrs));
}
@Override
public void characters(char[] chars, int start, int length) {
// テキストコンテンツの処理
recordedEvents.add(new BodyEvent(new String(chars, start, length)));
}
@Override
public void endElement(String uri, String localName, String qName) {
// 終了タグの処理
recordedEvents.add(new EndEvent(currentPath));
currentPath.pop();
}
}
XML解析手法の比較
| 特徴 | DOM4J | SAX | JSoup |
|---|---|---|---|
| 解析方式 | ツリーモデル | イベント駆動 | DOMライク |
| メモリ効率 | 高消費 | 低消費 | 中程度 |
| 大規模ファイル | 不向き | 最適 | 不向き |
| 変更機能 | あり | なし | あり |
| 外部依存 | 必要 | 標準API | 必要 |
Model構築とActionの関連付け
RuleStoreはXML要素のパスと処理を行うActionクラスのマッピングを管理する。
protected void buildSaxEventInterpreter(List<SaxEvent> events) {
// ルールストアの作成
RuleStore store = getRuleStore();
// パスとActionの関連付け
addElementSelectorAndActionAssociations(store);
// インタープリタの構築
this.eventInterpreter = new SaxEventInterpreter(
context, store, initialElementPath(), events
);
// 暗黙ルールの設定(未知の要素をプロパティとして扱う)
setImplicitRuleSupplier(eventInterpreter);
}
主要なパスとActionのマッピング
| 要素パス | Action | 生成されるModel | 処理Handler |
|---|---|---|---|
| configuration | ConfigurationAction | ConfigurationModel | ConfigurationModelHandlerFull |
| configuration/logger | LoggerAction | LoggerModel | LoggerModelHandler |
| configuration/root | RootLoggerAction | RootLoggerModel | RootLoggerModelHandler |
| */appender | AppenderAction | AppenderModel | AppenderModelHandler |
| */appender-ref | AppenderRefAction | AppenderRefModel | AppenderRefModelHandler |
| */property | PropertyAction | PropertyModel | PropertyModelHandler |
| */if | IfAction | IfModel | IfModelHandler |
DefaultProcessorによる2フェーズ処理
DefaultProcessorはModelの処理を2つのフェーズに分けて実行する。この設計により、要素間の依存関係を適切に解決できる。
public void processModel(Model model) {
// コンテキストの初期化
buildModelInterpretationContext();
interpretationContext.setTopModel(model);
interpretationContext.setConfiguratorHint(this);
// プロセッサの作成
DefaultProcessor processor = new DefaultProcessor(context, interpretationContext);
addModelHandlerAssociations(processor);
ReentrantLock lock = context.getConfigurationLock();
try {
lock.lock();
processor.process(model);
} finally {
lock.unlock();
}
}
public void process(Model model) {
if (model == null) {
addError("処理対象のModelがnullです");
return;
}
// 初期オブジェクトのプッシュ
initialObjectPush();
// フェーズ1: 依存関係を持たない要素の処理
mainTraverse(model, getPhaseOneFilter());
// 依存関係の分析
analyseDependencies(model);
// フェーズ2: 依存関係を持つ要素の処理
traversalLoop(this::secondPhaseTraverse, model, getPhaseTwoFilter(), "phase 2");
addInfo("設定処理完了");
finalObjectPop();
}
フェーズ分けの理由
以下のModelは他の要素への参照を持つため、フェーズ2で処理される。
LoggerModel/RootLoggerModel: appender-refへの参照AppenderModel: 他のAppenderへの参照可能性AppenderRefModel: 参照先Appenderの解決が必要
デフォルトコンポーネントの暗黙的解決
ModelInterpretationContextは、特定のプロパティに対してデフォルトの実装クラスを定義している。
| 親クラス | プロパティ | デフォルト実装 |
|---|---|---|
| AppenderBase | layout | PatternLayout |
| UnsynchronizedAppenderBase | encoder | PatternLayoutEncoder |
| SSLConfiguration | keyStore | KeyStoreFactoryBean |
| SSLConfiguration | trustStore | KeyStoreFactoryBean |
この仕組みにより、以下のようにclass属性を省略しても適切な実装が適用される。
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>logs/app.log</file>
<!-- class未指定時はPatternLayoutEncoderが適用される -->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
処理フローの全体像
logback.xmlの各要素は以下のパイプラインを通過する。
- SAX解析: XMLノード →
SaxEvent(Start/Body/End) - Model生成:
ActionによるSaxEvent→Model変換 - Model処理:
ModelHandlerによるModel→ 実際のログコンポーネント生成 - コンテキスト登録: 生成されたコンポーネントを
LoggerContextへ登録