これからのロガー JEP 264: Platform Logging API and Service


はじめに

Java9 の JPMS(Java Platform Module System) に合わせて導入された、JEP 264: Platform Logging API and Service ですが、大きな変更の陰に隠れて意外とマイナーな存在のままなので、こちらに紹介しておきます。


JEP は以下になります。

openjdk.org


Platform Logging API

Platform Logging API は、SFL4J(Simple Logging Facade) や、Apache Commons Logging のような、ロギング用の(最小限の)ファサード・インターフェースを提供します。

典型的には以下のように、java.util.logging.Logger.getLogger() に変えて System.getLogger() を利用します。

import java.lang.System.Logger;

private static final Logger logger = System.getLogger(Foo.class.getName());

logger.log(DEBUG, "Initialization is completed.");

Platform Logging の中心となるAPIは以下です。

  • System.Logger インターフェース
  • System.LoggerFinder 抽象クラス

旧来からの java.util.logging API は java.logging モジュールに属しますが、新しいAPIは java.lang パッケージに属しており、java.base モジュールのメンバです。

これにより、ロギングに関するモジュール間の依存が減り、モジュールグラフが簡素化されています。


Platform Logging では、java.util.ServiceLoader API により、System.LoggerFinder を介してロガーの実装をロードします。

System.LoggerFinder により、slf4j(logback) や log4j の実装が見つかればそれらを利用します。 見つからない場合は従来からの java.util.logging が使われます。


System.Logger

System.Logger インターフェースは以下のAPIが提供されています。

public interface Logger {

    public String getName();
    public boolean isLoggable(Level level);

    public default void log(Level level, String msg) { ... }
    public default void log(Level level, Supplier<String> msgSupplier) { ... }
    public default void log(Level level, Object obj) { ... }
    public default void log(Level level, String msg, Throwable thrown) { ... }
    public default void log(Level level, Supplier<String> msgSupplier, Throwable thrown) { ... }
    public default void log(Level level, String format, Object... params) { ... }

    public void log(Level level, ResourceBundle bundle, String msg, Throwable thrown);
    public void log(Level level, ResourceBundle bundle, String format, Object... params);
}

logger.debug("...") 形式ではなく、logger.log(DEBUG, "...") 形式なのは好みの分かれるところかも知れません。

Level は以下のような定義となっており、不評な JUL のレベル定義が簡素化され、slf4j や log4j のレベル定義に寄った定義になっています。

public enum Level {
    ALL(Integer.MIN_VALUE),  // typically mapped to/from j.u.l.Level.ALL
    TRACE(400),   // typically mapped to/from j.u.l.Level.FINER
    DEBUG(500),   // typically mapped to/from j.u.l.Level.FINEST/FINE/CONFIG
    INFO(800),    // typically mapped to/from j.u.l.Level.INFO
    WARNING(900), // typically mapped to/from j.u.l.Level.WARNING
    ERROR(1000),  // typically mapped to/from j.u.l.Level.SEVERE
    OFF(Integer.MAX_VALUE);  // typically mapped to/from j.u.l.Level.OFF
}

ログの出力は MessageFormat でフォーマットを指定できるため、以下のように文字列パラメータを指定できます。

logger.log(ERROR, "Index {0} out of bounds for length {1}", arg0, arg1);

重い文字列が必要な場合には、サプライヤ Supplier<String> msgSupplier でメッセージを構築することもできます。


slf4j(logback) をバックエンドとして使う場合は slf4j-jdk-platform-logging を使います。

runtimeOnly("org.slf4j:slf4j-jdk-platform-logging:2.0.0")
runtimeOnly("ch.qos.logback:logback-classic:1.3.0-beta0")

log4j2 の場合は log4j-jpl を使います。

runtimeOnly("org.apache.logging.log4j:log4j-core:2.18.0")
runtimeOnly("org.apache.logging.log4j:log4j-jpl:2.18.0")


System.LoggerFinder

先に述べた通り、LoggerFinder サービスを使うことで、ロガーのバックエンドを設定できます。

META-INF/services/java.lang.System$LoggerFinder に、独自実装した LoggerFinder サービスを登録し、LoggerFinder から Logger を提供します。

LoggerFinder は以下のような実装になっています。

public static abstract class LoggerFinder {

    static final RuntimePermission LOGGERFINDER_PERMISSION = new RuntimePermission("loggerFinder");

    protected LoggerFinder() {
        this(checkPermission());
    }

    private LoggerFinder(Void unused) {
        // nothing to do.
    }

    private static Void checkPermission() {
        // ...
    }

    public abstract Logger getLogger(String name, Module module);

    public Logger getLocalizedLogger(String name, ResourceBundle bundle, Module module) {
        return new LocalizedLoggerWrapper<>(getLogger(name, module), bundle);
    }

    public static LoggerFinder getLoggerFinder() {
        // ...
        return accessProvider();
    }

    private static volatile LoggerFinder service;

    @SuppressWarnings("removal")
    static LoggerFinder accessProvider() {
        // We do not need to synchronize: LoggerFinderLoader will
        // always return the same instance, so if we don't have it,
        // just fetch it again.
        if (service == null) {
            PrivilegedAction<LoggerFinder> pa = () -> LoggerFinderLoader.getLoggerFinder();
            service = AccessController.doPrivileged(pa, null, LOGGERFINDER_PERMISSION);
        }
        return service;
    }

}

サブクラスで public abstract Logger getLogger(String name, Module module) の実装を提供し、自身で実装した System.Logger を提供することで自作したロガーのバックエンドを使うことができます。


ここでは、slf4j(logback) と log4j2 における System.LoggerFinder の実装を確認しておきましょう。


SLF4JSystemLoggerFinder

META-INF/services/java.lang.System$LoggerFinder には以下のサービスが登録されています。

org.slf4j.jdk.platform.logging.SLF4JSystemLoggerFinder

System.LoggerFinder の実装である SLF4JSystemLoggerFinder は以下のようになっており、SLF4JPlarformLoggerFactory に処理が委譲されます。

public class SLF4JSystemLoggerFinder extends System.LoggerFinder {

    final SLF4JPlarformLoggerFactory platformLoggerFactory = new SLF4JPlarformLoggerFactory();
   
    @Override
    public System.Logger getLogger(String name, Module module) {
        SLF4JPlatformLogger adapter = platformLoggerFactory.getLogger(name);
        return adapter;
    }

}

SLF4JPlarformLoggerFactory では、ロガー名をキーとした SLF4JPlatformLogger のマップが管理されます。

public class SLF4JPlarformLoggerFactory {

    ConcurrentMap<String, SLF4JPlatformLogger> loggerMap = new ConcurrentHashMap<>();
   
    public SLF4JPlatformLogger getLogger(String loggerName) {
        SLF4JPlatformLogger spla = loggerMap.get(loggerName);
        if (spla != null) {
            return spla;
        } else {
            Logger slf4jLogger = LoggerFactory.getLogger(loggerName);
            SLF4JPlatformLogger newInstance = new SLF4JPlatformLogger(slf4jLogger);
            SLF4JPlatformLogger oldInstance = loggerMap.putIfAbsent(loggerName, newInstance);
            return oldInstance == null ? newInstance : oldInstance;
        }
    }
}

SLF4JPlatformLoggerSystem.Logger の実装で、 org.slf4j.Logger へ処理を委譲しています。

class SLF4JPlatformLogger implements System.Logger {

    private final Logger slf4jLogger;
    // ...
}


Log4jSystemLoggerFinder

META-INF/services/java.lang.System$LoggerFinder には以下のサービスが登録されています。

org.apache.logging.log4j.jpl.Log4jSystemLoggerFinder

System.LoggerFinder の実装である Log4jSystemLoggerFinder は以下のようになっており、Log4jSystemLoggerAdapter に処理が委譲されます。

public class Log4jSystemLoggerFinder extends System.LoggerFinder {

    private final Log4jSystemLoggerAdapter loggerAdapter = new Log4jSystemLoggerAdapter();

    @Override
    public Logger getLogger(String name, Module module) {
        return loggerAdapter.getLogger(name);
    }
}

Log4jSystemLoggerAdapter では、ロガー名をキーとした SLF4JPlatformLogger のマップが管理されます。

public class Log4jSystemLoggerAdapter extends AbstractLoggerAdapter<Logger> {

    @Override
    protected Logger newLogger(String name, LoggerContext context) {
        return new Log4jSystemLogger(context.getLogger(name));
    }

    @Override
    protected LoggerContext getContext() {
        return getContext(LogManager.getFactory().isClassLoaderDependent()
                ? StackLocatorUtil.getCallerClass(LoggerFinder.class)
                : null);
    }
}

AbstractLoggerAdapter で以下のようにコンテキスト別に、ロガー名をキーとした SLF4JPlatformLogger のマップが管理されています。

protected final Map<LoggerContext, ConcurrentMap<String, L>> registry = new ConcurrentHashMap<>();

newLogger(String name, LoggerContext context) でインスタンス化する Log4jSystemLoggerSystem.Logger の実装で、 org.apache.logging.log4j.spi.ExtendedLogger へ処理を委譲しています。

public class Log4jSystemLogger implements System.Logger {

    private final ExtendedLogger logger;
    // ...
}


まとめ

JEP 264: Platform Logging API and Service について簡単ではありますが、そして今更ではありますが、紹介しました。

ライブラリプロジェクトでは、なるべく依存を減らしたいため、ロギングライブラリに何を使うかは悩ましいところでした。 JUL がもう少しマシなら良かっただけの話ではあります。

これからは、System.Logger で書いておき、バックエンドは利用側に任せる形が良いでしょう。

blog1.mammb.com