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
ファイルのみで完結しています。
Apache Commons Logging のログ出力の抑制方法の紹介は以上です。