logging

第五十二章 日志 (Logging)

在项目开发中,记录日志是一个十分重要的功能,因为通过记录和观察日志能帮助开发人员了解系统运行的状态,并且在系统出错时,能向开发人员提供关键的问题定位信息。所以,Java 早在其1.4版本就发布了Java日志功能。然而,Java标准库的日志功能设计存在许多缺陷与不足。这些问题的长期存在导致Java开发者们纷纷开发出了各自的第三方日志库。这些日志库包括Apache Commons Logginglog4jLogbackSLF4J。我们将在本章中详细讲解这些日志库。

1. Java SE Logging

Java标准库中的日志框架和功能在包java.util.logging中,人们常常称之为JUL。它的工作方式很简单,如下面的代码所示。首先,开发人员需要创建一个Logger对象。习惯上,开发人员可以将当前所在类的类名传入getLogger()方法中,以获得一个Logger对象。然后,开发人员可以调用info()方法或者warning()方法来记录相应的日志消息。

import java.util.logging.Logger;

public class Example {
    public static void main(String[] args) {
        Logger logger = Logger.getLogger(Example.class.getName());
        logger.info("This is an info log message.");
        logger.warning("This is a warning log message.");
    }
}

在创建Logger对象后,开发人员可以添加Handler来处理这些消息。常见的消息处理Handler有FileHandler和ConsoleHandler。FileHandler会将消息写入文件中;ConsoleHandler会将消息写入标准错误输出流(System.err)中。例如,下面的代码会将消息写入文件system.log。

import java.util.logging.Logger;
import java.util.logging.FileHandler;
import java.io.IOException;

public class Example {
    public static void main(String[] args) {
        try {
            Logger logger = Logger.getLogger(Example.class.getName());
            logger.addHandler(new FileHandler("system.log"));
            logger.info("This is an info log message.");
            logger.warning("This is a warning log message.");
        } catch (IOException ex) {
            System.err.println(ex.getMessage());
        }
    }
}

第二个例子的代码会将消息写入标准错误输出流中。

import java.util.logging.Logger;
import java.util.logging.ConsoleHandler;

public class Example {
    public static void main(String[] args) {
        Logger logger = Logger.getLogger(Example.class.getName());
        logger.addHandler(new ConsoleHandler());
        logger.info("This is an info log message.");
        logger.warning("This is a warning log message.");
    }
}

每当记录一条消息后,Handler都会收到一个LogRecord对象。这个LogRecord对象包含了日志消息的级别(Logging Level,例如:info或者warning)、日志时间戳、日志源名称(我们在例子中使用的是类名)等信息。因此,Handler能在内部根据自身需求过滤和格式化消息。消息过滤是由过滤器类(Filter)完成的。它能控制消息是否被记录。消息格式化是由格式化类(Formatter)完成的。它能将消息格式化为更丰富、更易读的字符串格式。

import java.util.logging.Logger;
import java.util.logging.FileHandler;
import java.util.logging.SimpleFormatter;
import java.io.IOException;

public class Example {
    public static void main(String[] args) {
        try {
            Logger logger = Logger.getLogger(Example.class.getName());
            FileHandler handler = new FileHandler("system.log");
            handler.setFormatter(new SimpleFormatter());
            logger.addHandler(handler);
            logger.info("This is an info log message.");
            logger.warning("This is a warning log message.");
        } catch (IOException ex) {
			System.err.println(ex.getMessage());
		}
    }
}

运行上述例子后,system.log文件中会包含如下信息。

Nov 07, 2020 2:07:29 PM Example main
INFO: This is an info log message.
Nov 07, 2020 2:07:29 PM Example main
WARNING: This is a warning log message.

从上述的日志框架的设计和用例来看,Java标准库提供的日志功能已经能满足绝大部分需求。那么,它的问题出在哪里呢?为什么Java开发人员需要自行开发日志框架呢?在当时,开发人员认为Java标准库的日志功能存在以下几个问题。

  1. 有些开发人员不喜欢日志级别的名字。Java标准库中给出的日志级别名称不能准确表达问题的级别。例如:SEVERE表示最高级别的日志。但是,开发人员可能更需要ERROR这样的日志级别来表示出错信息。开发人员常常混淆FINE、FINER和FINEST的意义。
  2. Java标准库未提供灵活、可配置的消息格式化工具(Formatter)。开发人员需要自行开发。Java标准库中的Formatter仅支持简单的消息格式化输出。
  3. Java标准库的日志功能存在性能问题,SLF4J比Java标准库的效率更高。
  4. Java标准库不支持配置。其他的日志库能够从CLASSPATH中加载配置文件,并配置日志功能。

所以,综上所述,Java开发人员决定自行开发一套日志框架。自此,JUL处于非常尴尬的位置,很少有项目使用。

2. Log4j (v1)

为了将Log4j (v2)Logback的来龙去脉讲清楚,我们不得不先简单介绍一下Log4j (v1)。

Log4j (v1)为Log4j的第一个版本。Log4j (v1)由Apache软件基金会(Apache Software Foundation)开发;其主要的开发者为Ceki Gülcü。在Log4j (V1)中,设置了更合理的日志级别,并且提供了多种Appender。它除了支持将日志内容写入本地日志文件以外,开发人员还可以方便的记录email日志或者向远程日志服务器发送日志内容。它还设计了PatternLayout类,用于日志格式化。目前,Log4j (v1)已停止更新,Apache基金会已开发和发布Log4j (v2),Log4j的第二个版本,用于替代Log4j (v1)。Log4j (v2)的接口不兼容Log4j (v1)的接口。

import org.apache.log4j.Logger;

public class Example {
    public static void main(String[] args) {
        Logger logger = Logger.getLogger(Example.class.getName());
        logger.info("This is an info log message.");
        logger.debug("This is a debug log message.");
        logger.error("This is an error log message.");
    }
}

3. Logback

为了进一步改进Log4j (v1),Ceki Gülcü重写了Log4j (v1)。新的日志框架叫作Logback。与Log4j (v1)相比,Logback具备更加通用的特性,能适用于绝大多数应用场景。例如:Logback支持更多的过滤功能,开发人员能够使用更多的过滤选项来过滤日志记录;当配置文件更新后,Logback能够自动重新加载配置(Automatic Configuration Reloading);Logback效率更高;Logback能够自动删除老的日志文件,以确保日志文件不会占满硬盘空间等功能。

Logback大致可分为三个子模块: logback-core:是基础模块,用于支撑logback-classic和logback-access子模块。 logback-classic:可以认为是"改进版"的Log4j,接口与Log4j兼容。 logback-access:整合了Servlet容器(例如:TomcatJetty),可用于提供基于Http的日志功能。

Logback还提供了{}占位符({}-Placeholder)功能,方便了开发人员格式化日志消息。

4. Log4j (v2)

Apache Log4j (v2)是由Apache软件基金会(Apache Software Foundation)开发的Log4j的第二版;它又被简称为Log4j2。它的目标是用于改善Log4j (v1)、Java Util Logging (JUL)和Logback存在的问题。Log4j (v2)增加了许多特性,例如:它能自动重新加载日志文件;支持更多的过滤选项(Filtering Options);支持延迟运行日志语句(Lazy Evaluation of Log Statements);异步记录日志(Asynchronous Logger),以缩短响应时间等功能。

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
 
public class Example {
	private static Logger logger = LogManager.getLogger(Example.class.getName());

	public static void main(String[] args) {
		logger.info("This is an info log message."); 
        logger.error("This is an error log message.");
	}
}

5. SLF4J (Simple Logging Facade for Java)

当市场上出现了多个日志模块之后,开发人员可以根据自身情况选择Java日志库。Logback和Log4j (v2)都是很好的选择。但是,这也为模块/第三方库开发者带来了一个问题。假设小水滴计划开发一个项目。该项目需要集成一个Web服务器代码库(例如:Jetty)和一个JSON解析库(例如:Jackson)。假设这个Web服务器代码库采用Logback记录Web服务器的日常信息;而这个JSON解析库使用Log4J (v2)来记录JSON解析过程中的关键事件。那么,当小水滴集成这两个代码库时,不得不将Logback和Log4J (v2)两个日志代码库都集成进来。否则,Web服务器或者JSON解析库将无法正常工作。但是,小水滴并不希望这样做,因为如果这样做的话,会在产品中包含两份冗余的、功能相近的日志库。

因此,Java开发人员在日志库的顶层增加了一层抽象层。这层抽象层又被称为日志外观层(Logging Facade)。该抽象层并不实现具体的记录日志的工作;它仅提供一个日志记录接口给上层的应用程序使用。然后,它会根据当前的配置和运行环境,动态的选择日志实现库,并将日志记录传递给日志实现库处理。因此,在开发模块或者第三方库时,开发人员可以仅使用这一抽象层,而将日志库的选择权交由使用者决定。

图一 SLF4J应用结构图

图一 SLF4J应用结构图

SLF4J是顶层抽象层的实现。它接收日志记录,但是不记录任何日志。它能与java.util.logging (JUL)、Logback和Log4J整合,提供完整的日志解决方案。如下是SLF4J的示例代码。从代码中可以看出,开发人员并不需要明确指出使用哪个具体的日志库。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Example {
  public static void main(String[] args) {
    Logger logger = LoggerFactory.getLogger(Example.class);
    logger.info("This is an info log message.");
  }
}

6. Apache Commons Logging

为了提供相同的日志功能独立性(Logging Independency),Apache软件基金会(Apache Software Foundation)也设计了一套抽象层Apache Commons Logging。它提供的接口与SLF4J十分相似。它通过Java虚拟机中的类加载器(Class Loader)在查找具体使用的日志库。

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class Example {
  public static void main(String[] args) {
    Log log = LogFactory.getLog(Example.class);
    log.info("This is an info log message.");
    log.debug("This is a debug log message.");
  }
}

7. 日志库的选择

面对上述众多日志库,我们应该如何做出选择呢?这个问题,实际上,是选择日志抽象层和日志实现库两个问题。

日志抽象层(Logging Facade)的选择。小水滴建议选择SLF4J。原因是SLF4J和Apache Commons Logging提供了相似的接口,但是,Apache Commons Logging运行并不稳定。这个问题源自于Apache Commons Logging依赖于类加载器(Class Loader)来动态发现日志实现库上。这种实现机制给开发人员带来了许多更复杂的问题。更详细的问题分析报告可点击这里阅读。

日志实现库的选择。Logback和Log4j (v2)都是很好的日志实现库。开发人员可以从中任选一种作为日志库加入项目中。一些开发人员认为Log4j (v2)性能更好。但是,在记录日志数量不大的情况下,记录日志所占用的性能消耗比例并不大。据统计,日志功能代码量大约占整个项目代码量的4%。因此,Log4j (v2)的性能优势并不明显。所以,Logback和Log4j (v2)都是不错的选择。

8. 小结

本章介绍了Java生态环境中的几款日志库。因为Java标准库提供的日志功能非常简单,不能满足项目开发的需求。因此,Java开发人员开发了多款第三方日志库。正是因为市场上出现了许多功能相似的日志库,初学者可能会混淆它们的名字和功能。因此,我们撰写了这篇文章并且简单介绍比较了它们的来历和功能特点。因为SLF4J为开发人员提供了统一的接口,因此,我们将在下一章着重介绍SLF4J。

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.