Bootstrap

Java 日志框架Log4J2


前言
本篇博客主要介绍日志实现框架log4j2,其性能比logback更加好,未来趋势应该会使用slf4j+log4j2。其他日志框架内容可见日志专栏。

所有博客文件目录索引(包含日志框架系列学习):博客目录索引(持续更新)

一、认识Log4j2

1.1、介绍Log4j2

官网: https://logging.apache.org/log4j/2.x/

Log4j2:其是Apache推出一个日志实现框架同时也是一个日志门面,是Log4j的升级版,参考了logback的一些优秀设计并修复了一下问题,包含着重大的提升,主要有如下:

异常处理:在logback中,Appender的异常不会被应用感知到,但是在log4j2中提供了一些异常处理机制。
性能提升:Log4j2相对于log4j和logback都具有很明显的性能提升,大约有18倍。
自动重载配置:参考了logback的设计,会提供自动刷新配置,最实用的是在生产环境中可以动态的修改日志的级别而不需要重启应用,相当于热部署。
无垃圾机制:log4j2在大部分情况下,可以使用其设计的一套无垃圾机制,避免频繁的日志收集,减轻垃圾收集器的压力,并能提供更好的响应时间性能。

1.2、日志等级(6个)

查看org.apache.logging.log4j.Level源码,包含六个日志等级,如下:

OFF = new Level("OFF", StandardLevel.OFF.intLevel());//0
FATAL = new Level("FATAL", StandardLevel.FATAL.intLevel());//100
ERROR = new Level("ERROR", StandardLevel.ERROR.intLevel());//200
WARN = new Level("WARN", StandardLevel.WARN.intLevel());//300
INFO = new Level("INFO", StandardLevel.INFO.intLevel());//400
DEBUG = new Level("DEBUG", StandardLevel.DEBUG.intLevel());//500
TRACE = new Level("TRACE", StandardLevel.TRACE.intLevel());//600
ALL = new Level("ALL", StandardLevel.ALL.intLevel());//Integer.MAX_VALUE

其对应StandardLevel.OFF…是通过一个枚举类进行设置的。
若不进行自定义配置文件,默认日志等级为error

二、配置文件

若使用log4j2,我们可以自定义配置文件log4j2.xml,相关内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<!--  status="warn" 日志框架本身的输出日志级别(若是设置debug,log4j的配置过程也会展现出来)
      monitorInterval="5"  (监视时间间隔)自动加载配置文件的间隔时间,不低于5秒(及可以热部署)
-->

<Configuration status="warn" monitorInterval="5">
    <!--  集中配置属性进行管理,使用可通过:${name值}   -->
    <properties>
        <property name="LOG_HOME">D:/logs</property>
    </properties>
    <!--  日志处理   -->
    <Appenders>
        <!--   控制台输出 appender    -->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n" />
        </Console>

        <!--   日志文件输出 appender    -->
        <File name="file" fileName="${LOG_HOME}/myfile.log">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l%c{36} - %m%n" />
        </File>
    
        <!--   使用随机读写流的日志文件输出 appender 能够提高性能    -->
        <RandomAccessFile name="accessFile" fileName="${LOG_HOME}/myAcclog.log">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l%c{36} - %m%n" />
        </RandomAccessFile>
    
        <!--   按照一定规则拆分的文件日志的appender
               fileName:表示输出的文件路径  
               filePattern:在这里指定了拆分后的文件名,下面是以天为单位创建文件夹,并以分钟为单位创建日志文件
        -->
        <RollingFile name="rollingFile" fileName="${LOG_HOME}/myrollog.log"
                     filePattern="D:/logs/$${date:yyyy-MM-dd}/myrollog-%d{yyyy-MM-dd-HH-mm}-%i.log">
            <!--   日志级别过滤器(模仿的logback)这里设置日志等级为debug,匹配通过,不匹配不通过    -->
            <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY" />
            <!--   输出日志的消息格式    -->
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l%c{36} - %msg%n" />
            <!--   具体的拆分规则    -->
            <Policies>
                <!--   在系统启动时触发拆分规则,生产出一个新的配置文件(一旦重启即可触发)    -->
                <OnStartupTriggeringPolicy />
                <!--   按照文件指定大小拆分,设置为10MB    -->
                <SizeBasedTriggeringPolicy size="10 MB" />
                <!--   按照时间节点拆分,基于上面的filePattern规则定义    -->
                <TimeBasedTriggeringPolicy />
            </Policies>
            <!--   在同一个文件目录下,文件的个数限定为30个,超过则会进行覆盖   -->
            <DefaultRolloverStrategy max="30" />
        </RollingFile>
    </Appenders>
    
    <!--    logger定义(root或自定义)    -->
    <Loggers>
        <!--    配置RootLogger日志等级为trace,引用appender为上面配置name为console的ConsoleAppender    -->
        <Root level="trace">
            <AppenderRef ref="Console" />
        </Root>
    </Loggers>
    </Configuration>

三、实际应用

3.1、使用log4j2的日志门面

①引入log4j2的日志门面以及日志实现jar包:

<!--    Log4j2 门面API    -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.13.3</version>
</dependency>
<!--    Log4j2的日志实现   -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.11.1</version>
</dependency>

可以看到log4j-core中自带了日志门面的依赖。
②log4j2.xml配置文件见第二部分。

测试程序:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class LogTest {
 public static final Logger LOGGER= LogManager.getLogger(LogTest.class);
    public static void main(String[] args) {
        LOGGER.fatal("fatal");
        LOGGER.error("error");
        LOGGER.warn("warn");
        LOGGER.info("info");
        LOGGER.debug("debug");
        LOGGER.trace("trace");
    }
}

若是没有自定义配置文件的话会出现下面的情况:

3.2、使用slf4j+log4j2(推荐使用)

现如今主流使用slf4j作为日志门面,能够很方便的切换不同的日志框架

①我们在3.1基础上增加slf4j的日志门面以及slf4j绑定log4j2的适配器

<!--使用slf4j作为日志的门面,使用log4j2来记录日志 -->

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.30</version>
</dependency>
<!--为slf4j绑定日志实现 log4j2的适配器 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.10.0</version>
</dependency>
<!--    Log4j2 门面API    -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.13.3</version>
</dependency>
<!--    Log4j2的日志实现   -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.11.1</version>
</dependency>

我们可以注意到log4j-slf4j-impl的坐标中包含了其他三个坐标的依赖,这里为了演示所以引入了四个坐标,实际上我们直接引入slf4j-api、log4j-slf4j-impl两个坐标即可!!!
②添加log4j2.xml配置文件,见第二部分

测试程序:

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

public class Log4j2Test {
    //获取Logger实例
    public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);

    public static void main(String[] args) {
        System.out.println(LOGGER.getName());//xyz.changlu.LogTest
    
        //1、打印日志记录
        LOGGER.error("error");
        LOGGER.warn("warn");
        LOGGER.info("info");
        LOGGER.debug("debug");
        LOGGER.trace("trace");
    
        //2、占位符输出
        String name = "changlu";
        int age = 20;
        LOGGER.info("报错,name:{},age:{}",name,age);
    
        //3、打印堆栈信息
        try {
            int i = 5/0;
        }catch (Exception e){
            LOGGER.error("报错",e);
        }
    }
}

注意到这里使用的slf4j日志门面的API来调用LoggerFactory以及获取Logger实例。

四、异步日志

4.1、介绍异步日志

简单介绍
官网(Log4j2异步日志):https://logging.apache.org/log4j/2.x/manual/async.html

log4j2最大的特点就是异步日志,通过使用异步日志能够大大提升性能!

log4j2中提供了两种方式使用异步日志:AsyncAppender方式以及AsyncLogger方式。

其中AsyncLogger可以使用全局异步也可以是混合异步。

看下同步日志与异步日志之间的区别:

区别介绍

对于同步日志操作,先进行Logger中的操作并传输到LogEvent对象中接着再执行Appender操作进行日志输出,主线程会将这些步骤都执行完才算作一条日志结束。
对于异步日志操作,我们可以看到主线程当执行完Logger操作产生出LogEvent对象会放置到一个队列后就结束返回,对应在队列中的LogEvent会交由线程2操作,大大提升了效率。

性能比对
我们再看下官网提供的一个性能图比对图:

主要看64threads的性能表现对比特别突出:

第一个是使用AsyncLogger方式的全局异步,性能最高。
第二个是使用AsyncLogger方式的混合异步,性能会稍差全局异步。
第三个是使用AsyncAppender方式性能与Log4j、Logback都差不多了。

4.2、依赖jar包

引入对应依赖jar包即可使用log4j2的异步日志:

<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.3.4</version>
</dependency>

若是日志使用的是slf4j+log4j2,并使用log4j2的异步日志,直接引入如下依赖

    <!--使用slf4j作为日志的门面,使用log4j2来记录日志 -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.30</version>
    </dependency>
    <!--为slf4j绑定日志实现 log4j2的适配器 -->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <version>2.10.0</version>
    </dependency>
    <!--    Log4j2 门面API    -->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>2.13.3</version>
    </dependency>
    <!--    Log4j2的日志实现   -->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.11.1</version>
    </dependency>
    <!--    log4j2并发编程依赖包   -->
    <dependency>
        <groupId>com.lmax</groupId>
        <artifactId>disruptor</artifactId>
        <version>3.3.4</version>
    </dependency>

</dependencies>

4.3、异步日志的实际使用(3种)

①AsyncAppender方式
使用AsyncAppender是设置指定的一个appender为异步方式,如下:


<?xml version="1.0" encoding="UTF-8"?>

<Configuration status="debug" monitorInterval="5">
    <properties>
        <property name="LOG_HOME">D:/logs</property>
    </properties>
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n" />
        </Console>
        <!--   ①AsyncAppender方式 引用配置的appender    -->
        <Async name="Async">
            <AppenderRef ref="Console"/>
        </Async>
    </Appenders>
    <Loggers>
        <Root level="trace">
            <!--   ②引用name为Async的AsyncAppender方式    -->
            <AppenderRef ref="Async" />
        </Root>
    </Loggers>
</Configuration>

在Appenders标签中使用Async标签(引用想要进行异步日志的appender),最后将下面对应的RootLogger引用该异步Appender即可。
这种方式不太建议使用,因为其效率与logback差不多,并且日志输出时不显示行号。

②AsyncLogger方式
介绍

AsyncLogger才是log4j2的重头戏,也是官方推荐的异步方式。它可以使得调用Logger.log返回的 更快。你可以有两种选择:全局异步和混合异步。

全局异步则是让所有的日志都是异步的操作。
混合异步的话能够指定某个Appender进行异步,某个进行同步,更加灵活。

全局异步
全局异步就是所有的日志都是异步的操作,如何才能让所有的日志进行异步操作呢?我们只需要额外添加一个配置文件log4j2.component.properties并将其放置到resource目录下(Maven项目),配置内容如下:

Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

之后我们运行项目默认所有的日志都是异步的操作,需要注意打印的日志消息没有行号!!!

混合异步(含自定义logger)
在原本log4j2.xml配置文件中自定义logger对象设置为异步(不需要添加全局异步的配置文件)

log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="debug" monitorInterval="5">
    <properties>
        <property name="LOG_HOME">D:/logs</property>
    </properties>
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n" />
        </Console>
    </Appenders>
    <Loggers>
        <!--***************************************************-->
        <!--   自定义Logger对象(异步)
               includeLocation="false" 表示关闭日志记录的行号信息
               additivity="false"  不继承rootLogger对象
        -->
        <AsyncLogger name="xyz.changlu" level="trace"
                     includeLocation="false" additivity="false">
            <AppenderRef ref="file"/>
        </AsyncLogger>
        <!--***************************************************-->

        <Root level="trace">
            <AppenderRef ref="Console" />
        </Root>
    </Loggers>

</Configuration>

在标签中定义了一个AsyncLogger标签表示异步Logger,并且指定引用Appender。
这里配置RootLogger是同步的,自定义Logger是异步。

测试程序:

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

public class Log4j2Test {
    //获取Logger实例
    public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);

    public static void main(String[] args) {
        System.out.println(LOGGER.getName());//xyz.changlu.LogTest
    
        //1、打印日志记录
        LOGGER.error("error");
        LOGGER.warn("warn");
        LOGGER.info("info");
        LOGGER.debug("debug");
        LOGGER.trace("trace");
    
        //2、占位符输出
        String name = "changlu";
        int age = 20;
        LOGGER.info("报错,name:{},age:{}",name,age);
    
        //3、打印堆栈信息
        try {
            int i = 5/0;
        }catch (Exception e){
            LOGGER.error("报错",e);
        }
    
    }

}

使用这种方式的日志打印没有行号输出!!!

4.4、注意点

使用异步日志时的注意点如下:

使用异步日志时,打印的日志信息不会显示行号!
如果使用异步日志,AsyncAppender、AsyncLogger和全局日志,不要同时出现。否则性能会和 AsyncAppender一致,降至最低。
设置includeLocation=false ,打印位置信息会急剧降低异步日志的性能,比同步日志还要慢。

五、Log4j2的无垃圾模式介绍

垃圾收集暂停是延迟峰值的常见原因,并且对于许多系统而言,则需要花费更多大量精力来控制这些暂停。

许多日志库(包括以前版本的Log4j)在稳态日志记录期间分配临时对象,如日志事件对象,字符串, 字符数组,字节数组等。

这会对垃圾收集器造成压力并增加GC暂停发生的频率。

从版本2.6开始,默认情况下Log4j以“无垃圾”模式运行,其中重用对象和缓冲区,并且尽可能不分配临时对象。还有一个“低垃圾”模式,它不是完全无垃圾,但不使用ThreadLocal字段。

Log4j 2.6中的无垃圾日志记录部分通过重用ThreadLocal字段中的对象来实现,部分通过在将文本转换为字节时重用缓冲区来实现。

看下面两个版本的测试比较

使用Log4j 2.5:内存分配速率809 MB /秒,141个无效集合:

Log4j 2.6没有分配临时对象:0(零)垃圾回收:

通过使用无垃圾模式,我们可以看到GC收集数为0,并且每秒分配率为1.58MB/s。

避免创建临时对象机制的两个属性

有两个单独的系统属性可用于手动控制Log4j用于避免创建临时对象的机制:

log4j2.enableThreadlocals - 如果“true”(非Web应用程序的默认值)对象存储在ThreadLocal字段中并重新使用,否则将为每个日志事件创建新对象。
log4j2.enableDirectEncoders - 如果将“true”(默认)日志事件转换为文本,则将此文本转换 为字节而不创建临时对象。注意: 由于共享缓冲区上的同步,在此模式下多线程应用程序的同步日志记录性能可能更差。如果您的应用程序是多线程的并且日志记录性能很重要,请考虑使用异步记录器。

总结

1、Log4j2有6个日志等级,默认日志等级为error。—见第1部分

2、可设置定义配置文件log4j2.xml,包含了各个常使用的Appender以及RootLogger配置。—见第2部分

3、Log4j2也有日志门面,不过建议使用slf4j日志门面来对日志实现框架进行同一管理。—见第3部分

4、Log4j2也推出异步日志,其包含AsyncAppender方式、AsyncLogger方式((全局异步、混合异步))两种方式配置,其中全局异步日志性能最好大约是Logback的18倍,其次就是混合异步,性能最差的是AsyncAppender方式与Logback性能几乎差不多。使用异步日志需要注意的是其无法输出方法中的行号信息!—见4部分

5、在Log4j2的2.6版本之后使用无垃圾模式,通过重用ThreadLocal字段中的对象以及文本转换为字节时重用缓冲区来大大减少内存的使用。—见第5部分

;