jul-to-slf4jとは
-
jul-to-slf4j
とは、jul
のAPI経由でロギングを行っていても、slf4j
に処理を移譲してくれる便利なやつです。 -
jul
とは、java.util.logging
パッケージのことで、JavaにおけるデフォルトのロギングAPIです。 -
slf4j
とは、ログファサードライブラリです。- ファサードとは、Gofのデザインパターンの一つのファサードパターンからきています。
- ファサードパターンとは、僕の理解では複数の処理系の実行を呼び出し側から見て単純化するための窓口、インタフェース(≠言語としての
interface
)という感じだと思ってます。 -
slf4j
では、上記でいう複数の処理系はlogback
、log4j(2)
等の各ロギングライブラリを指し、それらの処理に対するインタフェースとしての役割を持ちます。- 実際に、
logback
やlog4j2
のAPIを直接叩くことは昨今ではあまり無いのではないでしょうか。実際に動くロギングライブラリ毎の設定自体はどうやっても無くしようがないですが。
- 実際に、
- ファサードパターンとは、僕の理解では複数の処理系の実行を呼び出し側から見て単純化するための窓口、インタフェース(≠言語としての
- ファサードとは、Gofのデザインパターンの一つのファサードパターンからきています。
- 以外といろんなものがこいつに依存してます。
- 古いライブラリとかを使っていると
jul
に直接依存している場合がまぁありますが、その場合であってもslf4j
に透過的に処理を移譲されるようにしてくれるのがこいつ、jul-to-slf4j
です。- 実際には、どこかで(可能な限りVM起動直後)で1つAPIを叩くことで適用されます。
- さらにいうと、大抵のフレームワークは初期化時に上記のAPIを叩いてくれるので、ほとんどの場合アプリケーション実装者は
jul-to-slf4j
を意識する必要はありません。
使用方法
SLF4JBridgeHandler.install();
って叩くだけです。
- くどいようですが、ほとんどの場合アプリケーション実装者は上記APIを意識する必要はありません。
- ただ、Java EEの上でフレームワークを挟まず直接アプリケーションを開発している場合等はその限りでは無いかと思います。
使用例
Spring Boot
のなかでは
...
private void configureJdkLoggingBridgeHandler() {
try {
if (isBridgeHandlerAvailable()) {
removeJdkLoggingBridgeHandler();
SLF4JBridgeHandler.install();
}
}
catch (Throwable ex) {
// Ignore. No java.util.logging bridge is installed.
}
}
...
- Spring Bootのロギングシステムの初期化の仕組みは、また今度書きたいと思います。
Play framework
のなかでは
...
def configure(properties: Map[String, String], config: Option[URL]): Unit = {
// Touching LoggerContext is not thread-safe, and so if you run several
// application tests at the same time (spec2 / scalatest with "new WithApplication()")
// then you will see NullPointerException as the array list loggerContextListenerList
// is accessed concurrently from several different threads.
//
// The workaround is to use a synchronized block around a singleton
// instance -- in this case, we use the StaticLoggerBinder's loggerFactory.
loggerFactory.synchronized {
// Redirect JUL -> SL4FJ
// Remove existing handlers from JUL
SLF4JBridgeHandler.removeHandlersForRootLogger()
// Configure logback
val ctx = loggerFactory.asInstanceOf[LoggerContext]
// Set a level change propagator to minimize the overhead of JUL
//
// Please note that translating a java.util.logging event into SLF4J incurs the
// cost of constructing LogRecord instance regardless of whether the SLF4J logger
// is disabled for the given level. Consequently, j.u.l. to SLF4J translation can
// seriously increase the cost of disabled logging statements (60 fold or 6000%
// increase) and measurably impact the performance of enabled log statements
// (20% overall increase). Please note that as of logback-version 0.9.25,
// it is possible to completely eliminate the 60 fold translation overhead for
// disabled log statements with the help of LevelChangePropagator.
//
// https://www.slf4j.org/api/org/slf4j/bridge/SLF4JBridgeHandler.html
// https://logback.qos.ch/manual/configuration.html#LevelChangePropagator
val levelChangePropagator = new LevelChangePropagator()
levelChangePropagator.setContext(ctx)
levelChangePropagator.setResetJUL(true)
ctx.addListener(levelChangePropagator)
SLF4JBridgeHandler.install()
ctx.reset()
// Ensure that play.Logger and play.api.Logger are ignored when detecting file name and line number for
// logging
val frameworkPackages = ctx.getFrameworkPackages
frameworkPackages.add(classOf[play.Logger].getName)
frameworkPackages.add(classOf[play.api.Logger].getName)
properties.foreach { case (k, v) => ctx.putProperty(k, v) }
config match {
case Some(url) =>
val initializer = new ContextInitializer(ctx)
initializer.configureByResource(url)
case None =>
System.err.println("Could not detect a logback configuration file, not configuring logback")
}
StatusPrinter.printIfErrorsOccured(ctx)
}
}
...
ウッ、scala
...
で、どういうしくみなの?
jul-to-slf4jの実装
こんな感じになってる。
public static void install() {
LogManager.getLogManager().getLogger("").addHandler(new SLF4JBridgeHandler());
}
...
public void publish(LogRecord record) {
// Silently ignore null records.
if (record == null) {
return;
}
Logger slf4jLogger = getSLF4JLogger(record);
// this is a check to avoid calling the underlying logging system
// with a null message. While it is legitimate to invoke j.u.l. with
// a null message, other logging frameworks do not support this.
// see also http://jira.qos.ch/browse/SLF4J-99
if (record.getMessage() == null) {
record.setMessage("");
}
if (slf4jLogger instanceof LocationAwareLogger) {
callLocationAwareLogger((LocationAwareLogger) slf4jLogger, record);
} else {
callPlainSLF4JLogger(slf4jLogger, record);
}
}
結論から、行われていることだけ順を追ってみると
-
SLF4JBridgeHandler#install()
でjul
上のルートロガーにjava.util.logging.Handler
を継承したSLF4JBridgeHandler
を追加 -
java.util.logging.logger#info()
等と叩いた際になんだかんだあってjava.util.logging.Handler#publish()
に処理が移譲 -
SLF4JBridgeHandler#publish()
ではさらにSlf4j
のロガーの処理を移譲しているため、jul
経由のロギングは、SLF4JBridgeHandler#install()
を実行しておくことで全てslf4j
へ処理が移譲される。
詳しくみていく
java.util.logging.Handler
は何者かというと、JavaDocによると、
Handlerオブジェクトは、Loggerからログ・メッセージを受け取り、それらをエクスポートします。たとえば、このオブジェクトは、コンソールやファイルに書き込み、ネットワーク・ログ・サービスに送信し、OSログへの転送などを実行します。
とあります。
つまりLogger
は、アプリケーションから見たインタフェースであり、実際に物理的なログ出力を行ってくれるのがこのHandler
って感じでしょうか。
僕はなんとなくlogback
でいうところのアペンダーのような存在と理解しました。
そんでもってLogManager
は何者かというと、import
文を読む限りjulのクラスです。
import java.util.logging.LogManager;
また、getLogger("")
というふうに、ロガー名として空文字を指定しているのがわかると思います。これはなんなのでしょうか。
名前が無いのでなんとなく想像が付かないわけでも無いですが、どうやら空文字のロガー名を指定するとルートロガーを取得できるらしいです。
private static java.util.logging.Logger getRootLogger() {
return LogManager.getLogManager().getLogger("");
}
ここで前提として、他のロギングAPIの例に漏れずjul
も全ロガーは子孫関係を持ち、必ずルートを継承します。
つまり、jul
上のルートロガーに対してSLF4JBridgeHandler
を追加することは、jul
経由でのロギングはすべてslf4j
に移譲されるということを意味します。