Apache Commons Logging のログ出力を抑制する

Apache Commons Logging Log4j java.util.logging など他のロガー実装に処理を委譲してくれるブリッジ ライブラリです。Apache Commons Logging API を使ってログを出力するようにコードが書かれていれば ログ出力に使う実装を java.util.logging から Log4j に切り替えるといったことが簡単にできます。

この Apache Commons Logging の仕組みは ライブラリ開発者がログ出力をするのに適しています。

こんな状況を思い浮かべてみましょう。開発者 A さんがライブラリ libA を開発しました。libA Log4j を使ってログを出力するように実装されています。開発者 B さんが libA を使ってアプリケーション appB を作りました。appB java.util.logging を使ってログを出力しますが libA からは Log4j でログが出力されるため 結果として 2 系統のログが混在してしまいます。

もしも libA Apache Commons Logging を使ってログを出力するように実装されていたら? 自分のアプリケーションに libA を組み込む開発者 B さんが libA で使用するロガー実装も切り替えることができます。つまり libA はログ出力に関して自由度があり 押しつけがましくないということです。

Apache Commons Logging と同じような位置付けのライブラリとして 他にも SLF4J があります。Simple Logging Facade for Java 略して SLF4J です。SLF4J では ファサード という言葉が使われていますが ブリッジと同じ意味と捉えて構いません。SLF4J も他のロガー実装に出力処理を委譲する抽象インターフェースを提供するライブラリです。

抽象化したインターフェースを提供するライブラリ
具体的なログ出力を実装しているライブラリ

Java のロギング ライブラリを分類するとこんな感じですね。

他の人に使ってもらうことを想定している汎用ライブラリの開発者さんは Log4j java.util.logging のような具体的なロガー実装を使わずに Apache Commons Logging または SLF4J のような抽象ブリッジを使ってログ出力を実装しておくのが良いと思います。

ログ出力を抑制したい

自分で出力しているログならどうにでもできますが 外部ライブラリが出力しているログだと制御するのが困難なことがあります。

私が PDFBox というライブラリを使ってアプリケーションを作っていたときのことです。アプリケーションを動かすと以下のようなログが出力されました。

5月 30, 2019 2:24:34 午後 org.apache.pdfbox.pdmodel.font.PDCIDFontType0 warn
警告: Using fallback YuMincho-Light for CID-keyed font Ryumin-Light
5月 30, 2019 2:24:34 午後 org.apache.fontbox.ttf.CmapSubtable warn
警告: Format 14 cmap table is not supported and will be ignored
5月 30, 2019 2:24:34 午後 org.apache.pdfbox.pdmodel.font.PDCIDFontType0 warn
警告: Using fallback IPAGothic for CID-keyed font GothicBBB-Medium
5月 30, 2019 2:24:34 午後 org.apache.pdfbox.pdmodel.font.PDSimpleFont warn
警告: No Unicode mapping for circlecopyrt (176) in font NECPCD+CMSY8

フォントにリュウミンがないから代わりに游明朝を使ったよ といった内容です。軽微な内容なので 私はこれらのログ出力を抑制したいと思いました。

PDFBox はログ出力に Apache Commons Logging を使用しています。

以下のコードを追加すると PDFBox のログ出力を抑制できる という情報をウェブで見つけました。

PDFBoxのログ出力を抑制する
java.util.logging.Logger.getLogger("org.apache.pdfbox").setLevel(Level.OFF);

たしかに このコードを追加すると PDFBox のログ出力が抑制されました。ですが このコードには違和感があります。

Apache Commons Logging を使っているのに 下位の java.util.logging を直接いじるのっておかしくない?

Apache Commons Logging では実際に使用するロガー実装を切り替えられるという話をしました。使用するロガーを Log4J に切り替えたら 上記の java.util.logging の設定は意味をなくし 再び PDFBox のログが出力される状態に戻ってしまいます。上記のログ抑制は ロガー実装として java.util.logging を選択していることを前提としているわけです。

ロガー実装に依存せずに Apache Commons Logging 側でログ出力をコントロールできるようにしたいと思いませんか?

Apache Commons Logging の仕組み

Apache Commons Logging ではロガー実装を切り替えることができます。ロガー実装を切り替える具体的な方法はどうなっているのでしょうか? その答えは Apache Commons Logging のガイドにありました。

commons-logging.properties ファイル または org.apache.commons.logging.Log システムプロパティで使用するロガーを指定することができます。これらの指定がされていない場合は以下の順序で自動的に使用するロガーが決定されます。

  • クラスパスに Log4J が含まれている場合は Log4J を使用します。
  • Java のバージョンが 1.4 以上であれば java.util.logging を使用します。
  • 上記に該当しなければ 標準出力を使用する簡易ロガー SimpleLog を使用します。

設定ファイルやシステムプロパティで明示的に指定していなければ クラスパスに Log4J が存在するかどうかといった構成のちょっとした違いで 選択されるロガーが変わってしまう可能性があるわけです。

Apache Commons Logging には org.apache.commons.logging.Log というインターフェースがあります。そして 設定や構成に応じて org.apache.commons.logging.Log インターフェースを使用する実装クラスが 1 つ決定されます。java.util.logging を使う場合は Jdk14Logger クラス Log4J を使う場合は Log4JLogger クラス といった具合です。実装クラスを決定する役割は LogFactory クラスが担っています。

org.apache.commons.logging.Log インターフェースを実装した具象クラスによって委譲するロガー実装が変わる仕組みです。上手くできていますね。

残念ながら Apache Commons Logging でログ出力レベルをコントロールする方法は見つかりませんでした。どうやら ログ出力レベルのコントロールは各ロガー実装に任せるというコンセプトになっているようです。

java.util.logging に委譲する Jdk14Logger クラスのソースコードは以下のようになっており ログを出力するかどうかの判定を java.util.logging.Logger#isLoggable メソッドに委ねています。

protected void log( Level level, String msg, Throwable ex ) {
	Logger logger = getLogger();
	if (logger.isLoggable(level)) {
		
		(・・・省略・・・)

		logger.logp( level, cname, method, msg );

java.util.logging.Logger#isLoggable の戻り値を制御するためには java.util.logging に介入しなければなりません。ですが それはしたくありません。ログ出力を抑制するためには上記の log メソッドが呼び出されること自体を止めなければなりません。この log メソッドはログレベルごとのメソッド trace debug info warn error fatal から呼ばれています。

Jdk14Logger warn メソッドの場合は以下のように実装されています。無条件で必ず log メソッドを呼んでいますね。

public void warn(Object message) {
	log(Level.WARNING, String.valueOf(message), null);
}

以下のように 条件付きで log メソッドを呼んでくれていたら良かったのですが…。

public void warn(Object message) {
	if(isWarnEnabled()) {
		log(Level.WARNING, String.valueOf(message), null);
	}
}

ログ出力レベルを制御する薄いラッパーを作る

Apache Commons Logging 自体にはログ出力レベルをコントロールする機能がないことが分かりました。ですが 実装クラスを LogFactory によって決定できるという自由度があることも分かりました。

LogFactory を独自の実装に置き換えて ログ出力レベルを判定するラッパーを作ればなんとかなりそうです。

Wrapper.java(抜粋)
public class Wrapper implements org.apache.commons.logging.Log { private Log impl; private boolean isWarnEnabled; public Wrapper(Log impl) { this.impl = impl; } public void setWarnEnabled(boolean b) { isWarnEnabled = b; } @Override public boolean isWarnEnabled() { return isWarnEnabled && impl.isWarnEnabled(); } @Override public void warn(Object message) { if(isWarnEnabled) { impl.warn(message); } } @Override public void warn(Object message, Throwable t) { if(isWarnEnabled) { impl.warn(message, t); } } }

このようなラッパークラスを作成しました。コードが長くなるので 警告ログを出力する warn のみを記載していますが 実際には trace debug info error fatal についても同様に実装しています。

このラッパークラスには Log 型のフィールド impl があり 実際の処理は impl に委譲します。impl Jdk14Logger クラスのインスタンスや Log4JLogger クラスのインスタンスを保持します。

このラッパークラスの特徴は setWarnEnabled メソッドが追加されていることです。このメソッドで警告ログの出力有無を変更することができます。setWarnEnabled(false) を呼び出すと警告ログ出力フラグがオフになり warn メソッドを呼び出しても何もしなくなります。impl warn を呼び出さなくなります

@Override
public void warn(Object message) {
	if(isWarnEnabled) {
		impl.warn(message);
	}
}

次に LogFactory Jdk14Logger インスタンスや Log4JLogger インスタンスをラップした Wrapper クラスのインスタンスを返すようにします。そのために ファクトリークラスにもラッパーを用意します。

LogFilter.java(抜粋)
package net.osdn.util; public class LogFilter extends org.apache.commons.logging.LogFactory { private LogFactory impl; public LogFilter() { impl = new org.apache.commons.logging.impl.LogFactoryImpl(); } @Override public Log getInstance(Class clazz) throws LogConfigurationException { Log log = impl.getInstance(clazz); if(log != null) { return new Wrapper(log, getLevel(clazz.getName())); } return null; } @Override public Log getInstance(String name) throws LogConfigurationException { Log log = impl.getInstance(name); if(log != null) { return new Wrapper(log, getLevel(name)); } return null; } @Override public Object getAttribute(String name) { return impl.getAttribute(name); } @Override public String[] getAttributeNames() { return impl.getAttributeNames(); } @Override public void release() { impl.release(); } @Override public void removeAttribute(String name) { impl.removeAttribute(name); } @Override public void setAttribute(String name, Object value) { impl.setAttribute(name, value); } }

これが LogFactory のラッパーです。名前は net.osdn.util.LogFilter にしました。委譲の仕組みは同じです。メソッド呼び出しをフィールド impl に委譲しています。

@Override
public Log getInstance(String name) throws LogConfigurationException {
	Log log = impl.getInstance(name);
	if(log != null) {
		return new Wrapper(log, getLevel(name));
	}
	return null;
}

getInstance メソッドには少しだけ手を加えています。impl#getInstance メソッドに委譲して Log インスタンスを取得し それを Wrapper でラップしてから返すようにしています。Wrapper クラスは Log インターフェースを実装しているので ラップした後も Log として振る舞うことができます。

これで ファクトリークラスの薄いラッパーもできました。あとは このファクトリークラスを Apache Commons Logging に差し込めば完了です。

ファクトリークラスを置き換える

Apache Commons Logging にはファクトリークラスを置き換える機能も用意されています。置き換えられなかったらファクトリークラスになっている意味がないので当たり前のことですけどね!

方法は簡単。org.apache.commons.logging.LogFactory システムプロパティにファクトリークラスの完全修飾クラス名を指定するだけです。

net.osdn.util.LogFilter という名前で作成したファクトリークラスに置き換える場合は以下のようになります。

System.setProperty(
		"org.apache.commons.logging.LogFactory", "net.osdn.util.LogFilter");

このファクトリークラスのラッパー LogFilter には setLevel ユーティリティーメソッドを追加してあります。setLevel メソッドではクラス名またはパッケージ名を指定して まとめてログ出力をコントロールすることができます。

PDFBox の例では warn 警告 が出力されるのが問題だったので レベルが error 以上の場合だけログを出力するように制御したいと思います。この場合は以下のように設定します。

PDFBoxの警告ログを抑制する
System.setProperty( "org.apache.commons.logging.LogFactory", "net.osdn.util.LogFilter"); LogFilter.setLevel("org.apache.pdfbox", LogFilter.Level.ERROR); LogFilter.setLevel("org.apache.fontbox", LogFilter.Level.ERROR);

この処理は main メソッドやメインクラスのスタティック イニシャライザーに書いておくのが良いです。

今回の PDFBox の例では警告ログを出力していたのは以下の 3 つのクラスでした。

  • org.apache.pdfbox.pdmodel.font.PDCIDFontType0
  • org.apache.pdfbox.pdmodel.font.PDSimpleFont
  • org.apache.fontbox.ttf.CmapSubtable

クラス名指定で個別に制御する場合は以下のようになります。

LogFilter.setLevel(
		"org.apache.pdfbox.pdmodel.font.PDCIDFontType0", LogFilter.Level.ERROR);
LogFilter.setLevel(
		"org.apache.pdfbox.pdmodel.font.PDSimpleFont", LogFilter.Level.ERROR);
LogFilter.setLevel(
		"org.apache.fontbox.ttf.CmapSubtable", LogFilter.Level.ERROR);

これだと記述量も多くなりますし ログを出力しているクラスを正確に洗い出すのにも手間がかかります。前述の org.apache.pdfbox org.apache.fontbox というパッケージ指定であれば 下位パッケージのクラスも含めてまとめて対象になるので簡単です。

ダウンロード

完成した Apache Commons Logging のログ出力を制御するラッパーは下記のリンクからダウンロードできます。完成版では Wrapper クラスを LogFilter の内部クラスにしているので LogFilter.java ファイルのみで完結しています。

commons-logging-filter-src.zip (2KB)

Apache Commons Logging のログ出力の抑制方法の紹介は以上です。

この記事を共有しませんか?