Java 日志框架


title: Java 日志框架
date: 2021/02/05 12:28


一、Java 日志框架简介

1.1 日志框架可以为我们做什么?

  1. 控制日志输出的内容和格式
  2. 控制日志输出的位置
  3. 日志优化:异步日志、日志文件的归档和压缩

1.2 现有日志框架

日志门面:JCL(Jakarta Commons Logging,已废弃)、slf4j(Simple Logging Facade for Java)、log4j2

日志实现:log4j、JUL(java util logging)、logback、log4j2

二、Java 原生日志框架

2.1 架构(其他日志框架基本上也是这个架构)

image

Logger:记录器,应用程序通过 Logger 对象调用它的 api 方法来发布日志信息。Logger 对象通常是应用程序访问日志系统的入口。

Appender:也被称为 Handlers,每个 Logger 会关联一组 Handlers,Logger 会将日志交给关联的 Handlers 处理,由 Handlers 负责对日志进行记录。Handler 是一个抽象,它的具体实现决定了日志记录的位置(例如:控制台、文件、网络上的其他日志服务等)。

Layout:也被称为 Formatters,他负责对日志进行转换和格式化,Layouts决定了 数据在一条日志记录中的最终形式。

Level:每条日志消息都有一个关联的日志级别。该级别粗略的描述了该日志的重要性。

Filter:过滤器,根据需要来定制哪些信息会被记录,哪些信息会被舍弃。

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

2.2 日志级别

JUL 中定义了 7 种日志级别:

  • SEVERE(最高值)
  • WARNING
  • INFO (默认级别)
  • CONFIG
  • FINE
  • FINER
  • FINEST(最低值)

其中还有两个特殊的级别:

  • OFF,可用来关闭日志记录。
  • ALL,启用所有消息的日志记录。

2.3 快速入门

@Test
public void testQuick() throws Exception {

    // 获取日志记录器对象
    Logger logger = Logger.getLogger("cn.x5456.JULTest");

    // 日志记录输出
    logger.info("hello jul");
    // 通用方法进行日志记录
    logger.log(Level.INFO, "info msg");
    // 通过占位符 方式输出变量值
    logger.log(Level.INFO, "用户信息:{0},{1}", new Object[]{"x5456", 18});
}

2.4 硬编码方式修改日志级别

@Test
public void testLogConfig() {
    // 获取日志记录器对象
    Logger logger = Logger.getLogger("cn.x5456.JULTest");
    // 关闭系统默认配置,如果不关闭 info 级别以下的会记录两遍,因为当前 Logger 的 Handler 记录完之后又交给了父 Logger(RootLogger)的 Handler 记录了一下
    logger.setUseParentHandlers(false);

    // 自定义配置日志级别
    // 创建 ConsoleHandler 控制台输出
    ConsoleHandler consoleHandler = new ConsoleHandler();
    // 创建简单格式转换对象
    SimpleFormatter simpleFormatter = new SimpleFormatter();
    // 关联输出的格式
    consoleHandler.setFormatter(simpleFormatter);
    logger.addHandler(consoleHandler);

    // 配置日志具体级别
    logger.setLevel(Level.ALL);
    // 配置控制台输出的等级
    consoleHandler.setLevel(Level.ALL);

//    // 场景FileHandler  文件输出
//    FileHandler fileHandler = new FileHandler("/log/jul.log");
//    // 关联输出的格式
//    ileHandler.setFormatter(simpleFormatter);
//    logger.addHandler(fileHandler);

    // 2.日志记录输出
    logger.severe("severe");
    logger.warning("warning");
    logger.info("info"); // 默认日志输出级别
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
}

2.5 Logger 的父子关系

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

@Test
public void testLogParent() {
    Logger logger1 = Logger.getLogger("cn.x5456");
    Logger logger2 = Logger.getLogger("cn");

    // true
    System.out.println(logger1.getParent() == logger2);
    // 所有日志记录器的顶级父元素 LogManager$RootLogger
    System.out.println("logger2 Parent:" + logger2.getParent());
}

这样做的目的是,我们可以指定某些包路径下的类打印日志的级别、格式等信息。

2.6 使用配置文件

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

配置文件:

# RootLogger 顶级父元素指定的默认处理器为:ConsoleHandler
handlers= java.util.logging.FileHandler,java.util.logging.ConsoleHandler
# RootLogger 顶级父元素默认的日志级别为:ALL
.level= ALL

# 自定义 Logger 使用哪些处理器
cn.x5456.handlers = java.util.logging.FileHandler,java.util.logging.ConsoleHandler
cn.x5456.level = CONFIG
# 关闭默认配置
cn.x5456.useParentHandlers = false

# 向日志文件输出的 handler 对象
# 指定日志文件路径 /logs/java0.log
java.util.logging.FileHandler.pattern = /Users/x5456/logs/java%u.log
# 指定日志文件内容大小
java.util.logging.FileHandler.limit = 50000
# 指定日志文件数量
java.util.logging.FileHandler.count = 1
# 指定 handler 对象日志消息格式对象
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# 指定以追加方式添加日志内容
java.util.logging.FileHandler.append = true


# 向控制台输出的 handler 对象
# 指定 handler 对象的日志级别
java.util.logging.ConsoleHandler.level = ALL
# 指定 handler 对象的日志消息格式对象
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# 指定 handler 对象的字符集
java.util.logging.ConsoleHandler.encoding = UTF-8
# 指定日志消息格式
java.util.logging.SimpleFormatter.format = %4$s: %5$s [%1$tc]%n

测试代码:

@Test
public void testLogProperties() throws IOException {

    // 读取配置文件,通过类加载器
    InputStream ins = JULTest.class.getClassLoader().getResourceAsStream("logging.properties");
    // 创建LogManager
    LogManager logManager = LogManager.getLogManager();
    // 通过LogManager加载配置文件
    logManager.readConfiguration(ins);

    // 获取到的日志记录器的父记录器是我们自定义的日志记录器
    Logger logger = Logger.getLogger("cn.x5456.JULTest");

    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");

    // 获取到的日志记录器的父记录器是 RootLogger
    Logger logger2 = Logger.getLogger("test");

    logger2.severe("severe test");
    logger2.warning("warning test");
    logger2.info("info test");
    logger2.config("config test");
    logger2.fine("fine test");
    logger2.finer("finer test");
    logger2.finest("finest test");
}

2.7 JUL 原理

  1. 初始化LogManager
    • LogManager加载logging.properties配置
    • 添加Logger到LogManager
  2. 从单例LogManager获取Logger
  3. 设置级别Level,并指定日志记录LogRecord
  4. Filter提供了日志级别之外更细粒度的控制
  5. Handler是用来处理日志输出位置
  6. Formatter是用来格式化LogRecord的
image

三、Log4j

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

使用 Log4j 记录日志只需要引入下面的依赖即可:

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

3.1 快速入门

@Test
public void test() {

    /*
    直接运行下面的代码会提示如下警告。

    log4j:WARN No appenders could be found for logger (cn.x5456.Log4JTest). 没有为 logger 配置 appenders
    log4j:WARN Please initialize the log4j system properly. 请配置 log4j.properties 以初始化
    log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
    
    也可以直接使用 BasicConfigurator.configure() 方法向 RootLogger 中新增一个默认的 appender,无需 properties 配置文件
    */
    // BasicConfigurator.configure();


    // 获取日志记录器对象
    Logger logger = Logger.getLogger(Log4JTest.class);
    // 日志记录输出
    logger.fatal("fatal"); // 严重错误,一般会造成系统崩溃并终止运行

    logger.error("error"); // 错误信息,不会影响系统运行
    logger.warn("warn");   // 警告信息,可能会发生问题
    logger.info("info");   // 运行信息,数据连接、网络连接、IO 操作等等
    logger.debug("debug"); // 调试信息,一般在开发中使用,记录程序变量参数传递信息等等

    logger.trace("trace"); // 追踪信息,记录程序所有的流程信息
}

注:还有两个特殊的级别: OFF,可用来关闭日志记录。ALL,启用所有消息的日志记录。

3.2 Log4j 的组件

Log4j 主要由 Loggers (日志记录器)、Appenders(输出端)和 Layout(日志格式化器)组成。

  • Loggers 控制日志的输出级别与日志是否输出
  • Appenders 指定日志的输出方式(输出到控制台、文件 等)
  • Layout 控制日志信息的输出格式。

3.2.1 Loggers

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

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

下图的 Category 是 Log4j 早期的命名,1.2 版本之后已经改成了 Logger。

image

3.2.2 Appenders

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

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

3.2.3 Layouts

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

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

log4j 采用类似 C 语言的 printf 函数的打印格式格式化日志信息,具体的占位符及其含义如下:

  • %m 输出代码中指定的日志信息
  • %p 输出优先级,及 DEBUG、INFO 等
  • %n 换行符(Windows平台的换行符为 "\r\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字符,就从左边交远销出的字符截掉

3.3 测试 Appender

@Test
public void testQuick() {

    // 开启 log4j 内置日志记录
    LogLog.setInternalDebugging(true);

    // 获取日志记录器对象
    Logger logger = Logger.getLogger(Log4jTest.class);

    //for (int i = 0; i < 10000; i++) {

    // 日志级别
    logger.fatal("fatal"); // 严重错误,一般会造成系统崩溃并终止运行

    logger.error("error"); // 错误信息,不会影响系统运行
    logger.warn("warn");   // 警告信息,可能会发生问题
    logger.info("info");   // 运行信息,数据连接、网络连接、IO 操作等等
    logger.debug("debug"); // 调试信息,一般在开发中使用,记录程序变量参数传递信息等等

    logger.trace("trace"); // 追踪信息,记录程序所有的流程信息
    //}
}

配置文件:

# 指定 RootLogger 顶级父元素默认配置信息
# 指定日志级别=trace,使用的 apeender 为=console
log4j.rootLogger = trace,console,dailyFile

# 指定控制台日志输出的 appender
log4j.appender.console = org.apache.log4j.ConsoleAppender
# 指定消息格式 layout
log4j.appender.console.layout = org.apache.log4j.PatternLayout
# 指定消息格式的内容
log4j.appender.console.layout.conversionPattern = [%-10p]%r  %l %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n

# 日志文件输出的 appender 对象
log4j.appender.file = org.apache.log4j.FileAppender
# 指定消息格式 layout
log4j.appender.file.layout = org.apache.log4j.PatternLayout
# 指定消息格式的内容
log4j.appender.file.layout.conversionPattern = [%-10p]%r  %l %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n
# 指定日志文件保存路径
log4j.appender.file.file = /Users/x5456/logs/log4j.log
# 指定日志文件的字符集
log4j.appender.file.encoding = UTF-8

# 按照文件大小拆分的 appender 对象
# 日志文件输出的 appender 对象
log4j.appender.rollingFile = org.apache.log4j.RollingFileAppender
# 指定消息格式 layout
log4j.appender.rollingFile.layout = org.apache.log4j.PatternLayout
# 指定消息格式的内容
log4j.appender.rollingFile.layout.conversionPattern = [%-10p]%r  %l %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n
# 指定日志文件保存路径
log4j.appender.rollingFile.file = /Users/x5456/logs/log4j.log
# 指定日志文件的字符集
log4j.appender.rollingFile.encoding = UTF-8
# 指定日志文件内容的大小
log4j.appender.rollingFile.maxFileSize = 1MB
# 指定日志文件的数量
log4j.appender.rollingFile.maxBackupIndex = 10


# 按照时间规则拆分的 appender 对象
log4j.appender.dailyFile = org.apache.log4j.DailyRollingFileAppender
# 指定消息格式 layout
log4j.appender.dailyFile.layout = org.apache.log4j.PatternLayout
# 指定消息格式的内容
log4j.appender.dailyFile.layout.conversionPattern = [%-10p]%r  %l %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n
# 指定日志文件保存路径
log4j.appender.dailyFile.file = /Users/x5456/logs/log4j.log
# 指定日志文件的字符集
log4j.appender.dailyFile.encoding = UTF-8
# 指定日期拆分规则
log4j.appender.dailyFile.datePattern = '.'yyyy-MM-dd-HH-mm-ss

3.4 自定义 Logger

@Test
public void testQuick() {

    // 开启 log4j 内置日志记录
    LogLog.setInternalDebugging(true);

    // 获取日志记录器对象
    Logger logger = Logger.getLogger(Log4jTest.class);

    // 日志级别
    logger.fatal("fatal"); // 严重错误,一般会造成系统崩溃并终止运行

    logger.error("error"); // 错误信息,不会影响系统运行
    logger.warn("warn");   // 警告信息,可能会发生问题
    logger.info("info");   // 运行信息,数据连接、网络连接、IO 操作等等
    logger.debug("debug"); // 调试信息,一般在开发中使用,记录程序变量参数传递信息等等

    logger.trace("trace"); // 追踪信息,记录程序所有的流程信息

    // 再创建一个日志记录器对象
    Logger logger1 = Logger.getLogger(Logger.class);
    logger1.fatal("fatal logger1"); // 严重错误,一般会造成系统崩溃并终止运行
    logger1.error("error logger1"); // 错误信息,不会影响系统运行
    logger1.warn("warn logger1");   // 警告信息,可能会发生问题
    logger1.info("info logger1");   // 运行信息,数据连接、网络连接、IO 操作等等
    logger1.debug("debug logger1"); // 调试信息,一般在开发中使用,记录程序变量参数传递信息等等
    logger1.trace("trace logger1"); // 追踪信息,记录程序所有的流程信息
}

配置文件:

# 指定 RootLogger 顶级父元素默认配置信息
# 指定日志级别=trace,使用的 apeender 为=console
log4j.rootLogger = trace,console

# 此时会同时向控制台和文件中输出 info 级别的日志,因为继承了 rootLogger 的 Appender
log4j.logger.cn.x5456=info,file
log4j.logger.org.apache = error

# 指定控制台日志输出的 appender
log4j.appender.console = org.apache.log4j.ConsoleAppender
# 指定消息格式 layout
log4j.appender.console.layout = org.apache.log4j.PatternLayout
# 指定消息格式的内容
log4j.appender.console.layout.conversionPattern = [%-10p]%r  %l %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n

# 日志文件输出的 appender 对象
log4j.appender.file = org.apache.log4j.FileAppender
# 指定消息格式 layout
log4j.appender.file.layout = org.apache.log4j.PatternLayout
# 指定消息格式的内容
log4j.appender.file.layout.conversionPattern = [%-10p]%r  %l %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n
# 指定日志文件保存路径
log4j.appender.file.file = /Users/x5456/logs/log4j.log
# 指定日志文件的字符集
log4j.appender.file.encoding = UTF-8

四、JCL(Jakarta Commons Logging)

Jakarta Commons Logging 是 Apache 提供的一个通用日志API。

它是为“所有的Java日志实现”提供一个统一的接口,它自身也提供一个日志的实现,但是功能非常弱 (SimpleLog),所以一般不会单独使用它。

他允许开发人员使用不同的具体日志实现工具: Log4j, Jdk 自带的日志(JUL)

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

image

JCL 依赖:

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>

4.1 为什么要使用日志门面

当我们的系统变的更加复杂的时候,我们的日志就容易发生混乱。随着系统开发的进行,可能会更新不同的日志框架,造成当前系统中存在不同的日志依赖,让我们难以统一的管理和控制。就算我们强制要求所有的模块使用相同的日志框架,系统中也难以避免使用其他类似 spring,mybatis 等其他的第三方框架,它们依赖于我们规定不同的日志框架,而且他们自身的日志系统就有着不一致性,依然会出来日志体系的混乱。

所以我们需要借鉴 JDBC 的思想,为日志系统也提供一套门面,那么我们就可以面向这些接口规范来开发,避免了直接依赖具体的日志框架。这样我们的系统在日志中,就存在了日志的门面和日志的实现。

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

4.2 使用

@Test
public void testQuick()throws Exception{
    // 获取 log日志记录器对象
    Log log = LogFactory.getLog(JCLTest.class);
    // 日志记录输出
    log.info("hello jcl");
}

4.3 原理

image

五、SLF4J

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

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

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

使用 SLF4J 的好处:

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

5.1 绑定日志的实现

使用slf4j的日志绑定流程:

  1. 添加slf4j-api的依赖
  2. 使用slf4j的API在项目中进行统一的日志记录
  3. 绑定具体的日志实现框架
    • 绑定已经实现了slf4j的日志框架,直接添加对应依赖
    • 绑定没有实现slf4j的日志框架,先添加日志的适配器,再添加实现类的依赖
  4. slf4j有且仅有一个日志实现框架的绑定(如果出现多个默认使用第一个依赖日志实现)

代码:

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Slf4jTest {

    public static final Logger LOGGER = LoggerFactory.getLogger(Slf4jTest.class);

    // 快速入门
    @Test
    public void test01()throws Exception{
        // 日志输出
        LOGGER.error("error");
        LOGGER.warn("wring");
        LOGGER.info("info"); // 默认级别
        LOGGER.debug("debug");
        LOGGER.trace("trace");

        // 使用占位符输出日志信息
        String name = "itheima";
        Integer age = 14;
        LOGGER.info("用户:{},{}",name,age);

        // 将系统的异常信息输出
        try {
            int i = 1/0;
        } catch (Exception e) {
           // e.printStackTrace();
            LOGGER.error("出现异常:",e);
        }
    }
}

Maven 依赖:

<!-- slf4j 日志门面 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.26</version>
</dependency>

<!-- slf4j 内置的简单实现
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.21</version>
</dependency>
-->

<!--logback 日志实现-->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

<!--nop 日志开关
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-nop</artifactId>
    <version>1.7.25</version>
</dependency>
-->
<!--绑定 log4j 日志实现,需要导入适配器
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.12</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.25</version>
</dependency>
-->

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

image

5.2 绑定原理

image
image
image
image

5.3 日志桥接

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

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

使用步骤:

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

不常用,不想讲了。其实所谓的桥接器就是把之前的日志框架的一些类全部重写了,改为调用 slf4j 了而已(JUL 除外)。

六、Logback

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

Logback主要分为三个模块:

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

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

6.1 快速开始

maven 依赖:

<!--slf4j 日志门面-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.26</version>
</dependency>
<!--logback 日志实现-->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

代码:

public static final Logger LOGGER = LoggerFactory.getLogger(LogbackTest.class);

@Test
public void testQuick() {
    // 日志输出
    LOGGER.error("error");
    LOGGER.warn("wring");
    LOGGER.info("info");
    LOGGER.debug("debug");// 默认级别
    LOGGER.trace("trace");
}

6.2 logback.xml

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

    <!--
        配置集中管理属性
        我们可以直接改属性的 value 值
        格式:${name}
    -->
    <property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %c %M %L [%thread] %m%n" />
    <!--
    日志输出格式:
        %-5level
        %d{yyyy-MM-dd HH:mm:ss.SSS}日期
        %c类的完整名称
        %M为method
        %L为行号
        %thread线程名称
        %m或者%msg为信息
        %n换行
      -->
    <!--定义日志文件保存路径属性-->
    <property name="log_dir" value="/Users/x5456/logs" />

    <!--控制台日志输出的 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="file" class="ch.qos.logback.core.FileAppender">
        <!--日志文件保存路径-->
        <file>${log_dir}/logback.log</file>
        <!--日志消息格式配置-->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
    </appender>

    <!--html 格式日志文件输出 appender-->
    <appender name="htmlFile" class="ch.qos.logback.core.FileAppender">
        <!--日志文件保存路径-->
        <file>${log_dir}/logback.html</file>
        <!--html 消息格式配置-->
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <layout class="ch.qos.logback.classic.html.HTMLLayout">
                <pattern>%-5level%d{yyyy-MM-dd HH:mm:ss.SSS}%c%M%L%thread%m</pattern>
            </layout>
        </encoder>
    </appender>


    <!--日志拆分和归档压缩的 appender 对象-->
    <appender name="rollFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--日志文件保存路径-->
        <file>${log_dir}/roll_logback.log</file>
        <!--日志消息格式配置-->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
        <!--指定拆分规则-->
        <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 class="ch.qos.logback.classic.filter.LevelFilter">
            <!--日志过滤规则,只保存 ERROR 级别的错误-->
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!--异步日志-->
    <appender name="async" class="ch.qos.logback.classic.AsyncAppender">
        <!--指定某个具体的 appender-->
        <appender-ref ref="rollFile"/>
    </appender>


    <!--root logger 配置-->
    <root level="ALL">
        <appender-ref ref="console"/>
        <appender-ref ref="async"/>
        <appender-ref ref="htmlFile"/>
    </root>

    <!--自定义 looger 对象
        additivity="false" 自定义 logger 对象是否继承 rootLogger
     -->
    <logger name="com.itheima" level="info" additivity="false">
        <appender-ref ref="console"/>
    </logger>
</configuration>

七、Log4j2

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

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

7.1 快速入门

使用 log4j2 自己的门面

依赖:

<!--log4j2日志门面-->
<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>

代码:

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

public class Log4j2Test {

    // 定义日志记录器对象
    public static final Logger LOGGER = LogManager.getLogger(Log4j2Test.class);

    // 快速入门
    @Test
    public void testQuick()throws Exception{
        // 日志消息输出
        LOGGER.fatal("fatal");
        LOGGER.error("error");
        LOGGER.warn("warn");
        LOGGER.info("inf");
        LOGGER.debug("debug");
        LOGGER.trace("trace");
    }
}

使用 slf4j 做门面

依赖:

<!--使用slf4j 作为日志门面-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.26</version>
</dependency>
<!--使用 log4j2 的适配器进行绑定-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.9.1</version>
</dependency>


<!--log4j2日志门面-->
<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>

代码:

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Slf4jTest {

    public static final Logger LOGGER = LoggerFactory.getLogger(Slf4jTest.class);

    // 快速入门
    @Test
    public void test01()throws Exception{
        // 日志输出
        LOGGER.error("error");
        LOGGER.warn("wring");
        LOGGER.info("info");
        LOGGER.debug("debug");
        LOGGER.trace("trace");
    }
}

7.2 log4j2.xml

<?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_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>

        <!--<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="/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/>
                <!--按照文件大小拆分,10MB -->
                <SizeBasedTriggeringPolicy size="10 MB"/>
                <!--按照时间节点拆分,规则根据filePattern定义的-->
                <TimeBasedTriggeringPolicy/>
            </Policies>
            <!--在同一个目录下,文件的个数限定为 30 个,超过进行覆盖-->
            <DefaultRolloverStrategy max="30"/>
        </RollingFile>

    </Appenders>

    <!--logger 定义-->
    <Loggers>
        <!--自定义异步 logger 对象
            includeLocation="false" 关闭日志记录的行号信息
            additivity="false" 不在继承 rootlogger 对象
        -->
        <AsyncLogger name="com.itheima" level="trace" includeLocation="false" additivity="false">
            <AppenderRef ref="Console"/>
        </AsyncLogger>


        <!--使用 rootLogger 配置 日志级别 level="trace"-->
        <Root level="trace">
            <!--指定日志使用的处理器-->
            <AppenderRef ref="Console"/>

            <!--使用异步 appender-->
            <AppenderRef ref="Async"/>
        </Root>
    </Loggers>
</Configuration>

7.3 异步日志

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

异步日志需要下面这个依赖:

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

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

AsyncAppender 方式

<Appenders>
    <!--控制台输出 appender-->
    <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>

    <Async name="Async">
        <AppenderRef ref="file"/>
    </Async>

AsyncLogger 方式

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

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

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

混合异步就是,你可以在应用中同时使用同步日志和异步日志,这使得日志的配置方式更加灵活。

<!--logger 定义-->
<Loggers>
    <!--自定义异步 logger 对象
        includeLocation="false" 关闭日志记录的行号信息
        additivity="false" 不在继承 rootlogger 对象
    -->
    <AsyncLogger name="com.itheima" level="trace" includeLocation="false" additivity="false">
        <AppenderRef ref="Console"/>
    </AsyncLogger>


    <!--使用 rootLogger 配置 日志级别 level="trace"-->
    <Root level="trace">
        <!--指定日志使用的处理器-->
        <AppenderRef ref="Console"/>

        <!--使用异步 appender-->
        <AppenderRef ref="Async"/>
    </Root>
</Loggers>

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

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

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

八、SpringBoot 中日志的使用

8.1 SpringBoot 中日志设计

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

依赖关系图:

image

总结:

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

8.2 使用

springboot 支持 logback.xml 和 logback-spring.xml,两者的区别是,后者可以被 spring 解析 springProfile、springProperties 等属性。

在 application.properties 中也可以简单的对日志进行配置:

# 指定自定义 logger 对象日志级别
logging.level.com.itheima=trace

# 指定控制台输出消息格式
logging.pattern.console=[%-5level] %d{yyyy-MM-dd HH:mm:ss} %c [%thread]===== %msg %n

# 指定存放日志文件的具体路径
# logging.file=/logs/springboot.log
# 指定日志文件存放的目录,默认的文件名 spring.log
logging.file.path=/logs/springboot/
# 指定日志文件消息格式
logging.pattern.file=[%-5level] %d{yyyy-MM-dd HH:mm:ss} %c [%thread]===== %msg %n

8.3 切换日志实现

<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 的日志启动器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

此时 SpringBoot 支持解析 log4j2.xml 和 log4j2-spring.xml。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容