为什么需要Log4j 2

Log4j1.X在经过长时间的迭代中,因为为了兼容他变得越来越难以维护,所以官方开启了Log4j2.X的项目,但是在珠玉在前的logback面前为什么还需要Log4j2.x呢?在官方的文档中我们找到了答案,官方列举了十几点,我们简单摘要几点:

  1. 在配置更新的时候,Log4j 1.x和Logback都会丢失事件,但是Log4j 2不会;Logback的appender出现异常是不会在项目中抛出的,即会把异常吃掉,但是Log4j 2可以通过配置暴露异常;
  2. Log4j 2搞了一个新的logger,很牛逼 在多线程中与Log4j 1.x和Logback相比,新的Logger的吞吐量高10倍,延迟降低了几个数量级;
  3. 垃圾回收机制很牛逼;
  4. Log4j 2使用插件化来扩展Log4j 2,例如lookup就是其中一个典型的插件;
  5. 支持自定义日志等级(这个我觉得跟marker也可以勉强一战);
  6. 支持Java8 的lambda表达式;
  7. Log4j 2充分利用了Java 5并发支持,并以最低的级别执行锁定。 Log4j 1.x已知死锁问题。 其中许多已在Logback中修复,但许多Logback类仍需要较高级别的同步。

等等。。。就不一一叙述了,总之,新的日志框架Log4j 2很牛批,快来用它,快来用它!

架构

架构图
架构图

使用Log4j 2 API的应用程序将从LogManager请求具有特定名称的Logger。LogManager将找到合适的LoggerContext,然后从中获取Logger。 如果必须创建Logger,它将与LoggerConfig关联,该LoggerConfig包含a)与Logger相同的名称,b)父程序包的名称或c)根LoggerConfig。 LoggerConfig对象是根据配置中的Logger声明创建的。 LoggerConfig与实际提供LogEvent的Appender关联。

以上是官网的介绍做了翻译,简单来说跟Logback的整个过程是基本一致的,声明一个独特名字的Logger,Logger寻找到合适的配置打印日志,没有就往上寻找ROOT,如果允许就打印。

还有LoggerCondfig中层级关系概念,例如"java" 是"java.util"的父级,也是 "java.util.Vector"的父级。最顶层有root LoggerConfig ,它是所有的LoggerConfig 父级,他是必须存在的一个LoggerConfig ,直接使用ROOT LoggerConfig的Logger可以通过以下方式获

Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);

或者

Logger logger = LogManager.getRootLogger();

概念介绍

LoggerContext

LoggerContext充当Logging系统的锚点。 但是,根据情况,一个应用程序中可能有多个活动LoggerContext。 有关LoggerContext的更多详细信息,请参见“日志分隔”部分。

Configuration

每个LoggerContext都有一个活动的Configuration。 该配置包含所有Appender,context-wide Filters,LoggerConfigs,并包含对StrSubstitutor的引用。 在重新配置期间,将存在两个配置对象。 一旦所有记录器都重定向到新的配置,旧的配置将被停止并丢弃。

Logger

通过调用LogManager.getLogger创建记录器。Logger本身不执行任何直接操作。 它仅具有一个名称,并与LoggerConfig关联。 它扩展了AbstractLogger并实现了所需的方法。 修改配置后,Logger可能与其他LoggerConfig关联,从而导致其行为被修改。

LogManager.getLogger返回的Logger是可以复用的,当你创建了一个之后下一次调用就是同一个了,类似于Spring中的bean。

LoggerConfig

在log配置中声明Logger时,将创建LoggerConfig对象。 LoggerConfig包含一组过滤器,必须允许LogEvent通过,然后才能将其传递给任何Appender。 它包含对应用于处理事件的一组Appender的引用。

LoggerConfigs将被分配一个日志级别。 内置级别集包括TRACE,DEBUG,INFO,WARN,ERROR和FATAL。 Log4j 2还支持自定义日志级别。 获得更多粒度的另一种机制是改为使用标记。

Log4j 1.x和Logback都具有“级别继承”的概念。 在Log4j 2中,Logger和LoggerConfig是两个不同的对象,因此此概念的实现方式有所不同。 每个Logger都引用适当的LoggerConfig,后者又可以引用其父级,从而达到相同的效果。

从架构图中来看的话Logger包含了LoggerConfig,要注意的是与logback中的概念不一样,logback中的logger相当于Log4j 2中的LoggerConfig,虽然在xml配置文件中,LoggerConfig其实用的是<Logger>标签。Log4j 2中的Logger有点像太上皇,什么事都交给LoggerConfig干。

日志级别

这里直接参照logback,不做赘述,基本是一致的,只是在ERROR级别上多加了一个FATAL(致命的)。

Filter

除了上述的自动日志级别过滤之外,Log4j还提供过滤器,可以在控制权传递给任何LoggerConfig之前应用;或在控制权传递给LoggerConfig之后但在调用任何Appender之前应用;或在控制权传递给LoggerConfig之后,但在调用特定的Appender之前应用;以及给每个Appender添加过滤。以与防火墙过滤器非常相似的方式,每个过滤器可以返回三个结果之一:Accept, DenyNeutralAccept的响应意味着不应该调用其他过滤器,并且事件将被处理。Deny的回应意味着事件应该立即被忽略,控制权应该返回给调用者。Neutral的响应表示该事件应该传递给其他过滤器。如果没有其他过滤器,事件将被处理。

一个事件可能被一个过滤器接受,但事件仍然可能不会被记录。这种情况发生在事件被LoggerConfig之前的过滤器接受,但是被LoggerConfig内的过滤器拒绝,或被所有Appender拒绝。

Appender

根据logger选择性地启用或禁用记录请求的能力只是Log4j的其中一个作用。Log4j 允许将日志请求输出到多个

Appender,即你可以打印在控制台也可以打印在日志文件里。一个Logger可以连接多个Appender

可以通过调用当前配置的addLoggerAppender方法将Appender添加到Logger中 。如果与Logger名称匹配的LoggerConfig不存在,则将创建一个LoggerConfig,将Appender附加到该LoggerConfig,然后所有Logger被通知去更新其LoggerConfig引用。

对于给定的logger,每个启用的记录请求将被转发给Logger的LoggerConfig中的所有appender以及LoggerConfig父级的Appender。 换句话说,Appender是从LoggerConfig层次继承的。例如,如果将控制台appender添加到根日志记录器,则所有启用的日志记录请求将至少在控制台上打印。如果此时新增一个file appender名为C的appender到LoggerConfig,那启用日志请求时C和C的子级将在一个文件和在控制台上打印。可以重写此默认行为,通过在配置文件的Logger声明中设置additivity = "false",可使Appender累积功能不再具有可加性。

Appender的概念也和Logback是一样的。

Layout

多数情况下,用户不仅希望自定义输出目标,还希望能够自定义输出格式。这是通过将Layout与Appender关联来实现的 。Layout 负责根据用户的意愿格式化LogEvent,而appender负责将格式化的输出发送到目的地。所述的PatternLayout,是log4j分发标准的一部分,能让用户根据类似于C语言的printf函数的转换模式来指定输出格式。

例如,具有转换模式“%r [%t]%-5p%c - %m%n”的PatternLayout将输出类似于:

176 [main] INFO  org.foo.Bar - Located nearest gas station.

关于Layout我们后续讲他的pattern语法再谈。其实也和logback是一样的。

StrSubstitutor and StrLookup

该 StrSubstitutor 类和 StrLookup 接口是从Apache Commons Lang中借用的,然后加以修改来支持评估LOGEVENTS。另外 Interpolator 类是从Apache Commons Configuration借用来允许StrSubstitutor评估来自多个StrLookups的变量。它也被修改为支持评估LogEvents。上述代码提供了一种机制,允许配置引用来自系统属性,配置文件,LogEvent中的ThreadContext Map,StructuredData的变量。如果组件能够处理它,则可以在处理配置时或处理每个事件时解析变量。

Api概览

当我们对上述这些概念有了一些些的了解之后我们可以开始我们的hello world之旅!

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>${log4j.version}</version>
        </dependency>

这里我们使用最新的版本,2.13.0。

为了能够让日志能够不走默认的日志等级打印,我们需要创建一个log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="debug">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

我们先忽略如何配置的知识点,后续讲到配置文件会讲它的语法,这里是因为默认的日志配置是error级别,为了能够打印,我们加上这个配置文件,让他不走默认。

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * @author chenly
 * @create 2020-02-20 20:41
 */
public class HelloLog4j {
    public static Logger logger= LogManager.getLogger(HelloLog4j.class);

    public static void main(String[] args) {
        logger.info("Hello, World!");
    }
}

如上是超级简单的示例,接下去我们看看它有哪些其他用法

参数替换

//1
if (logger.isDebugEnabled()) {
    logger.debug("Logging in user " + user.getName() + " with birthday " + user.getBirthdayCalendar());
}
//2
logger.debug("Logging in user {} with birthday {}", user.getName(), user.getBirthdayCalendar());

我们一般来说用下面的方式打印日志,很少使用1方式打印,因为“+”拼接字符串会使得日志打印速度慢上一些。

格式化参数

这个使用方式在logback里是没有的,如果toString()不是我们想要的打印结果,我们可以使用这种方式进行日志的格式化。 为了便于格式化,可以使用与Java的Formatter相同的格式字符串。

public static Logger logger= LogManager.getFormatterLogger(HelloLog4j.class);

    public static void main(String[] args) {
        User user=new User("nicole", LocalDateTime.now());

        logger.debug("Logging in user %s with birthday %s", user.getName(), user.getBirthDay());
        logger.debug("Logging in user %1$s with birthday %2$tm %2$te,%2$tY", user.getName(), user.getBirthDay());
        logger.debug("Integer.MAX_VALUE = %,d", Integer.MAX_VALUE);
        logger.debug("Long.MAX_VALUE = %,d", Long.MAX_VALUE);
    }
22:58:30.650 [main] DEBUG HelloLog4j - Logging in user nicole with birthday 2020-02-21T22:58:30.648
22:58:30.650 [main] DEBUG HelloLog4j - Logging in user nicole with birthday 02 21,2020
22:58:30.651 [main] DEBUG HelloLog4j - Integer.MAX_VALUE = 2,147,483,647
22:58:30.651 [main] DEBUG HelloLog4j - Long.MAX_VALUE = 9,223,372,036,854,775,807

混合使用:格式化+普通方式

Formatter logger可以对输出格式进行细粒度的控制,但缺点是必须指定正确的类型(例如,为%d格式参数传递除十进制整数以外的任何值都会产生异常)。

如果您的主要用法是使用{}样式的参数,但有时您需要对输出格式进行细粒度的控制,则可以使用printf方法:

    public static Logger logger= LogManager.getLogger(HelloLog4j.class);

    public static void main(String[] args) {
        logger.error("Hello, World!");

        User user=new User("nicole", LocalDateTime.now());

        logger.debug("Opening connection to {}...", user);
        logger.printf(Level.INFO, "Logging in user %1$s with birthday %2$tm %2$te,%2$tY", user.getName(), user.getBirthDay());
    }
23:07:58.348 [main] ERROR HelloLog4j - Hello, World!
23:07:58.364 [main] DEBUG HelloLog4j - Opening connection to User(name=nicole, birthDay=2020-02-21T23:07:58.362)...
23:07:58.365 [main] INFO  HelloLog4j - Logging in user nicole with birthday 02 21,2020

Java 8 Lambda支持延迟日志记录

在2.4版本之后,log4j 2支持了Lambda写法,可以懒加载的方式记录日志,不需要显式判断日志的等级

之前的写法:

if (logger.isDebugEnabled()) {
    logger.debug("Some long-running operation returned {}", expensiveOperation());
        }

现在:

logger.debug("Some long-running operation returned {}", () -> expensiveOperation());

Logger 名称

一般来说我们都是使用Java类名作为Logger的名字,因为Logger是有层级结构的,而Java的层级结构很适合这么使用,当然你也可以自定义名称,或者不定义名称。如果你不定义名称也可以,log4j就会使用最流行的Java类名作为我们的Logger名称。