Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
3
Help us understand the problem. What is going on with this article?
@kochoru

jul-to-slf4jのしくみ

More than 1 year has passed since last update.

jul-to-slf4jとは

  • jul-to-slf4jとは、julのAPI経由でロギングを行っていても、slf4jに処理を移譲してくれる便利なやつです。
  • julとは、java.util.loggingパッケージのことで、JavaにおけるデフォルトのロギングAPIです。
  • slf4jとは、ログファサードライブラリです。
    • ファサードとは、Gofのデザインパターンの一つのファサードパターンからきています。
      • ファサードパターンとは、僕の理解では複数の処理系の実行を呼び出し側から見て単純化するための窓口、インタフェース(≠言語としてのinterface)という感じだと思ってます。
      • slf4jでは、上記でいう複数の処理系はlogbacklog4j(2)等の各ロギングライブラリを指し、それらの処理に対するインタフェースとしての役割を持ちます。
        • 実際に、logbacklog4j2のAPIを直接叩くことは昨今ではあまり無いのではないでしょうか。実際に動くロギングライブラリ毎の設定自体はどうやっても無くしようがないですが。
  • 以外といろんなものがこいつに依存してます。
  • 古いライブラリとかを使っているとjulに直接依存している場合がまぁありますが、その場合であってもslf4jに透過的に処理を移譲されるようにしてくれるのがこいつ、jul-to-slf4jです。
    • 実際には、どこかで(可能な限りVM起動直後)で1つAPIを叩くことで適用されます。
    • さらにいうと、大抵のフレームワークは初期化時に上記のAPIを叩いてくれるので、ほとんどの場合アプリケーション実装者はjul-to-slf4jを意識する必要はありません。

使用方法

SLF4JBridgeHandler.install();

って叩くだけです。

  • くどいようですが、ほとんどの場合アプリケーション実装者は上記APIを意識する必要はありません。
  • ただ、Java EEの上でフレームワークを挟まず直接アプリケーションを開発している場合等はその限りでは無いかと思います。

使用例

Spring Bootのなかでは

参考URL

Slf4jLoggingSystem.java
...
    private void configureJdkLoggingBridgeHandler() {
        try {
            if (isBridgeHandlerAvailable()) {
                removeJdkLoggingBridgeHandler();
                SLF4JBridgeHandler.install();
            }
        }
        catch (Throwable ex) {
            // Ignore. No java.util.logging bridge is installed.
        }
    }
...
  • Spring Bootのロギングシステムの初期化の仕組みは、また今度書きたいと思います。

Play frameworkのなかでは

参考URL

LogbackLoggerConfigurator.scala
...
  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の実装

こんな感じになってる。

SLF4JBridgeHandler.java
    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のクラスです。

SLF4JBridgeHandler.java
import java.util.logging.LogManager;

また、getLogger("")というふうに、ロガー名として空文字を指定しているのがわかると思います。これはなんなのでしょうか。
名前が無いのでなんとなく想像が付かないわけでも無いですが、どうやら空文字のロガー名を指定するとルートロガーを取得できるらしいです。

SLF4JBridgeHandler.java
    private static java.util.logging.Logger getRootLogger() {
        return LogManager.getLogManager().getLogger("");
    }

ここで前提として、他のロギングAPIの例に漏れずjulも全ロガーは子孫関係を持ち、必ずルートを継承します。
つまり、jul上のルートロガーに対してSLF4JBridgeHandlerを追加することは、jul経由でのロギングはすべてslf4jに移譲されるということを意味します。

参考

3
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
kochoru
batonz
インターネットによる事業承継・M&Aマッチングサービスを開発・運営するスタートアップ

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
3
Help us understand the problem. What is going on with this article?