JAVA中的日志

57 阅读25分钟

日志的概念

日志的概念

日志文件是用于记录系统操作事件的文件集合,可分为事件日志和消息日志。具有处理历史数据、诊断

问题的追踪以及理解系统的活动等重要作用。

在计算机中,日志文件是记录在操作系统或其他软件运行中发生的事件或在通信软件的不同用户之间的

消息的文件。记录是保持日志的行为。在最简单的情况下,消息被写入单个日志文件。

许多操作系统,软件框架和程序包括日志系统。广泛使用的日志记录标准是在因特网工程任务组

(IETF)RFC5424中定义的syslog。 syslog标准使专用的标准化子系统能够生成,过滤,记录和分析日

志消息。

调试日志

软件开发中,我们经常需要去调试程序,做一些信息,状态的输出便于我们查询程序的运行状况。为了

让我们能够更加灵活和方便的控制这些调试的信息,所有我们需要专业的日志技术。java中寻找bug会

需要重现。调试也就是debug 可以在程序运行中暂停程序运行,可以查看程序在运行中的情况。日志主

要是为了更方便的去重现问题。

系统日志

系统日志是记录系统中硬件、软件和系统问题的信息,同时还可以监视系统中发生的事件。用户可以通

过它来检查错误发生的原因,或者寻找受到攻击时攻击者留下的痕迹。系统日志包括系统日志、应用程

序日志和安全日志。

系统日志的价值

系统日志策略可以在故障刚刚发生时就向你发送警告信息,系统日志帮助你在最短的时间内发现问题。

系统日志是一种非常关键的组件,因为系统日志可以让你充分了解自己的环境。这种系统日志信息对于

决定故障的根本原因或者缩小系统攻击范围来说是非常关键的,因为系统日志可以让你了解故障或者袭

击发生之前的所有事件。为虚拟化环境制定一套良好的系统日志策略也是至关重要的,因为系统日志需

要和许多不同的外部组件进行关联。良好的系统日志可以防止你从错误的角度分析问题,避免浪费宝贵

的排错时间。另外一种原因是借助于系统日志,管理员很有可能会发现一些之前从未意识到的问题,在

几乎所有刚刚部署系统日志的环境当中。

日志门面和日志实现的关系

image.png

日志框架出现的历史顺序:

log4j -->JUL-->JCL--> slf4j --> logback --> log4j2

JAVA日志框架

问题:

  1. 控制日志输出的内容和格式
  2. 控制日志输出的位置
  3. 日志优化:异步日志,日志文件的归档和压缩
  4. 日志系统的维护
  5. 面向接口开发 -- 日志的门面

为什么要用日志框架

因为软件系统发展到今天已经很复杂了,特别是服务器端软件,涉及到的知识,内容,问题太多。在某 些方面使用别人成熟的框架,就相当于让别人帮你完成一些基础工作,你只需要集中精力完成系统的业 务逻辑设计。而且框架一般是成熟,稳健的,他可以处理系统很多细节问题,比如,事务处理,安全 性,数据流控制等问题。还有框架一般都经过很多人使用,所以结构很好,所以扩展性也很好,而且它 是不断升级的,你可以直接享受别人升级代码带来的好处。

现有的日志框架

JUL(java util logging)、logback、log4j、log4j2 JCL(Jakarta Commons Logging)、slf4j( Simple Logging Facade for Java)

日志门面

JCL、slf4j

日志实现

JUL、logback、log4j、log4j2

ps: slf4j,log4j,reload4j,log4j2,logback都是同一个作者

JUL

JUL全称Java util Logging是java原生的日志框架,使用时不需要另外引用第三方类库,相对其他日志框

架使用方便,学习简单,能够在小型应用中灵活使用。

JUL入门

架构介绍

image.png

  • Loggers:被称为记录器,应用程序通过获取Logger对象,调用其API来来发布日志信息。Logger通常是应用程序访问日志系统的入口程序。
  • Appenders:也被称为Handlers,每个Logger都会关联一组Handlers,Logger会将日志交给关联Handlers处理,由Handlers负责将日志做记录。Handlers在此是一个抽象,其具体的实现决定了日志记录的位置可以是控制台、文件、网络上的其他日志服务或操作系统日志等。
  • Layouts:也被称为Formatters,它负责对日志事件中的数据进行转换和格式化。Layouts决定了数据在一条日志记录中的最终形式
  • Level:每条日志消息都有一个关联的日志级别。该级别粗略指导了日志消息的重要性和紧迫,我可以将Level和Loggers,Appenders做关联以便于我们过滤消息。
  • Filters:过滤器,根据需要定制哪些信息会被记录,哪些信息会被放过。

总结一下就是:

用户使用Logger来进行日志记录,Logger持有若干个Handler,日志的输出操作是由Handler完成的。Handler在输出日志前,会经过Filter的过滤,判断哪些日志级别过滤放行哪些拦截,Handler会将日志内容输出到指定位置(日志文件、控制台等)。Handler在输出日志时会使用Layout,将输出内容进行排版。

入门案例

@Test
public void testQuick(){
    //1.创建日志记录器对象
    Logger logger = Logger.getLogger("com.wzwl.jul.JulTest");
    // 2.日志记录输出
    logger.info("hello jul");
​
    logger.log(Level.INFO,"info msg");
​
    String name = "itcast";
    Integer age = 13;
    logger.log(Level.INFO,"用户信息:{0},{1}",new Object[]{name,age});
}

日志的级别

jul中定义的日志级别

* java.util.logging.Level中定义了日志的级别:
    SEVERE(最高值)
    WARNING
    INFO (默认级别)
    CONFIG
    FINE
    FINER
    FINEST(最低值)
* 还有两个特殊的级别:
    OFF,可用来关闭日志记录。
    ALL,启用所有消息的日志记录。

虽然我们测试了7个日志级别但是默认只实现info以上的级别、

@Test
public void testLogLevel(){
    Logger logger = Logger.getLogger("com.wzwl.jul.JulTest");
​
    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
}

自定义日志级别配置

@Test
public void testLogConfig() throws IOException {
    //1.创建日志记录器对象
    Logger logger = Logger.getLogger("com.wzwl.jul.JulTest");
​
    //一、自定义日志级别
    //1.关闭系统默认配置
    logger.setUseParentHandlers(false);
    //2.创建handler对象
    ConsoleHandler consoleHandler = new ConsoleHandler();
    //3.创建Formatter对象
    SimpleFormatter simpleFormatter = new SimpleFormatter();
    //4.进行关联
    logger.addHandler(consoleHandler);
    consoleHandler.setFormatter(simpleFormatter);
​
    //二、输出到日志文件
    FileHandler fileHandler = new FileHandler("logs/jul.log");
    fileHandler.setFormatter(simpleFormatter);
    logger.addHandler(fileHandler);
​
    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
}

Logger之间的父子关系

JUL中Logger之间存在父子关系,这种父子关系通过树状结构存储,JUL在初始化时会创建一个顶层RootLogger作为所有Logger父Logger,存储上作为树状结构的根节点。并父子关系通过路径来关联。

@Test
public void testLogParent(){
    //日志记录器对象父子关系
    Logger logger1 = Logger.getLogger("com.wzwl");
    Logger logger2 = Logger.getLogger("com");
​
    System.out.println(logger1.getParent() == logger2);
    //日志记录器对象的顶级父元素 class为java.util.logging.LogManager$RootLogger name为""
    System.out.println("Logger2 Parent:"+logger2.getParent()+",name:"+logger2.getParent().getName());
​
    //一、自定义日志级别
    //1.关闭系统默认配置
    logger2.setUseParentHandlers(false);
    //2.创建handler对象
    ConsoleHandler consoleHandler = new ConsoleHandler();
    //3.创建Formatter对象
    SimpleFormatter simpleFormatter = new SimpleFormatter();
    //4.进行关联
    logger2.addHandler(consoleHandler);
    consoleHandler.setFormatter(simpleFormatter);
    //5.设置日志自己别
    logger2.setLevel(Level.ALL);
    consoleHandler.setLevel(Level.ALL);
​
    //测试日志记录器对象父子关系
    logger1.severe("severe");
    logger1.warning("warning");
    logger1.info("info");
    logger1.config("config");
    logger1.fine("fine");
    logger1.finer("finer");
    logger1.finest("finest");
}

日志的配置文件

默认配置文件路径$JAVAHOME\jre\lib\logging.properties

@Test
public void testProperties() throws IOException {
    //读取自定义的配置文件
    InputStream is = JulTest.class.getClassLoader().getResourceAsStream("logging.properties");
    //获取日志管理器对象
    LogManager logManager = LogManager.getLogManager();
    //通过日志管理器加载配置文件
    logManager.readConfiguration(is);
​
    Logger logger = Logger.getLogger("com.wzwl.jul.JulTest");
    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
}

配置文件:

## RootLogger使用的处理器(获取时设置)
handlers= java.util.logging.ConsoleHandler
# RootLogger日志等级
.level= INFO
​
## 自定义Logger
com.wzwl.handlers= java.util.logging.FileHandler
# 自定义Logger日志等级
com.wzwl.level= INFO
# 忽略父日志设置
com.wzwl.useParentHandlers=false## 控制台处理器
# 输出日志级别
java.util.logging.ConsoleHandler.level = INFO
# 输出日志格式
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
​
## 文件处理器
# 输出日志级别
java.util.logging.FileHandler.level=INFO
# 输出日志格式
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# 输出日志文件路径
java.util.logging.FileHandler.pattern = logs/java%u.log
# 输出日志文件限制大小(50000字节)
java.util.logging.FileHandler.limit = 50000
# 输出日志文件限制个数
java.util.logging.FileHandler.count = 10
# 输出日志文件 是否是追加
java.util.logging.FileHandler.append=true

日志原理解析

  1. 初始化LogManager

    1. LogManager加载logging.properties配置
    2. 添加Logger到LogManager
  2. 从单例LogManager获取Logger

  3. 设置级别Level,并指定日志记录LogRecord

  4. Filter提供了日志级别之外更细粒度的控制

  5. Handler是用来处理日志输出位置

  6. Formatter是用来格式化LogRecord的

image.png

Log4j

Log4j是Apache下的一款开源的日志框架,通过在项目中使用 Log4J,我们可以控制日志信息输出到控制台、文件、甚至是数据库中。我们可以控制每一条日志的输出格式,通过定义日志的输出级别,可以更灵活的控制日志的输出过程。方便项目的调试。

Log4j入门

  1. 建立maven工程
  2. 添加依赖
<dependencies>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
     </dependency>
</dependencies>
  1. java代码

    @Test
        public void testQuick(){
            //初始化系统配置,不需要配置文件
            BasicConfigurator.configure();
            //创建日志记录器对象
            Logger log = Logger.getLogger(TestLog4j.class);
            //日志记录输出
            log.info("hello log4j");
    ​
            // 日志级别
            log.fatal("fatal"); // 严重错误,一般会造成系统崩溃和终止运行
            log.error("error"); // 错误信息,但不会影响系统运行
            log.warn("warn"); // 警告信息,可能会发生问题
            log.info("info"); // 程序运行信息,数据库的连接、网络、IO操作等
            log.debug("debug"); // 调试信息,一般在开发阶段使用,记录程序的变量、参数等
            log.trace("trace"); // 追踪信息,记录程序的所有流程信息
    ​
        }
    

日志的级别

* 每个Logger都被了一个日志级别(log level),用来控制日志信息的输出。日志级别从高到低分为:
    fatal 指出每个严重的错误事件将会导致应用程序的退出。
    error 指出虽然发生错误事件,但仍然不影响系统的继续运行。
    warn 表明会出现潜在的错误情形。
    info 一般和在粗粒度级别上,强调应用程序的运行全程。
    debug 一般用于细粒度级别上,对调试应用程序非常有帮助。
    trace 是程序追踪,可以用于输出程序运行中的变量,显示执行的流程。
* 还有两个特殊的级别:
    OFF,可用来关闭日志记录。
    ALL,启用所有消息的日志记录。

注:一般只使用4个级别,优先级从高到低为 ERROR > WARN > INFO > DEBUG

Log4j组件

Log4J 主要由 Loggers (日志记录器)、Appenders(输出端)和 Layout(日志格式化器)组成。其中Loggers 控制日志的输出级别与日志是否输出;Appenders 指定日志的输出方式(输出到控制台、文件等);Layout 控制日志信息的输出格式。

Loggers

日志记录器,负责收集处理日志记录,实例的命名就是类“XX”的full quailied name(类的全限定名),Logger的名字大小写敏感,其命名有继承机制:例如:name为org.apache.commons的logger会继承name为org.apache的logger。

Log4J中有一个特殊的logger叫做“root”,他是所有logger的根,也就意味着其他所有的logger都会直接或者间接地继承自root。root logger可以用Logger.getRootLogger()方法获取。

但是,自log4j 1.2版以来, Logger 类已经取代了 Category 类。对于熟悉早期版本的log4j的人来说,Logger 类可以被视为 Category 类的别名。

image.png

Appenders

Appender 用来指定日志输出到哪个地方,可以同时指定日志的输出目的地。Log4j 常用的输出目的地有以下几种:

输出端类型作用
ConsoleAppender将日志输出到控制台
FileAppender将日志输出到文件中
DailyRollingFileAppender将日志输出到一个日志文件,并且每天输出到一个新的文件
RollingFileAppender将日志信息输出到一个日志文件,并且指定文件的尺寸,当文件大小达到指定尺寸时,会自动把文件改名,同时产生一个新的文件
JDBCAppender把日志信息保存到数据库中

Layouts

布局器 Layouts用于控制日志输出内容的格式,让我们可以使用各种需要的格式输出日志。Log4j常用的Layouts:

格式化器类型作用
HTMLLayout格式化日志输出为HTML表格形式
SimpleLayout简单的日志输出格式化,打印的日志格式为(info - message)
PatternLayout最强大的格式化期,可以根据自定义格式输出日志,如果没有指定转换格式,就是用默认的转换格式

Layout的格式

在 log4j.properties 配置文件中,我们定义了日志输出级别与输出端,在输出端中分别配置日志的输出格式。

* log4j 采用类似 C 语言的 printf 函数的打印格式格式化日志信息,具体的占位符及其含义如下:
    %m 输出代码中指定的日志信息
    %p 输出优先级,及 DEBUG、INFO 等
    %n 换行符(Windows平台的换行符为 "\n",Unix 平台为 "\n")
    %r 输出自应用启动到输出该 log 信息耗费的毫秒数
    %c 输出打印语句所属的类的全名
    %t 输出产生该日志的线程全名
    %d 输出服务器当前时间,默认为 ISO8601,也可以指定格式,如:%d{yyyy年MM月dd日HH:mm:ss}
    %l 输出日志时间发生的位置,包括类名、线程、及在代码中的行数。如:Test.main(Test.java:10)
    %F 输出日志消息产生时所在的文件名称
    %L 输出代码中的行号
    %% 输出一个 "%" 字符
* 可以在 % 与字符之间加上修饰符来控制最小宽度、最大宽度和文本的对其方式。如:
    %5c 输出category名称,最小宽度是5,category<5,默认的情况下右对齐
    %-5c 输出category名称,最小宽度是5,category<5,"-"号指定左对齐,会有空格
    %.5c 输出category名称,最大宽度是5,category>5,就会将左边多出的字符截掉,<5不会有空格
    %20.30c category名称<20补空格,并且右对齐,>30字符,就从左边交远销出的字符截掉

Appender的输出

控制台,文件,数据库

#指定日志的输出级别与输出端
log4j.rootLogger=INFO,Console
# 控制台输出配置
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n
# 文件输出配置
log4j.appender.A = org.apache.log4j.DailyRollingFileAppender
#指定日志的输出路径
log4j.appender.A.File = D:/log.txt
log4j.appender.A.Append = true
#使用自定义日志格式化器
log4j.appender.A.layout = org.apache.log4j.PatternLayout
#指定日志的输出格式
log4j.appender.A.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [%t:%r] -[%p] %m%n
#指定日志的文件编码
log4j.appender.A.encoding=UTF-8
#mysql
log4j.appender.logDB=org.apache.log4j.jdbc.JDBCAppender
log4j.appender.logDB.layout=org.apache.log4j.PatternLayout
log4j.appender.logDB.Driver=com.mysql.jdbc.Driver
log4j.appender.logDB.URL=jdbc:mysql://localhost:3306/test
log4j.appender.logDB.User=root
log4j.appender.logDB.Password=root
log4j.appender.logDB.Sql=INSERT INTO
log(project_name,create_date,level,category,file_name,thread_name,line,all_categ
ory,message) values('itcast','%d{yyyy-MM-dd
HH:mm:ss}','%p','%c','%F','%t','%L','%l','%m')
CREATE TABLE `log` (
    `log_id` int(11) NOT NULL AUTO_INCREMENT,
    `project_name` varchar(255) DEFAULT NULL COMMENT '目项名',
    `create_date` varchar(255) DEFAULT NULL COMMENT '创建时间',
    `level` varchar(255) DEFAULT NULL COMMENT '优先级',
    `category` varchar(255) DEFAULT NULL COMMENT '所在类的全名',
    `file_name` varchar(255) DEFAULT NULL COMMENT '输出日志消息产生时所在的文件名称 ',
    `thread_name` varchar(255) DEFAULT NULL COMMENT '日志事件的线程名',
    `line` varchar(255) DEFAULT NULL COMMENT '号行',
    `all_category` varchar(255) DEFAULT NULL COMMENT '日志事件的发生位置',
    `message` varchar(4000) DEFAULT NULL COMMENT '输出代码中指定的消息',
    PRIMARY KEY (`log_id`)
);

ps: 开起log4j打印自己输出的信息

LogLog.setInternalDebugging(true);//开起log4j打印自己输出的信息

自定义Logger

# RootLogger配置
log4j.rootLogger = trace,console
# 自定义Logger
log4j.logger.com.itheima = info,file
log4j.logger.org.apache = error
@Test
    public void testQuick(){
        //初始化系统配置,不需要配置文件
        //BasicConfigurator.configure();
        LogLog.setInternalDebugging(true);//开起log4j打印自己输出的信息
        //创建日志记录器对象
        Logger log = Logger.getLogger(TestLog4j.class);
        //日志记录输出
        log.info("hello log4j");

        // 日志级别
        log.fatal("fatal"); // 严重错误,一般会造成系统崩溃和终止运行
        log.error("error"); // 错误信息,但不会影响系统运行
        log.warn("warn"); // 警告信息,可能会发生问题
        log.info("info"); // 程序运行信息,数据库的连接、网络、IO操作等
        log.debug("debug"); // 调试信息,一般在开发阶段使用,记录程序的变量、参数等
        log.trace("trace"); // 追踪信息,记录程序的所有流程信息
    }

JCL

全称为Jakarta Commons Logging,是Apache提供的一个通用日志API。它是为 "所有的Java日志实现"提供一个统一的接口,它自身也提供一个日志的实现,但是功能非常常弱(SimpleLog)。所以一般不会单独使用它。他允许开发人员使用不同的具体日志实现工具: Log4j, Jdk自带的日志(JUL)

JCL 有两个基本的抽象类:Log(基本记录器)和LogFactory(负责创建Log实例)。

image.png

JCL入门

  1. 建立maven工程

  2. 添加依赖

    <dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
    </dependency>
    
  3. 入门代码

    public class JULTest {
        @Test
        public void testQuick() throws Exception {
            // 创建日志对象
            Log log = LogFactory.getLog(JULTest.class);
            // 日志记录输出
            log.fatal("fatal");
            log.error("error");
            log.warn("warn");
            log.info("info");
            log.debug("debug");
        }
    }
    

我们为什么要使用日志门面:

  1. 面向接口开发,不再依赖具体的实现类。减少代码的耦合
  2. 项目通过导入不同的日志实现类,可以灵活的切换日志框架
  3. 统一API,方便开发者学习和使用
  4. 统一配置便于项目日志的管理

JCL原理

  1. 通过LogFactory动态加载Log实现类

image.png

  1. 日志门面支持的日志实现数组

    private static final String[] classesToDiscover =
        new String[]{"org.apache.commons.logging.impl.Log4JLogger",
                    "org.apache.commons.logging.impl.Jdk14Logger",
                    "org.apache.commons.logging.impl.Jdk13LumberjackLogger",
                    "org.apache.commons.logging.impl.SimpleLog"};
    
  2. 获取具体的日志实现

for(int i = 0; i < classesToDiscover.length && result == null; ++i) {
    result = this.createLogFromClass(classesToDiscover[i], logCategory,true);
}

SLF4J

简单日志门面(Simple Logging Facade For Java) SLF4J主要是为了给Java日志访问提供一套标准、规范的API框架,其主要意义在于提供接口,具体的实现可以交由其他日志框架,例如log4j和logback等。当然slf4j自己也提供了功能较为简单的实现,但是一般很少用到。对于一般的Java项目而言,日志框架会选择slf4j-api作为门面,配上具体的实现框架(log4j、logback等),中间使用桥接器完成桥接。

官方网站: www.slf4j.org/

SLF4J是目前市面上最流行的日志门面。现在的项目中,基本上都是使用SLF4J作为我们的日志系统。

SLF4J日志门面主要提供两大功能:

1. 日志框架的绑定
2. 日志框架的桥接

SLF4J入门

  1. 添加依赖

    <!--slf4j core 使用slf4j必須添加-->
    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.27</version>
    </dependency>
    <!--slf4j 自带的简单日志实现 -->
    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.27</version>
    </dependency>
    
  2. 编写代码

    public class Slf4jTest {
    // 声明日志对象
    public final static Logger LOGGER =
    LoggerFactory.getLogger(Slf4jTest.class);
    @Test
    public void testQuick() throws Exception {
    //打印日志信息
    LOGGER.error("error");
    LOGGER.warn("warn");
    LOGGER.info("info");
    LOGGER.debug("debug");
    LOGGER.trace("trace");
    // 使用占位符输出日志信息
    String name = "jack";
    Integer age = 18;
    LOGGER.info("用户:{},{}", name, age);
    // 将系统异常信息写入日志
    try {
    int i = 1 / 0;
    } catch (Exception e) {
    // e.printStackTrace();
    LOGGER.info("出现异常:", e);
    }
    }
    }
    

为什么要使用SLF4J作为日志门面?

  1. 使用SLF4J框架,可以在部署时迁移到所需的日志记录框架。
  2. SLF4J提供了对所有流行的日志框架的绑定,例如log4j,JUL,Simple logging和NOP。因此可以在部署时切换到任何这些流行的框架。
  3. 无论使用哪种绑定,SLF4J都支持参数化日志记录消息。由于SLF4J将应用程序和日志记录框架分离,因此可以轻松编写独立于日志记录框架的应用程序。而无需担心用于编写应用程序的日志记录框架。
  4. SLF4J提供了一个简单的Java工具,称为迁移器。使用此工具,可以迁移现有项目,这些项目使用日志框架(如Jakarta Commons Logging(JCL)或log4j或Java.util.logging(JUL))到SLF4J。

绑定日志的实现(Binding)

如前所述,SLF4J支持各种日志框架。SLF4J发行版附带了几个称为“SLF4J绑定”的jar文件,每个绑定对应一个受支持的框架。

使用slf4j的日志绑定流程:

  1. 添加slf4j-api的依赖

  2. 使用slf4j的API在项目中进行统一的日志记录

  3. 绑定具体的日志实现框架

    1. 绑定已经实现了slf4j的日志框架,直接添加对应依赖
    2. 绑定没有实现slf4j的日志框架,先添加日志的适配器,再添加实现类的依赖
  4. slf4j有且仅有一个日志实现框架的绑定(如果出现多个默认使用第一个依赖日志实现)

通过maven引入常见的日志实现框架:

<!--slf4j core 使用slf4j必須添加-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.27</version>
</dependency>
<!-- log4j-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.27</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- jul -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.27</version>
</dependency>
<!--jcl -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jcl</artifactId>
<version>1.7.27</version>
</dependency>
<!-- nop -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.27</version>
</dependency>

要切换日志框架,只需替换类路径上的slf4j绑定。例如,要从java.util.logging切换到log4j,只需将slf4j-jdk14-1.7.27.jar替换为slf4j-log4j12-1.7.27.jar即可。

SLF4J不依赖于任何特殊的类装载。实际上,每个SLF4J绑定在编译时都是硬连线的,以使用一个且只有一个特定的日志记录框架。例如,slf4j-log4j12-1.7.27.jar绑定在编译时绑定以使用log4j。在您的代码中,除了slf4j-api-1.7.27.jar之外,您只需将您选择的一个且只有一个绑定放到相应的类路径位置。不要在类路径上放置多个绑定。以下是一般概念的图解说明。

image.png

桥接旧的日志框架(Bridging)

通常,您依赖的某些组件依赖于SLF4J以外的日志记录API。您也可以假设这些组件在不久的将来不会切换到SLF4J。为了解决这种情况,SLF4J附带了几个桥接模块,这些模块将对log4j,JCL和java.util.logging API的调用重定向,就好像它们是对SLF4J API一样。

桥接解决的是项目中日志的遗留问题,当系统中存在之前的日志API,可以通过桥接转换到slf4j的实现

  1. 先去除之前老的日志框架的依赖
  2. 添加SLF4J提供的桥接组件
  3. 为项目添加SLF4J的具体实现

image.png

迁移的方式:

如果我们要使用SLF4J的桥接器,替换原有的日志框架,那么我们需要做的第一件事情,就是删除掉原有项目中的日志框架的依赖。然后替换成SLF4J提供的桥接器。

<!-- log4j-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>
<!-- jul -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jul-to-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>
<!--jcl -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>

注意问题:

  1. jcl-over-slf4j.jar和 slf4j-jcl.jar不能同时部署。前一个jar文件将导致JCL将日志系统的选择委托给SLF4J,后一个jar文件将导致SLF4J将日志系统的选择委托给JCL,从而导致无限循环。
  2. log4j-over-slf4j.jar和slf4j-log4j12.jar不能同时出现
  3. jul-to-slf4j.jar和slf4j-jdk14.jar不能同时出现
  4. 所有的桥接都只对Logger日志记录器对象有效,如果程序中调用了内部的配置类或者是Appender,Filter等对象,将无法产生效果。

SLF4J原理解析

  1. SLF4J通过LoggerFactory加载日志具体的实现对象。
  2. LoggerFactory在初始化的过程中,会通过performInitialization()方法绑定具体的日志实现。
  3. 在绑定具体实现的时候,通过类加载器,加载org/slf4j/impl/StaticLoggerBinder.class
  4. 所以,只要是一个日志实现框架,在org.slf4j.impl包中提供一个自己的StaticLoggerBinder类,在其中提供具体日志实现的LoggerFactory就可以被SLF4J所加载

Logback的使用

Logback是由log4j创始人设计的另一个开源日志组件,性能比log4j要好。

官方网站:logback.qos.ch/index.html

Logback主要分为三个模块:

  • logback-core:其它两个模块的基础模块
  • logback-classic:它是log4j的一个改良版本,同时它完整实现了slf4j API
  • logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能

后续的日志代码都是通过SLF4J日志门面搭建日志系统,所以在代码是没有区别,主要是通过修改配置文件和pom.xml依赖

logback入门

  1. 添加依赖

    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
    </dependency>
    <dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
    </dependency>
    
  2. java代码

    //定义日志对象
    public final static Logger LOGGER =
    LoggerFactory.getLogger(LogBackTest.class);
    @Test
    public void testSlf4j(){
    //打印日志信息
    LOGGER.error("error");
    LOGGER.warn("warn");
    LOGGER.info("info");
    LOGGER.debug("debug");
    LOGGER.trace("trace");
    }
    

logback配置

logback会依次读取以下类型配置文件:

  • logback.groovy
  • logback-test.xml
  • logback.xml 如果均不存在会采用默认配置

logback组件之间的关系

  1. Logger:日志的记录器,把它关联到应用的对应的context上后,主要用于存放日志对象,也可以定义日志类型、级别。
  2. Appender:用于指定日志输出的目的地,目的地可以是控制台、文件、数据库等等。
  3. Layout:负责把事件转换成字符串,格式化的日志信息的输出。在logback中Layout对象被封装在encoder中。

基本配置信息

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--
    日志输出格式:
    %-5level
    %d{yyyy-MM-dd HH:mm:ss.SSS}日期
    %c类的完整名称
    %M为method
    %L为行号
    %thread线程名称
    %m或者%msg为信息
    %n换行
    -->
    <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
    <property name="pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %c [%thread] %-5level %msg%n"/>
​
    <!--
        Appender: 设置日志信息的去向,常用的有以下几个:
            ch.qos.logback.core.ConsoleAppender (控制台)
            ch.qos.logback.core.rolling.RollingFileAppender (文件大小到达指定尺寸的时候产生一个新文件)
            ch.qos.logback.core.FileAppender (文件)
    -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!--输出流对象 默认 System.out 改为 System.err-->
        <target>System.err</target>
        <!--日志格式配置-->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
    </appender>
    
    <root level="ALL">
        <appender-ref ref="console"/>
    </root>
</configuration>

FileAppender配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--
    日志输出格式:
    %-5level
    %d{yyyy-MM-dd HH:mm:ss.SSS}日期
    %c类的完整名称
    %M为method
    %L为行号
    %thread线程名称
    %m或者%msg为信息
    %n换行
    -->
    <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
    <property name="pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %c [%thread] %-5level %msg%n"/>
    <!-- 日志文件存放目录 -->
    <property name="log_dir" value="logs"></property>
​
    <!--
        Appender: 设置日志信息的去向,常用的有以下几个:
            ch.qos.logback.core.ConsoleAppender (控制台)
            ch.qos.logback.core.rolling.RollingFileAppender (文件大小到达指定尺寸的时候产生一个新文件)
            ch.qos.logback.core.FileAppender (文件)
    -->
​
    <!-- 生成html格式appender对象 -->
    <appender name="htmlFile" class="ch.qos.logback.core.FileAppender">
        <!--日志格式配置-->
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <layout class="ch.qos.logback.classic.html.HTMLLayout">
                <pattern>%level%d{yyyy-MM-dd HH:mm:ss}%c%M%L%thread%m</pattern>
            </layout>
        </encoder>
        <!--日志输出路径-->
        <file>${log_dir}/logback.html</file>
    </appender>
​
    <!--日志文件输出appender对象-->
    <appender name="file" class="ch.qos.logback.core.FileAppender">
        <!--日志格式配置-->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
        <!--日志输出路径-->
        <file>${log_dir}/logback.log</file>
    </appender>
​
    <!--控制台输出appender对象-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!--输出流对象 默认 System.out 改为 System.err-->
        <target>System.err</target>
        <!--日志格式配置-->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
    </appender>
​
    <root level="ALL">
        <appender-ref ref="console"/>
        <appender-ref ref="file"/>
        <appender-ref ref="htmlFile"/>
    </root>
</configuration>

RollingFileAppender配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--
    日志输出格式:
    %-5level
    %d{yyyy-MM-dd HH:mm:ss.SSS}日期
    %c类的完整名称
    %M为method
    %L为行号
    %thread线程名称
    %m或者%msg为信息
    %n换行
    -->
    <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
    <property name="pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %c [%thread] %-5level %msg%n"/>
    <!-- 日志文件存放目录 -->
    <property name="log_dir" value="logs"></property>
​
    <!--
        Appender: 设置日志信息的去向,常用的有以下几个:
            ch.qos.logback.core.ConsoleAppender (控制台)
            ch.qos.logback.core.rolling.RollingFileAppender (文件大小到达指定尺寸的时候产生一个新文件)
            ch.qos.logback.core.FileAppender (文件)
    -->
​
    <!-- 生成html格式appender对象 -->
    <appender name="htmlFile" class="ch.qos.logback.core.FileAppender">
        <!--日志格式配置-->
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <layout class="ch.qos.logback.classic.html.HTMLLayout">
                <pattern>%level%d{yyyy-MM-dd HH:mm:ss}%c%M%L%thread%m</pattern>
            </layout>
        </encoder>
        <!--日志输出路径-->
        <file>${log_dir}/logback.html</file>
    </appender>
​
    <!--日志文件输出appender对象-->
    <appender name="file" class="ch.qos.logback.core.FileAppender">
        <!--日志格式配置-->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
        <!--日志输出路径-->
        <file>${log_dir}/logback.log</file>
    </appender>
​
    <!--控制台输出appender对象-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!--输出流对象 默认 System.out 改为 System.err-->
        <target>System.err</target>
        <!--日志格式配置-->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
    </appender>
​
    <!-- 日志文件拆分和归档的appender对象-->
    <appender name="rollFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--日志格式配置-->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
        <!--日志输出路径-->
        <file>${log_dir}/roll_logback.log</file>
        <!--指定日志文件拆分和压缩规则-->
        <rollingPolicy
                class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!--通过指定压缩文件名称,来确定分割文件方式-->
            <fileNamePattern>${log_dir}/rolling.%d{yyyy-MM-dd}.log%i.gz</fileNamePattern>
            <!--文件拆分大小-->
            <maxFileSize>1MB</maxFileSize>
        </rollingPolicy>
    </appender>
​
    <root level="ALL">
        <appender-ref ref="console"/>
        <appender-ref ref="rollFile"/>
        <!--<appender-ref ref="file"/>-->
        <!--<appender-ref ref="htmlFile"/>-->
    </root>
</configuration>

Filter和异步日志配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--
    日志输出格式:
    %-5level
    %d{yyyy-MM-dd HH:mm:ss.SSS}日期
    %c类的完整名称
    %M为method
    %L为行号
    %thread线程名称
    %m或者%msg为信息
    %n换行
    -->
    <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
    <property name="pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %c [%thread] %-5level %msg%n"/>
    <!-- 日志文件存放目录 -->
    <property name="log_dir" value="logs"></property>
​
    <!--
        Appender: 设置日志信息的去向,常用的有以下几个:
            ch.qos.logback.core.ConsoleAppender (控制台)
            ch.qos.logback.core.rolling.RollingFileAppender (文件大小到达指定尺寸的时候产生一个新文件)
            ch.qos.logback.core.FileAppender (文件)
    -->
​
    <!-- 生成html格式appender对象 -->
    <appender name="htmlFile" class="ch.qos.logback.core.FileAppender">
        <!--日志格式配置-->
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <layout class="ch.qos.logback.classic.html.HTMLLayout">
                <pattern>%level%d{yyyy-MM-dd HH:mm:ss}%c%M%L%thread%m</pattern>
            </layout>
        </encoder>
        <!--日志输出路径-->
        <file>${log_dir}/logback.html</file>
    </appender>
​
    <!--日志文件输出appender对象-->
    <appender name="file" class="ch.qos.logback.core.FileAppender">
        <!--日志格式配置-->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
        <!--日志输出路径-->
        <file>${log_dir}/logback.log</file>
    </appender>
​
    <!--控制台输出appender对象-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!--输出流对象 默认 System.out 改为 System.err-->
        <target>System.err</target>
        <!--日志格式配置-->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
    </appender>
​
    <!-- 日志文件拆分和归档的appender对象-->
    <appender name="rollFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--日志格式配置-->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
        <!--日志输出路径-->
        <file>${log_dir}/roll_logback.log</file>
        <!--指定日志文件拆分和压缩规则-->
        <rollingPolicy
                class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!--通过指定压缩文件名称,来确定分割文件方式-->
            <fileNamePattern>${log_dir}/rolling.%d{yyyy-MM-dd}.log%i.gz</fileNamePattern>
            <!--文件拆分大小-->
            <maxFileSize>1MB</maxFileSize>
        </rollingPolicy>
        <!--filter配置-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!--设置拦截日志级别-->
            <level>error</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
​
    <!--异步日志-->
    <appender name="async" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="rollFile"/>
    </appender>
​
    <root level="ALL">
        <appender-ref ref="console"/>
        <appender-ref ref="rollFile"/>
        <!--<appender-ref ref="file"/>-->
        <!--<appender-ref ref="htmlFile"/>-->
    </root>
​
    <!--自定义logger additivity表示是否从rootLogger继承配置-->
    <logger name="com.wzwl" level="debug" additivity="false">
        <appender-ref ref="console"/>
    </logger>
</configuration>

官方提供的log4j.properties转换成logback.xml

logback.qos.ch/translator/

logback-access的使用

logback-access模块与Servlet容器(如Tomcat和Jetty)集成,以提供HTTP访问日志功能。我们可以使用logback-access模块来替换tomcat的访问日志。

  1. 将logback-access.jar与logback-core.jar复制到$TOMCAT_HOME/lib/目录下
  2. 修改$TOMCAT_HOME/conf/server.xml中的Host元素中添加:
<Valve className="ch.qos.logback.access.tomcat.LogbackValve" />
  1. logback默认会在$TOMCAT_HOME/conf下查找文件 logback-access.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
    <!-- always a good activate OnConsoleStatusListener -->
    <statusListener
    class="ch.qos.logback.core.status.OnConsoleStatusListener"/>
    <property name="LOG_DIR" value="${catalina.base}/logs"/>
    <appender name="FILE"
    class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${LOG_DIR}/access.log</file>
    <rollingPolicy
    class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <fileNamePattern>access.%d{yyyy-MM-dd}.log.zip</fileNamePattern>
    </rollingPolicy>
    <encoder>
    <!-- 访问日志的格式 -->
    <pattern>combined</pattern>
    </encoder>
    </appender>
    <appender-ref ref="FILE"/>
    </configuration>
    
  2. 官方配置:

    logback.qos.ch/access.html…

log4j2的使用

Apache Log4j 2是对Log4j的升级版,参考了logback的一些优秀的设计,并且修复了一些问题,因此带来了一些重大的提升,主要有:

  • 异常处理,在logback中,Appender中的异常不会被应用感知到,但是在log4j2中,提供了一些异常处理机制。
  • 性能提升, log4j2相较于log4j 和logback都具有很明显的性能提升,后面会有官方测试的数据。
  • 自动重载配置,参考了logback的设计,当然会提供自动刷新参数配置,最实用的就是我们在生上可以动态的修改日志的级别而不需要重启应用。
  • 无垃圾机制,log4j2在大部分情况下,都可以使用其设计的一套无垃圾机制,避免频繁的日志收集导致的jvm gc。

Log4j2入门

目前市面上最主流的日志门面就是SLF4J,虽然Log4j2也是日志门面,因为它的日志实现功能非常强大,性能优越。所以大家一般还是将Log4j2看作是日志的实现,Slf4j + Log4j2应该是未来的大势所趋。

  1. 添加依赖
<!-- Log4j2 门面API-->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.11.1</version>
        </dependency>
        <!-- Log4j2 日志实现 -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.11.1</version>
        </dependency>
  1. JAVA代码

    public class TestLog4j2 {
    ​
        // 定义日志记录器对象
        public static final Logger LOGGER = LogManager.getLogger(TestLog4j2.class);
    ​
        @Test
        public void testQuick() throws Exception {
            LOGGER.fatal("fatal");
            LOGGER.error("error");
            LOGGER.warn("warn");
            LOGGER.info("info");
            LOGGER.debug("debug");
            LOGGER.trace("trace");
        }
    ​
    ​
    }
    
  2. 使用slf4j作为日志的门面,使用log4j2作为日志的实现

<!--使用slf4j作为日志的门面,使用log4j2来记录日志 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.27</version>
        </dependency>
        <!--为slf4j绑定日志实现 log4j2的适配器 -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.10.0</version>
        </dependency>

Log4j2配置

log4j2默认加载classpath下的 log4j2.xml 文件中的配置。

<?xml version="1.0" encoding="UTF-8"?>
<!--
    status="warn" 日志框架本身的级别
    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-->
        <RollingFile name="rollingFile" fileName="${LOG_HOME}/myrollog.log"
                     filePattern="D:/logs/$${date:yyyy-MM-dd}/myrollog-%d{yyyy-MM-dd-HH-mm}-%i.log">
            <!--日志级别过滤器-->
            <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/>
                <!--按照文件大小进行拆分-->
                <SizeBasedTriggeringPolicy size="10 MB"/>
                <!--按照时间节点进行拆分,根据filePattern定义的-->
                <TimeBasedTriggeringPolicy/>
            </Policies>
            <!--在同一个目录下,文件的个数限定为30个,超过进行覆盖-->
            <DefaultRolloverStrategy max="30"/>
        </RollingFile>
    </Appenders>
​
    <!--Logger定义-->
    <Loggers>
        <!--使用rootLogger配置日志级别-->
        <Root level="trace">
            <!--指定日志使用的处理器-->
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

Log4j2异步日志

异步日志

log4j2最大的特点就是异步日志,其性能的提升主要也是从异步日志中受益,我们来看看如何使用log4j2的异步日志。

  • 同步日志

image.png

  • 异步日志

image.png

Log4j2提供了两种实现日志的方式,一个是通过AsyncAppender,一个是通过AsyncLogger,分别对应前面我们说的Appender组件和Logger组件。

注意:配置异步日志需要添加依赖

<!--异步日志依赖-->
        <dependency>
            <groupId>com.lmax</groupId>
            <artifactId>disruptor</artifactId>
            <version>3.3.4</version>
        </dependency>
  1. AsyncAppender方式

    <?xml version="1.0" encoding="UTF-8"?>
    <!--
        status="warn" 日志框架本身的级别
        monitorInterval="5" 自动加载配置文件的间隔事件,不低于5秒
    -->
    <Configuration status="debug" monitorInterval="5">
    ​
        <!--集中配置属性进行管理,使用时通过:${name}-->
        <properties>
            <property name="LOG_HOME">logs</property>
        </properties>
    ​
        <!--日志处理-->
        <Appenders>
            <!--控制台输出的appender-->
            <!--<Console name="Console" target="SYSTEM_OUT">-->
            <Console name="Console" target="SYSTEM_ERR">
                <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>
    ​
            <!--把file appender设为异步的-->
            <Async name="Async">
                <AppenderRef ref="file"/>
            </Async>
    ​
            <!--使用随机读写流的日志文件输出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-->
            <RollingFile name="rollingFile" fileName="${LOG_HOME}/myrollog.log"
                         filePattern="D:/logs/$${date:yyyy-MM-dd}/myrollog-%d{yyyy-MM-dd-HH-mm}-%i.log">
                <!--日志级别过滤器-->
                <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/>
                    <!--按照文件大小进行拆分-->
                    <SizeBasedTriggeringPolicy size="10 MB"/>
                    <!--按照时间节点进行拆分,根据filePattern定义的-->
                    <TimeBasedTriggeringPolicy/>
                </Policies>
                <!--在同一个目录下,文件的个数限定为30个,超过进行覆盖-->
                <DefaultRolloverStrategy max="30"/>
            </RollingFile>
        </Appenders>
    ​
        <!--Logger定义-->
        <Loggers>
    ​
            <!--
                includeLocation="false" 关闭日志记录的行号信息
                additivity="false" 不再集成rootLogger对象
            -->
            <!--<AsyncLogger name="com.wzwl" level="trace" includeLocation="false" additivity="false">
                <AppenderRef ref="Console"/>
            </AsyncLogger>-->
    ​
            <!--使用rootLogger配置日志级别-->
            <Root level="trace">
                <!--指定日志使用的处理器-->
                <AppenderRef ref="Console"/>
                <!--调用异步appender-->
                <AppenderRef ref="Async"/>
            </Root>
        </Loggers>
    </Configuration>
  2. AsyncLogger方式

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

    1. 全局异步就是,所有的日志都异步的记录,在配置文件上不用做任何改动,只需要添加一个log4j2.component.properties 配置;

      Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
      
    2. 混合异步就是,你可以在应用中同时使用同步日志和异步日志,这使得日志的配置方式更加灵活。

      <AsyncLogger name="com.wzwl" level="trace" includeLocation="false" additivity="false">
                  <AppenderRef ref="Console"/>
              </AsyncLogger>
      

    如上配置: com.wzwl 日志是异步的,root日志是同步的。

    使用异步日志需要注意的问题

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

Log4j2的性能

Log4j2最牛的地方在于异步输出日志时的性能表现,Log4j2在多线程的环境下吞吐量与Log4j和Logback的比较如下图。下图比较中Log4j2有三种模式:1)全局使用异步模式;2)部分Logger采用异步模式;3)异步Appender。可以看出在前两种模式下,Log4j2的性能较之Log4j和Logback有很大的优势。

无垃圾记录

垃圾收集暂停是延迟峰值的常见原因,并且对于许多系统而言,花费大量精力来控制这些暂停。许多日志库(包括以前版本的Log4j)在稳态日志记录期间分配临时对象,如日志事件对象,字符串,字符数组,字节数组等。这会对垃圾收集器造成压力并增加GC暂停发生的频率。

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

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

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

image.png

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

image.png

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

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

SpringBoot中的日志使用

springboot框架在企业中的使用越来越普遍,springboot日志也是开发中常用的日志系统。springboot默认就是使用SLF4J作为日志门面,logback作为日志实现来记录日志。

SpringBoot中的日志设计

springboot中的日志

<dependency>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>

依赖关系图:

image.png

总结:

  1. springboot 底层默认使用logback作为日志实现。
  2. 使用了SLF4J作为日志门面
  3. JUL也转换成slf4j
  4. 也可以使用log4j2作为日志门面,但是最终也是通过slf4j调用logback

SpringBoot日志使用

  1. 在springboot中测试打印日志

    @SpringBootTest
    class SpirngbootLogApplicationTests {
    ​
        //记录器
        public static final Logger LOGGER = LoggerFactory.getLogger(SpirngbootLogApplicationTests.class);
    ​
        @Test
        void contextLoads() {
            // 打印日志信息
            LOGGER.error("error");
            LOGGER.warn("warn");
            LOGGER.info("info"); // 默认日志级别
            LOGGER.debug("debug");
            LOGGER.trace("trace");
    ​
            //使用4j2使用桥接器转换为slf4j门面和logback日志实现
            org.apache.logging.log4j.Logger logger = LogManager.getLogger(SpirngbootLogApplicationTests.class);
            logger.info("log4j2 info");
        }
    ​
    }
    
  2. 修改默认日志配置

logging.level.com.example=trace
# 在控制台输出的日志的格式 同logback
logging.pattern.console=%d{yyyy-MM-dd} [%thread] [%-5level] %logger{50} - %msg%n
# 指定文件中日志输出的格式
logging.file.path=logs
logging.pattern.file=%d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n
  1. 指定配置

给类路径下放上每个日志框架自己的配置文件;SpringBoot就不使用默认配置的了

image.png

logback.xml:直接就被日志框架识别了

  1. 使用SpringBoot解析日志配置

logback-spring.xml:由SpringBoot解析日志配置

<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--<pattern>${pattern}</pattern>-->
            <springProfile name="dev">
                <pattern>${pattern}</pattern>
            </springProfile>
            <springProfile name="pro">
                <pattern>%d{yyyyMMdd:HH:mm:ss.SSS} [%thread] %-5level ||| %msg%n</pattern>
            </springProfile>
        </encoder>

application.properties

spring.profiles.active=pro
  1. 将日志切换为log4j2
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <!--排除logback-->
                <exclusion>
                    <artifactId>spring-boot-starter-logging</artifactId>
                    <groupId>org.springframework.boot</groupId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- 添加log4j2 startar启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>

log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="debug" monitorInterval="5">
    <Appenders>
        <!--控制台输出的appender-->
        <Console name="Console" target="SYSTEM_ERR">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L =-= %m%n"/>
        </Console>
    </Appenders>

    <Loggers>
        <Logger level="debug" name="com" additivity="false">
            <AppenderRef ref="Console"/>
        </Logger>
    </Loggers>
</Configuration>