Contents

Java 9 日志API

1. 简介

在本教程中,我们将探索 Java 9 中新引入的 Logging API,并实现一些示例来涵盖最常见的情况。 此 API 已在 Java 中引入,以**提供一种通用机制来处理所有平台日志并公开可由库和应用程序自定义的服务接口。**这样JDK平台的日志就可以和应用使用同一个日志框架,减少项目依赖。

2. 创建自定义实现

在本节中,我们将展示我们必须实现以创建新记录器的 Logging API 的主要类。我们将通过实现一个将所有日志打印到控制台的简单记录器来做到这一点。

2.1. 创建Logger

我们必须创建的主要类是Logger。这个类至少要实现System.Logger接口和这四个方法:

  • getName():返回记录器的名称。JDK 将使用它来按名称创建记录器
  • isLoggable():指示记录器启用的级别
  • log():它是将日志打印到应用程序使用的任何底层系统的方法——在我们的例子中是控制台。有 2 个*log()*方法可以实现,每个方法接收不同的参数

让我们看看我们的实现会是什么样子:

public class ConsoleLogger implements System.Logger {
    @Override
    public String getName() {
        return "ConsoleLogger";
    }
    @Override
    public boolean isLoggable(Level level) {
        return true;
    }
    @Override
    public void log(Level level, ResourceBundle bundle, String msg, Throwable thrown) {
        System.out.printf("ConsoleLogger [%s]: %s - %s%n", level, msg, thrown);
    }
    @Override
    public void log(Level level, ResourceBundle bundle, String format, Object... params) {
        System.out.printf("ConsoleLogger [%s]: %s%n", level, 
          MessageFormat.format(format, params));
    }
}

我们的ConsoleLogger类覆盖了上面提到的四个方法。getName()方法返回一个字符串, 而isLoggable()方法在所有情况下都返回true。最后,我们有输出到控制台的 2 *log()*方法。

2.2. 创建LoggerFinder

一旦我们创建了记录器,我们需要实现一个LoggerFinder来创建我们的ConsoleLogger实例。 为此,我们必须扩展抽象类System.LoggerFinder并实现*getLogger()*方法:

public class CustomLoggerFinder extends System.LoggerFinder {
    @Override
    public System.Logger getLogger(String name, Module module) {
        return new ConsoleLogger();
    }
}

在这种情况下,我们总是返回我们的ConsoleLogger最后,我们需要将LoggerFinder注册为服务,以便 JDK 可以发现它。如果我们不提供实现,则默认使用SimpleConsoleLogger

JDK 用来加载实现的机制是ServiceLoader。您可以在这里 中找到有关它的更多信息。 由于我们使用的是 Java 9,我们将把我们的类打包到一个模块中,并在module-info.java文件中注册我们的服务:

module com.blogdemo.logging {
    provides java.lang.System.LoggerFinder
      with com.blogdemo.logging.CustomLoggerFinder;
    exports com.blogdemo.logging;
}

有关 Java 模块的更多信息,请查看此其他教程

2.3. 测试我们的例子

为了测试我们的示例,让我们创建另一个用作应用程序的模块。这将只包含使用我们的服务实现的Main类。 此类将通过调用System.getLogger()方法获取我们的ConsoleLogger的实例:

public class MainApp {
    private static System.Logger LOGGER = System.getLogger("MainApp");
    public static void main(String[] args) {
        LOGGER.log(Level.ERROR, "error test");
        LOGGER.log(Level.INFO, "info test");
    }
}

在内部,JDK 将获取我们的CustomLoggerFinder实现并创建我们的ConsoleLogger实例。

之后,让我们为这个模块创建module文件:

module com.blogdemo.logging.app {
}

此时,我们的项目结构将如下所示:

├── src
│   ├── modules
│   │   ├── com.blogdemo.logging
│   │   │   ├── com
│   │   │   │   └── blogdemo
│   │   │   │       └── logging
│   │   │   │           ├── ConsoleLogger.java
│   │   │   │           └── CustomLoggerFinder.java
│   │   │   └── module-info.java
│   │   ├── com.blogdemo.logging.app
│   │   │   ├── com
│   │   │   │   └── blogdemo
│   │   │   │       └── logging
│   │   │   │           └── app
│   │   │   │               └── MainApp.java
│   │   │   └── module-info.java
└──

最后,我们将编译我们的两个模块,并将它们放在mods目录中:

javac --module-path mods -d mods/com.blogdemo.logging \
  src/modules/com.blogdemo.logging/module-info.java \
  src/modules/com.blogdemo.logging/com/blogdemo/logging/*.java
javac --module-path mods -d mods/com.blogdemo.logging.app \
  src/modules/com.blogdemo.logging.app/module-info.java \
  src/modules/com.blogdemo.logging.app/com/blogdemo/logging/app/*.java

最后,让我们运行app模块的Main类:

java --module-path mods \
  -m com.blogdemo.logging.app/com.blogdemo.logging.app.MainApp

如果我们看一下控制台输出,我们可以看到我们的日志是使用我们的ConsoleLogger打印的:

ConsoleLogger [ERROR]: error test
ConsoleLogger [INFO]: info test

3. 添加外部日志框架

在我们之前的示例中,我们将所有消息记录到控制台,这与默认记录器所做的相同。Java 9 中 Logging API 最有用的用途之一是让应用程序将 JDK 日志路由到应用程序正在使用的同一日志记录框架,这就是我们在本节中要做的事情。 我们将创建一个使用 SLF4J 作为日志外观和 Logback 作为日志框架的新模块。

由于我们已经在上一节中解释了基础知识,现在我们可以专注于如何添加外部日志框架。

3.1. 使用 SLF4J 的自定义实现

首先,我们将实现另一个Logger,它将为每个实例创建一个新的 SLF4J 记录器:

public class Slf4jLogger implements System.Logger {
    private final String name;
    private final Logger logger;
    public Slf4jLogger(String name) {
        this.name = name;
        logger = LoggerFactory.getLogger(name);
    }
    @Override
    public String getName() {
        return name;
    }
    
    //...
}

请注意,此Logger是 org.slf4j.Logger

对于其余的方法,我们将依赖 SLF4J 记录器实例上的实现。因此,如果启用了 SLF4J 记录器,我们的Logger将被启用:

@Override
public boolean isLoggable(Level level) {
    switch (level) {
        case OFF:
            return false;
        case TRACE:
            return logger.isTraceEnabled();
        case DEBUG:
            return logger.isDebugEnabled();
        case INFO:
            return logger.isInfoEnabled();
        case WARNING:
            return logger.isWarnEnabled();
        case ERROR:
            return logger.isErrorEnabled();
        case ALL:
        default:
            return true;
    }
}

并且日志方法将根据使用的日志级别调用适当的 SLF4J 记录器方法:

@Override
public void log(Level level, ResourceBundle bundle, String msg, Throwable thrown) {
    if (!isLoggable(level)) {
        return;
    }
    switch (level) {
        case TRACE:
            logger.trace(msg, thrown);
            break;
        case DEBUG:
            logger.debug(msg, thrown);
            break;
        case INFO:
            logger.info(msg, thrown);
            break;
        case WARNING:
            logger.warn(msg, thrown);
            break;
        case ERROR:
            logger.error(msg, thrown);
            break;
        case ALL:
        default:
            logger.info(msg, thrown);
    }
}
@Override
public void log(Level level, ResourceBundle bundle, String format, Object... params) {
    if (!isLoggable(level)) {
        return;
    }
    String message = MessageFormat.format(format, params);
    switch (level) {
        case TRACE:
            logger.trace(message);
            break;
        // ...
        // same as the previous switch
    }
}

最后,让我们创建一个使用 Slf4jLogger的新LoggerFinder

public class Slf4jLoggerFinder extends System.LoggerFinder {
    @Override
    public System.Logger getLogger(String name, Module module) {
        return new Slf4jLogger(name);
    }
}

3.2. 模块配置

一旦我们实现了所有类,让我们在我们的模块中注册我们的服务并添加 SLF4J 模块的依赖项:

module com.blogdemo.logging.slf4j {
    requires org.slf4j;
    provides java.lang.System.LoggerFinder
      with com.blogdemo.logging.slf4j.Slf4jLoggerFinder;
    exports com.blogdemo.logging.slf4j;
}

该模块将具有以下结构:

├── src
│   ├── modules
│   │   ├── com.blogdemo.logging.slf4j
│   │   │   ├── com
│   │   │   │   └── blogdemo
│   │   │   │       └── logging
│   │   │   │           └── slf4j
│   │   │   │               ├── Slf4jLoggerFinder.java
│   │   │   │               └── Slf4jLogger.java
│   │   │   └── module-info.java
└──

现在我们可以像上一节那样将此模块编译到mods目录中。

**请注意,我们必须将 slf4j-api jar 放在 mods 目录中才能编译此模块。另外,请记住使用库的模块化版本。**最新版本可以在Maven Central 中找到。

3.3. 添加 Logback

我们几乎完成了,但我们仍然需要添加 Logback 依赖项和配置。为此,请将logback-classiclogback-core的jar放在mods目录中。

和以前一样,我们必须确保我们使用的是模块化版本的库。同样,最新版本可以在Maven Central 中找到。

最后,让我们创建一个 Logback 配置文件并将其放在我们的mods目录中:

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>
                %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} -- %msg%n
            </pattern>
        </encoder>
    </appender>
    <root>
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

3.4. 运行我们的应用程序

此时,我们可以 使用我们的 SLF4J 模块运行我们的应用程序。

在这种情况下,我们还需要指定我们的 Logback 配置文件

java --module-path mods \
  -Dlogback.configurationFile=mods/logback.xml \
  -m com.blogdemo.logging.app/com.blogdemo.logging.app.MainApp

最后,如果我们检查输出,我们可以看到我们的日志是使用我们的 Logback 配置打印的:

2018-08-25 14:02:40 [main] ERROR MainApp -- error test
2018-08-25 14:02:40 [main] INFO  MainApp -- info test