为什么需要Log4j 2
Log4j1.X在经过长时间的迭代中,因为为了兼容他变得越来越难以维护,所以官方开启了Log4j2.X的项目,但是在珠玉在前的logback面前为什么还需要Log4j2.x呢?在官方的文档中我们找到了答案,官方列举了十几点,我们简单摘要几点:
- 在配置更新的时候,Log4j 1.x和Logback都会丢失事件,但是Log4j 2不会;Logback的appender出现异常是不会在项目中抛出的,即会把异常吃掉,但是Log4j 2可以通过配置暴露异常;
- Log4j 2搞了一个新的logger,很牛逼 在多线程中与Log4j 1.x和Logback相比,新的Logger的吞吐量高10倍,延迟降低了几个数量级;
- 垃圾回收机制很牛逼;
- Log4j 2使用插件化来扩展Log4j 2,例如lookup就是其中一个典型的插件;
- 支持自定义日志等级(这个我觉得跟marker也可以勉强一战);
- 支持Java8 的lambda表达式;
- 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
, Deny
或 Neutral
。Accept
的响应意味着不应该调用其他过滤器,并且事件将被处理。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名称。