01-Java-日志框架

1 日志技术概述

1.1 什么是日志技术

​ 日志技术是一种记录和存储应用程序运行时信息的技术。它可以捕获应用程序的状态、事件、错误和警告等信息,并将其保存到日志文件或其他存储介质中。日志技术可以帮助开发人员和运维团队了解应用程序的运行情况,进行故障排查、性能优化、安全监控和运营分析等工作。

以下是日志技术的一些关键概念和组成部分:

  • 日志级别:日志级别用于标识日志的重要性和严重程度。常见的日志级别包括DEBUG、INFO、WARN、ERROR和FATAL等。开发人员可以根据需要选择适当的日志级别来记录不同类型的信息。
  • 日志格式:日志格式定义了日志记录的结构和内容。它通常包括时间戳、日志级别、日志消息和其他相关信息。开发人员可以根据需求自定义日志格式,以便更好地理解和分析日志。
  • 日志输出:日志输出指定了日志记录的目标位置。常见的日志输出包括控制台输出、文件输出和远程日志服务器等。开发人员可以根据需要配置多个日志输出,以便在不同环境下进行日志记录和分析。
  • 日志框架:日志框架是一种用于记录和管理日志的软件库或工具。它提供了一组API和配置选项,使开发人员能够方便地生成和处理日志。常见的日志框架包括Log4j、Logback和java.util.logging等。
  • 日志分析:日志分析是指对大量日志数据进行处理和分析,以提取有用的信息和洞察。日志分析工具可以帮助开发人员和运维团队发现潜在的问题、优化性能、监控安全和了解用户行为等。

1.2 我们接触过的日志

我们之前都是使用输出语句(System.out.println)打印日志的,有什么问题呢?

  • 日志打印在控制台,当我们关闭控制台以后日志就不见了
  • 不能方便的将日志记录到其他的位置(如文件、数据库等)
  • 想要取消或修改日志,需要修改源代码

1.3 引入日志技术的作用

引入日志技术是为了更好地管理和跟踪应用程序的运行情况。以下是一些使用日志技术的好处:

  • 故障排查和调试:当应用程序出现问题时,日志可以提供有关错误和异常的详细信息,帮助开发人员快速定位和解决问题。通过查看日志,您可以了解应用程序在特定时间点的状态、执行路径和输入输出数据。
  • 监控和性能优化:日志可以记录应用程序的性能指标、资源使用情况和系统状态。通过分析日志,您可以识别性能瓶颈、优化代码和资源分配,以提高应用程序的性能和可伸缩性。
  • 安全和合规性:日志可以记录应用程序的安全事件、用户操作和敏感数据访问。这些日志可以用于监测潜在的安全威胁、追踪用户活动和满足合规性要求。
  • 运营和维护:日志可以提供应用程序的运行情况、错误和警告信息。通过监视日志,您可以及时发现并解决潜在的问题,确保应用程序的稳定运行。
  • 分析和洞察:通过对日志进行分析,您可以了解应用程序的使用情况、用户行为和业务趋势。这些洞察可以帮助您做出更好的业务决策、改进产品功能和提供更好的用户体验。

​ 引入日志技术可以帮助开发人员和运维团队更好地理解和管理应用程序的运行情况,提高开发效率、减少故障修复时间,并提供更好的用户体验。

1.4 日志技术的体系

日志技术的体系结构:

  • 日志门面(接口、规则):JCL(Jakarta Commons Logging 由Apache Jakarta提供)、SLF4J(Simple Logging Facade for Java)、Log4j2 API
  • 日志实现(框架):JUL(java util logging)、Logback、Log4j、Log4j2-core

日志门面是设计日志框架的一套标准,所有的日志框架都需要实现这些接口

Log4j实现的是JCL

Logbook实现的是SLF4j

Log4J2-core实现的是Log4j2 API

说明:

​ 因为对JCL接口不满意,所以有人就搞出来了SLF4J;因为对Log4j的性能不满意,所以就出现了Logback

Logback是基于SLF4J的日志规范实现的日志框架

日志框架出现的历史顺序

Log4j --> JUL --> JCL -> Slf4j --> Loback --> Log4j2

2 JUL

​ JUL(Java Util Logging)是Java平台自带(原生)的日志框架。它是Java标准库的一部分,不需要额外的依赖。JUL提供了一套简单的日志记录API,可以用于在应用程序中记录日志信息。

2.1 JUL主要组件

JUL的主要组件包括:

  • Logger(日志记录器):用于记录日志信息的主要组件。可以通过Logger.getLogger()方法获取Logger实例。Logger通常是应用程序访问日志系统的入口程序。
  • Handler(处理器):也称为Appenders,用于处理日志记录的输出。每个Loigger都会关联一组Handlers,Logger会将日志交给关联的Handlers进行处理,由Handler负责将日志做记录。可以将日志记录发送到不同的目标,如控制台、文件、数据库等。(Handler是一个抽象,具体需要将日志发送到哪里还需要具体的实现)
  • Formatter(格式化器):也称为Layout用于格式化日志记录的输出。可以定义日志记录的格式,包括日期、时间、日志级别、类名等信息。
  • Level(日志级别):用于控制日志记录的级别。可以设置不同的日志级别,如INFO、DEBUG、WARNING、ERROR等,只有达到指定级别的日志记录才会被记录。
  • Filters(过滤器):根据需要定制那些消息会被记录,那些消息会被放过

​ JUL的配置文件是一个文本文件,通常命名为logging.properties。可以通过修改配置文件来配置日志记录的级别、输出目标、格式等。

2.2 JUL的使用

1)获取Logger实例:使用 Logger.getLogger() 方法获取Logger实例,传入一个唯一的名称作为参数。通常使用类的全限定名作为名称。

public static final Logger LOGGER = Logger.getLogger(类名);

2)配置日志级别:可以通过修改配置文件 logging.properties来配置日志级别。在配置文件中,可以为不同的包或类设置不同的日志级别。

配置文件可以放置在以下位置之一:

  • 在应用程序的根目录下。
  • src/main/resources目录下(对于Maven或Gradle项目)。
  • WEB-INF/classes目录下(对于Web应用程序)

logging.properties

# 设置根日志级别为INFO
.level=INFO

# 设置com.clear包的日志级别为DEBUG
com.clear.level=DEBUG

3)使用Logger实例的不同方法记录不同级别的日志信息。

演示:

import java.util.logging.Logger;

public class JULTest {
    // 获取Logger实例
    public static final Logger LOGGER = Logger.getLogger("com.clear.JULTest");
    @Test
    public void test1(){
        LOGGER.severe("server");  // 严重
        LOGGER.warning("warning");  // 警告
        LOGGER.info("info");  // 信息
    }
}

2.3 日志级别

记录日志信息:使用Logger实例的不同方法记录不同级别的日志信息。

常用的日志级别包括:

  • SEVERE:最高级别的日志,表示严重错误。
  • WARNING:警告级别的日志,表示潜在的问题。
  • INFO:信息级别的日志,表示一般的运行信息。(默认)
  • CONFIG:配置级别的日志,表示配置信息。
  • FINE:细粒度的调试信息。
  • FINER:更细粒度的调试信息。
  • FINEST:最细粒度的调试信息。
LOGGER.severe("This is a severe message");
LOGGER.warning("This is a warning message");
LOGGER.info("This is an info message");
LOGGER.config("This is a config message");
LOGGER.fine("This is a fine message");
LOGGER.finer("This is a finer message");
LOGGER.finest("This is a finest message");

此外,还有两个特殊的级别:

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

2.4 日志配置

// 获取Logger实例
public static final Logger LOGGER = Logger.getLogger("com.clear.JULTest");

@Test
public void test2() throws IOException {
    // 禁用父级处理器。日志消息将不会传递给父级处理器进行处理。
    LOGGER.setUseParentHandlers(false);
    // 创建了一ConsoleHandler实例
    ConsoleHandler consoleHandler = new ConsoleHandler();
    // 设置其日志格式为SimpleFormatter。ConsoleHandler用于将日志消息输出到控制台。
    consoleHandler.setFormatter(new SimpleFormatter());
    // 设置日志级别,所有级别的日志消息都将被记录。
    consoleHandler.setLevel(Level.ALL);

    // 创建一个FileHandler实例
    // 为了简单,我们将异常抛出去
    FileHandler fileHandler = new FileHandler("D:/test/jul.log",true);
    fileHandler.setFormatter(new SimpleFormatter());
    fileHandler.setLevel(Level.WARNING);

    LOGGER.setLevel(Level.ALL);
    // 将ConsoleHandler添加到LOGGER的处理器列表中,以便将日志消息发送到控制台。
    LOGGER.addHandler(consoleHandler);
    // 将FileHandler添加到LOGGER的处理器列表中,以便将日志消息发送到文件中
    LOGGER.addHandler(fileHandler);
    // 日志输出
    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(Java Util Logging)中,日志记录器(Logger)之间可以建立父子关系。这种父子关系是通过日志记录器的名称来确定的(即建立树状结构来存储父子关系)。

父子关系的建立是通过日志记录器的名称来实现的。当创建一个新的日志记录器时,可以指定其父级日志记录器的名称。如果没有指定父级日志记录器的名称,则默认为根日志记录器(RootLogger)

​ 默认情况下,子Logger会继承父Logger的属性。

  • 任何一个logger都是单例的,名字相同的只有一个
// 获取Logger实例
public static final Logger LOGGER = Logger.getLogger("com.clear.JULTest");

@Test
public void test3() {
    // 任何一个logger都是单例的,名字相同的只有一个
    Logger logger = Logger.getLogger(JULTest.class.getName());
    System.out.println(LOGGER.hashCode());  // 4959864
    System.out.println(logger.hashCode());  // 4959864
    System.out.println(LOGGER == logger);  // true
}

所有的Logger实例都是由LoggerManager统一管理的

@Test
public void test4(){
    Logger logger = Logger.getLogger("com.clear.JULTest");
    // todo 如果没有指定父级日志记录器的名称,则默认为根日志记录器(RootLogger)
    System.out.println("默认父Logger:"+logger.getParent()); 
    // 默认父Logger:java.util.logging.LogManager$RootLogger@4bae78
    System.out.println("默认父Logger名字:"+logger.getParent().getName());  
    // 默认父Logger名字:

    // 父Logger
    // todo 父子关系的建立是通过日志记录器的名称来实现的
    Logger pLogger = Logger.getLogger("com.clear");
    System.out.println("建立父子关系以后:");
    System.out.println("父Logger:"+logger.getParent());  
    // 父Logger:java.util.logging.Logger@1764bce
    System.out.println("父Logger名字:"+logger.getParent().getName()); 
    // 父Logger名字:com.clear
    System.out.println("父Logger的父Logger:"+pLogger.getParent());  
    // 父Logger的父Logger:java.util.logging.LogManager$RootLogger@4bae78
    System.out.println("父Logger的父Logger名称:"+pLogger.getParent().getName());  
    // 父Logger的父Logger名称:
}
  • 默认情况下,子Logger会继承父Logger的属性。
// 默认情况下,子Logger会继承父Logger的属性。
@Test
public void test5(){
    Logger logger = Logger.getLogger("com.clear.JULTest");
    // 父Logger
    Logger pLogger = Logger.getLogger("com.clear");
    // 父Logger的默认日志打印级别为info
    pLogger.info("info");
    pLogger.fine("fine");
    logger.setLevel(Level.ALL);
    // todo 设置父类Logger
    // 禁用父级处理器
    pLogger.setUseParentHandlers(false);
    ConsoleHandler consoleHandler = new ConsoleHandler();
    consoleHandler.setFormatter(new SimpleFormatter());
    consoleHandler.setLevel(Level.ALL);
    pLogger.addHandler(consoleHandler);

    // 父Logger可以打印fine级别的信息了
    pLogger.info("info");
    pLogger.fine("fine");
    // todo 子Logger继承了父Logger的属性,也可以打印fine级别的信息了
    logger.info("info");
    logger.fine("fine");
}

2.6 日志格式化

​ 在JUL(Java Util Logging)中,可以使用格式化器(Formatter)来定义日志消息的输出格式。格式化器可以将日志消息的各个部分(如日期、级别、类名、方法名、消息内容等)组合成一个字符串,并指定其输出的格式。

JUL提供了两种内置的格式化器:

  • SimpleFormatter:这是默认的格式化器,它将日志消息格式化为一行文本。默认格式为[日期时间] [级别] [类名] [方法名] - [消息内容]
  • XMLFormatter:这是一个将日志消息格式化为XML格式的格式化器。它将日志消息的各个部分作为XML元素,并使用属性来表示其值。

可以通过以下方式来配置日志记录器的格式化器:

  1. 在代码中使用Handler.setFormatter(Formatter formatter)方法来设置格式化器。例如,handler.setFormatter(new SimpleFormatter())handler的格式化器设置为SimpleFormatter
  2. 在配置文件中使用java.util.logging.ConsoleHandler.formatter属性来设置控制台处理器的格式化器。例如,java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter表示将控制台处理器的格式化器设置为SimpleFormatter
  3. 在配置文件中使用java.util.logging.FileHandler.formatter属性来设置文件处理器的格式化器。例如,java.util.logging.FileHandler.formatter=java.util.logging.XMLFormatter表示将文件处理器的格式化器设置为XMLFormatter

除了使用内置的格式化器,还可以自定义格式化器来满足特定的需求。自定义格式化器需要实现java.util.logging.Formatter接口,并实现其中的format(LogRecord record)方法来定义日志消息的输出格式。

通过配置日志记录器的格式化器,可以灵活地控制日志消息的输出格式,以满足不同的需求和标准。

2.6.1 String的format方法

String类的format方法用于创建格式化的字符串以及连接多个字符串对象。

public static String format(String format, Object... args) {
    return new Formatter().format(format, args).toString();
}
    
public static String format(Locale l, String format, Object... args) {
    return new Formatter(l).format(format, args).toString();
}

在这个方法中,我们可以定义字符串模板,例如:

String str = String.format("hello %s","world");

2.6.2 常用的转换符

转换符 说明 举例
%s 字符串类型 “hello”
%c 字符类型 ‘a’
%b 布尔类型 true
%d 整数类型(十进制) 88
%x 整数类型(十六进制) FF
%o 整数类型(八进制) 77
%f 浮点类型 8.888
%a 十六进制浮点类型 FF.35AE
%e 指数类型 9.38e+5
%n 换行符
%tx 日期与时间类型(x代表不同的日期与时间转换符)

2.6.3 特殊符号

标志 说明 举例 结果
+ 为正数或负数添加符合,因为一般整数不会主动加符号 (“+%d”, 15) +15
0 数字前面补0,用于对齐 (“%04d”, 99) 0099
空格 在整数之前添加指定数量的空格 (“%4d”, 99) 99
, 以","对数字进行分组(常用于显示金额) (“%,f”, 9999.99) 9,999,990000
( 使用括号包含负数 (“%(f”, -99.99) (99.990000)

默认情况下,我们的可变参数就是按照顺序依次替换,但是如果我们想重复利用可变参数的情况:

我们可以采用在转换符中加 数字$,完成匹配,例如:

System.out.printf("%1$s %1$s %1$s","小明");
// 其中,1$就代表第一个参数,那么2$就代表第二个参数

2.6.4 日期处理

前面的 %tx 中,x就代表日期转换符

标志 说明 举例
c 包括全部日期和时间信息 周四 10月 21 14:30:30 GMT+08:00 2023
F "年-月-日"格式 2023-7-18
D "年/月/日"格式 2023/7/18
r "HH:MM:SS PM"格式(12时制) 8:20:30 下午
T "HH:MM:SS"格式(12时制) 20:20:30
R "HH:MM"格式(24时制) 20:20
b 月份本地化 7月
y 两位的年 23
Y 四位的年 2023
m 7
d 18
H 24小时制的时 20
l 12小时制的时 8
M 55
S 55
s 秒为单位的时间戳 23144232
p 上午还是下午 下午

我们可以使用以下三个类去进行格式化,其中可能存在不支持的情况,比如LocalDateTime不支持c:

System.out.println("tc", new Date());
System.out.println("tc", ZonedDateTime.now());
System.out.println("tF", LocalDateTime.now();

2.7 配置文件

JUL(Java Util Logging)加载配置文件的源码解读如下:

  • 首先,JUL使用LogManager类来管理日志记录器和配置信息。LogManager是一个单例类,通过LogManager.getLogManager()方法获取实例。

  • 当调用LogManager.getLogManager().readConfiguration(inputStream)方法时,会执行以下步骤:

    • a. LogManager类的readConfiguration方法会创建一个新的Properties对象,用于存储配置信息。
    • b. 使用Properties对象的load方法,将配置文件的内容加载到Properties对象中。这里的inputStream参数是通过getClass().getClassLoader().getResourceAsStream("logging.properties")获取的,它会从类路径中读取logging.properties文件的内容。
    • c. 加载配置文件后,LogManager会根据配置信息更新日志记录器的配置。具体的配置信息包括日志级别、日志输出目标等。
  • 如果没有指定配置文件或加载配置文件失败,JUL会使用默认的配置。默认的配置包括将日志输出到控制台,并使用默认的日志级别。

​ 总结:JUL加载配置文件的源码主要涉及LogManager类的readConfiguration方法,该方法通过读取配置文件的内容,并根据配置信息更新日志记录器的配置。

2.7.1 读取自定义配置文件

在JUL(Java Util Logging)中,加载配置文件的常规步骤如下:

1)创建一个名为logging.properties的文本文件,该文件包含了JUL的配置信息。

2)将logging.properties文件放置在类路径下,通常是放在src/main/resources目录下。

3)在代码中使用以下代码片段来加载配置文件:

InputStream inputStream = getClass().getClassLoader().getResourceAsStream("logging.properties");
if (inputStream != null) {
    LogManager.getLogManager().readConfiguration(inputStream);
}

演示:

1)在src/main/resources目录下创建 logging.properties 文件,内容如下:

# 设置根日志记录器的级别为FINE
.level=FINE

handlers=java.util.logging.ConsoleHandler,java.util.logging.FileHandler
# 配置控制台处理器
java.util.logging.ConsoleHandler.level=FINE
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
# 配置文件处理器
java.util.logging.FileHandler.level=FINE
# 指定日志文件的路径和名称模式
java.util.logging.FileHandler.pattern=%h/java%u.log
# 指定日志文件的大小限制
java.util.logging.FileHandler.limit=50000
# 指定日志文件的数量限制
java.util.logging.FileHandler.count=1
# 指定日志消息的格式化器
java.util.logging.FileHandler.formatter=java.util.logging.XMLFormatter

2)编写代码读取配置文件

// 读取配置文件
@Test
public void readConfig() {
    LogManager manager = LogManager.getLogManager();
    // 获取配置文件
    InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("logging.properties");
    //        InputStream inputStream = getClass().getClassLoader().getResourceAsStream("logging.properties");

    try {
        manager.readConfiguration(inputStream);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    Logger logger = Logger.getLogger(JULTest.class.getName());
    // 默认的日志打印级别为INFO,加载我们自己的配置文件后级别为FINE
    // 如果能打印fine级别的日志,证明加载成功
    logger.fine("fine");
}
}

2.7.2 配置文件解读

# 设置根日志记录器的级别为INFO
.level=INFO
# 配置控制台处理器、文件处理器
handlers=java.util.logging.ConsoleHandler,java.util.logging.FileHandler
# 为RootLogger根日志记录器配置handlers,指定处理器的列表,多个处理器之间使用逗号分隔
# 配置文件处理器的相关属性
# 设置日志输出级别
java.util.logging.FileHandler.level=INFO
# 指定日志文件的路径和名称模式    %h 表示用户家目录
java.util.logging.FileHandler.pattern=%h/java%u.log
# 指定日志文件的大小限制
java.util.logging.FileHandler.limit=50000
# 指定日志文件的数量限制(不能小于1)
java.util.logging.FileHandler.count=1
# 指定日志文件是追加写
java.util.logging.FileHandler.append
# 指定日志消息的格式化器
java.util.logging.FileHandler.formatter=java.util.logging.XMLFormatter
# 配置控制台处理器的相关属性
# 设置日志输出级别
java.util.logging.ConsoleHandler.level=INFO
# 指定日志消息的格式化器
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
# 指定日志消息的格式
java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n

2.7.3 独立日志的配置

logging.properites

在配置文件中加入如下配置

# 设置 com.clear 日志记录器
com.clear.handlers=java.util.logging.ConsoleHandler
com.clear.level=WARNING
# 禁用父类的Handler
com.clear.useParentHandlers=false

为了让我们自定义的日志记录器生效,必须在获取Logger实例之前加载配置文件

我们可以采用静态代码块的方式实现

public class JULTest {
    static {
        LogManager manager = LogManager.getLogManager();
        // 获取配置文件
        InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("logging.properties");
        try {
            manager.readConfiguration(inputStream);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    // 读取配置文件
    @Test
    public void readConfig() {
        // 此时,com.clear包下,只会打印WARNING级别及以上的日志
        LOGGER.warning("warning");
        LOGGER.fine("fine");
    }
}

2.8 过滤器

​ 在JUL(Java Util Logging)中,过滤器用于过滤日志消息,只有符合特定条件的日志消息才会被记录或处理。JUL提供了两种类型的过滤器:日志记录器过滤器(Logger Filter)和处理器过滤器(Handler Filter)。

  • 日志记录器过滤器(Logger Filter)

    • java.util.logging.Filter接口定义了日志记录器过滤器的方法boolean isLoggable(LogRecord record),该方法接收一个LogRecord对象作为参数,并返回一个布尔值,表示是否允许记录该日志消息。
    • 可以通过实现Filter接口来自定义日志记录器过滤器,然后将其设置给特定的日志记录器。
  • 处理器过滤器(Handler Filter)

    • java.util.logging.Handler接口定义了处理器过滤器的方法boolean isLoggable(LogRecord record),该方法接收一个LogRecord对象作为参数,并返回一个布尔值,表示是否允许处理该日志消息。
    • 可以通过实现Handler接口来自定义处理器过滤器,然后将其设置给特定的处理器。

演示:

public class FilterTest {
    public static final Logger LOGGER = Logger.getLogger(FilterTest.class.getName());

    // 日志记录器过滤器测试
    @Test
    public void loggerFilterTest() {
        // 给日志记录器设置过滤器
        LOGGER.setFilter(logRecord ->  // 只打印级别>= INFO的日志
                logRecord.getLevel().intValue() >= Level.INFO.intValue());
        LOGGER.info("hello");
        LOGGER.fine("world");
        LOGGER.finer("apache");
    }

    // 处理器过滤器测试
    @Test
    public void handlerFilterTest() {
        // 禁用父级处理器
        LOGGER.setUseParentHandlers(false);

        ConsoleHandler consoleHandler = new ConsoleHandler();
        consoleHandler.setFormatter(new SimpleFormatter());
        consoleHandler.setLevel(Level.ALL);
        // 给处理器设置过滤器
        consoleHandler.setFilter(logRecord ->  // 只打印a开头的日志
                logRecord.getMessage().startsWith("a"));
        // 将处理器Handler加入到日志记录器Logger中
        LOGGER.addHandler(consoleHandler);

        LOGGER.info("hello");
        LOGGER.info("world");
        LOGGER.info("apache");
    }
}

2.9 打印异常堆栈

在JUL(Java Util Logging)中,可以通过配置日志记录器的级别和处理器来打印异常堆栈信息。

  • 配置日志记录器的级别:

    • 使用Logger.setLevel(Level level)方法来设置日志记录器的级别。
    • 通常,将日志记录器的级别设置为Level.SEVERE或更低的级别,以便记录所有的异常信息。
  • 配置处理器:

    • 使用Handler.setFormatter(Formatter formatter)方法来设置处理器的格式化器。
    • 使用SimpleFormatter类作为格式化器,它会将日志消息格式化为包含时间戳、日志级别和消息内容的字符串。

演示:

将异常的堆栈信息打印至控制台和文件中

package com.clear;

import org.junit.Test;

import java.io.IOException;
import java.util.logging.*;

public class ExceptionTest {
    public static final Logger LOGGER = Logger.getLogger(ExceptionTest.class.getName());

    @Test
    public void printStackTest() throws IOException {
        // 设置日志记录器的级别为FINER
        LOGGER.setLevel(Level.FINER);
        // 创建处理器
        Handler consoleHandler = new ConsoleHandler();
        consoleHandler.setLevel(Level.FINER);
        // 设置处理器的格式化器
        consoleHandler.setFormatter(new SimpleFormatter());
        Handler fileHandler = new FileHandler();
        fileHandler.setLevel(Level.FINER);
        fileHandler.setFormatter(new SimpleFormatter());
        // 将处理器添加到日志记录器
        LOGGER.addHandler(consoleHandler);
        LOGGER.addHandler(fileHandler);

        try {
            // 抛出异常
            int a = 10 / 0;
        } catch (ArithmeticException e) {
            // 打印异常堆栈信息
            LOGGER.throwing(ExceptionTest.class.getName(), "printStackTest", e);
        }
    }
}

3 LOG4J

​ LOG4J是一个流行的Java日志框架,它提供了强大的日志记录和管理功能。LOG4J可以帮助开发人员在应用程序中实现灵活的日志记录,并提供了多种配置选项和输出格式。

LOG4J的一些主要特点和用法:

  • 配置文件:LOG4J使用一个配置文件来定义日志记录器、日志级别、输出目标等配置信息。默认的配置文件名为log4j.properties,也可以使用XML格式的配置文件log4j.xml
  • 日志级别:LOG4J提供了多个日志级别,包括DEBUGINFOWARNERRORFATAL。可以根据需要设置不同的日志级别,以控制日志记录的详细程度。
  • 日志记录器:LOG4J使用日志记录器(Logger)来记录日志消息。每个日志记录器都有一个唯一的名称,可以根据需要创建多个日志记录器。
  • 输出目标:LOG4J支持将日志消息输出到不同的目标,如控制台、文件、数据库等。可以根据需要配置不同的输出目标。
  • 格式化:LOG4J允许自定义日志消息的格式。可以使用预定义的格式化器,如PatternLayout,也可以自定义格式化器。
  • 运行时配置:LOG4J支持在运行时动态修改日志配置。可以通过修改配置文件或使用API来改变日志记录器的级别、输出目标等配置。

3.1 LOG4J的使用

1)添加POM依赖

​ 添加LOG4J依赖:在项目的构建文件(如Maven的pom.xml)中添加LOG4J的依赖项

<dependency>
    <groupId>log4jgroupId>
    <artifactId>log4jartifactId>
    <version>1.2.17version>
dependency>

2)创建LOG4J配置文件

​ 创建LOG4J配置文件:在项目的资源目录下创建一个LOG4J的配置文件,命名为log4j.propertieslog4j.xml。配置文件中定义了日志记录器、日志级别、输出目标等配置信息

参考如下:

# 设置根日志级别为INFO
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{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n

3)在代码中使用

在代码中使用LOG4J:在需要记录日志的类中,通过获取LOG4J的Logger对象来进行日志记录

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

public class Log4jTest {
    // 获取Logger实例
    public static final Logger logger = Logger.getLogger(Log4jTest.class);

    @Test
    public void test1() {
        // 日志输出
        logger.fatal("fatal");
        logger.error("error");
        logger.warn("warn");
        logger.info("info");
        logger.debug("debug");
        logger.trace("trace");
    }
}

如果我们不添加配置文件,会报如下错误:

D:\software\jdk1.8.0_131\bin\java.exe ...
log4j:WARN No appenders could be found for logger (com.clear.Log4jTest).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.

JUL没有添加配置文件,不会报错,是因为他是Java原生的日志框架,拥有默认的配置文件

如果我们没有添加配置文件,加上如下方法也是可以的,他会初始化一个默认的配置文件

BasicConfigurator.configure();

4)运行和查看日志

​ 运行项目时,LOG4J会根据配置文件中的设置进行日志记录。日志消息将根据配置的输出目标(如控制台、文件)进行输出。可以根据配置文件中的日志级别设置,过滤和查看不同级别的日志消息。

3.2 默认配置解读

BasicConfigurator.configure();

当我们在代码中加上如上一行时,可以在使用log4j时不需要添加配置文件

演示:

// 获取Logger实例
public static final Logger logger = Logger.getLogger(Log4jTest.class);

@Test
public void test1() {
    BasicConfigurator.configure();
    // 日志输出
    logger.fatal("fatal");
    logger.error("error");
    logger.warn("warn");
    logger.info("info");
    logger.debug("debug");
    logger.trace("trace");
}

结果如下:

0 [main] FATAL com.clear.Log4jTest  - fatal
1 [main] ERROR com.clear.Log4jTest  - error
1 [main] WARN com.clear.Log4jTest  - warn
1 [main] INFO com.clear.Log4jTest  - info
1 [main] DEBUG com.clear.Log4jTest  - debug

源码解读:

BasicConfigurator.configure(); 这一行代码会给RootLogger加上一个控制台的输出源,类似于JUL的JHandler

在LOG4J中,BasicConfigurator.configure()是一个静态方法,用于简单配置日志记录器。它会自动创建一个默认的日志记录器,并将其配置为使用控制台输出日志消息。

BasicConfigurator.configure()方法的主要作用是:

1)创建一个默认的日志记录器:该方法会创建一个名为"root"的日志记录器,并将其设置为根日志记录器。

2)配置日志记录器:默认的配置会将日志级别设置为DEBUG,并将一个ConsoleAppender添加到日志记录器中。

3)设置日志输出格式:默认的配置会使用PatternLayout来定义日志消息的输出格式。

使用BasicConfigurator.configure()方法可以快速启动LOG4J的基本配置,方便进行简单的日志记录。但是,它的配置选项有限,如果需要更复杂的配置,可以使用自定义的配置文件或编程方式进行配置

public static  void configure() {
    Logger root = Logger.getRootLogger();
    root.addAppender(new ConsoleAppender(
        new PatternLayout(PatternLayout.TTCC_CONVERSION_PATTERN)));
}

// 其中 PatternLayout.TTCC_CONVERSION_PATTERN)
public final static String TTCC_CONVERSION_PATTERN = "%r [%t] %p %c %x - %m%n"

3.3 日志级别

在Log4j中,日志级别从低到高分别为(与JUL略有不同)

  • TRACE:最低级别的日志,用于追踪程序的详细执行过程。
  • DEBUG:用于调试程序,输出一些调试信息。(默认)
  • INFO:用于输出程序的运行信息,如启动、关闭等。
  • WARN:用于输出警告信息,表示可能出现的问题。
  • ERROR:用于输出错误信息,表示程序出现了错误但不影响程序的继续运行。
  • FATAL:最高级别的日志,用于输出严重的错误信息,表示程序无法继续运行。

和JUL一样,也有两个特殊的级别:

  • OFF 用于关闭日志记录
  • ALL 启用所有消息的日志记录

可以根据需要选择适当的日志级别来记录日志,通常建议在生产环境中使用INFO级别或更高级别,而在开发和调试阶段使用DEBUG级别。

3.4 核心组件

3.4.1 Loggers

​ Logger(日志记录器):用于记录日志消息的主要组件。每个Logger对象都与一个特定的日志记录器名称相关联,可以通过该名称来标识不同的日志记录器。

​ 实例的命名就是类"XX"的 full quailied name(类的全限定名),Logger的名字对大小写敏感,其命名有继承机制:例如:名称为 com.clear.service 的logger 会继承自名称为 com.clear 的logger,与JUL一致。

​ Log4j有一个特殊的Logger叫做"root",它是所有Logger的根,所有的Logger都会直接或间接继承自它。root logger可以用Logger.getRootLogger()方法获取。在JUL中也有一个 名为.的根

3.4.2 Appenders

​ Appender(日志输出器):与JUL的Handler类似,用于将日志消息输出到不同的目标,如控制台、文件、数据库等。Log4j提供了多种类型的Appender,可以根据需求选择合适的输出目标。

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

例如:

// 配置一个控制台输出源
ConsoleAppender consoleAppender = new ConsoleAppender();
consoleAppender.setName("clear");
consoleAppender.setWriter(new PrintWriter(System.out));
logger.addAppender(consoleAppender);

3.4.3 Layouts

Layout(日志布局器):用于定义日志消息的格式。Layout将日志消息转换为特定的字符串格式,以便输出到Appender指定的目标。

3.4.4 Lever

​ Level(日志级别):用于控制日志消息的输出级别。可以根据需要设置不同的日志级别,只有达到指定级别及以上的日志消息才会被记录和输出。

3.4.5 Configuration

​ Configuration(配置):用于配置Log4j的各个组件。可以通过配置文件或编程方式来配置Log4j,包括设置日志级别、指定Appender和Layout等。

总结

​ 这些核心组件共同工作,实现了灵活、可配置的日志记录功能。通过合理配置和使用这些组件,可以实现对日志消息的记录、输出和管理。

3.5 编程配置

在Log4j中,可以通过编程方式进行配置

// 获取Logger实例
public static final Logger logger = Logger.getLogger(Log4jTest.class);

@Test
public void test2(){
    // 获取根Logger实例
    Logger rootLogger = Logger.getRootLogger();
    // 创建一个ConsoleAppender对象
    ConsoleAppender consoleAppender = new ConsoleAppender();
    // 设置输出流为标准输出流
    consoleAppender.setWriter(new PrintWriter(System.out));
    Layout layout = new PatternLayout(PatternLayout.TTCC_CONVERSION_PATTERN);
    // 将consoleAppender添加到根Logger实例中,以便将日志输出到控制台
    consoleAppender.setLayout(layout);
    rootLogger.addAppender(consoleAppender);

    logger.warn("warn");
    logger.info("info");
    logger.debug("debug");

}

3.6 配置文件

​ Log4j的配置文件是一个用于配置日志记录器的文本文件,它定义了日志记录器的行为和输出方式。Log4j支持多种配置文件格式,包括XML、JSON和属性文件

​ 配置文件的命名通常为log4j.properties或log4j.xml,可以放置在类路径下或指定的位置。在应用程序启动时,Log4j会自动加载并解析配置文件,根据配置文件的定义进行日志记录

例如:

log4j.properties

# 设置根日志级别为INFO,并将日志输出到Console和File(其实这里的名字可以随意起,影响不大)
log4j.rootLogger=INFO, Console, File

# 定义控制台输出
log4j.appender.Console=org.apache.log4j.ConsoleAppender
# 指定控制台输出的Layout为 PatternLayout
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
# 设置控制台输出的日志消息格式
log4j.appender.Console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5p %c{1} - %m%n

# 定义文件输出
log4j.appender.File=org.apache.log4j.RollingFileAppender
# 指定日志文件的路径和文件名(这里定义的是相对路径,可根据实际需要进行修改)
log4j.appender.File.File=logs/app.log
# 设置日志文件的最大大小为10MB
log4j.appender.File.MaxFileSize=10MB
# 设置保留的日志文件备份数量为10个
log4j.appender.File.MaxBackupIndex=10
# 指定文件输出的 Layout为 PatternLayout
log4j.appender.File.layout=org.apache.log4j.PatternLayout
# 设置文件输出的日志消息格式
log4j.appender.File.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5p %c{1} - %m%n

说明

​ 控制台输出使用PatternLayout进行格式化,文件输出使用RollingFileAppender进行日志文件的滚动。日志文件将按照文件大小进行切割,每个文件最大为10MB,最多保留10个备份文件。

如果需要分别控制输出到控制台和文件中的日志级别,则如下:

# 输出到控制台的日志级别
log4j.rootLogger=INFO, Console

# 输出到文件的日志级别(自定义Logger)
log4j.logger.myapp=WARN, File
# 禁止将myapp Logger的日志事件传递给父Logger(在自定义Logger时需要配置,否则会重复打印日志)
log4j.additivity.myapp=false

# 控制台输出的配置
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n

# 文件输出的配置
log4j.appender.File=org.apache.log4j.RollingFileAppender
log4j.appender.File.File=/path/to/log/file.log
log4j.appender.File.MaxFileSize=10MB
log4j.appender.File.MaxBackupIndex=10
log4j.appender.File.layout=org.apache.log4j.PatternLayout
log4j.appender.File.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n

log4j2.xml


<Configuration status="WARN">
    <Appenders>
        
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
        Console>

        
        <RollingFile name="File" fileName="logs/app.log"
            filePattern="logs/app-%d{yyyy-MM-dd}.log">
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
            <Policies>
                <SizeBasedTriggeringPolicy size="10MB" />
            Policies>
            <DefaultRolloverStrategy max="10" />
        RollingFile>
    Appenders>

    <Loggers>
        
        <Root level="INFO">
            <AppenderRef ref="Console" />
            <AppenderRef ref="File" />
        Root>
    Loggers>
Configuration>
  • :指定XML文件的版本和编码。
  • :设置Log4j的全局配置,其中status属性设置为WARN表示只输出警告级别以上的日志信息。
  • :定义日志输出的Appenders。
  • :定义控制台输出的Appender,其中name属性为Appender的名称,target属性指定输出目标为系统标准输出。
  • :设置控制台输出的日志消息格式。
  • :定义文件输出的Appender,其中name属性为Appender的名称,fileName属性指定日志文件的路径和文件名,filePattern属性指定日志文件的滚动模式。
  • :定义日志文件滚动的策略。
  • :设置日志文件的滚动触发策略为基于文件大小,当日志文件达到10MB时触发滚动。
  • :设置日志文件的滚动策略为默认策略,最多保留10个备份文件。
  • :定义日志记录器。
  • :设置根日志记录器的级别为INFO。
  • :将控制台输出和文件输出的Appender引用添加到根日志记录器中。

输出格式

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 输出代码中的行号
%x 输出和当前线程相关的 NDC(嵌套诊断环境)
%% 输出一个 “%” 字符

可以在 % 与字符之间加上修饰符来控制最小宽度、最大宽度和文本的对其方式。如:

  • %5c 输出category名称,最小宽度是5,category<5,默认的情况下右对齐
  • %-5c 输出category名称,最小宽度是5,category<5,"-"号指定左对齐,会有空格
  • %.5c 输出category名称,最大宽度是5,category>5,就会将左边多出的字符截掉,<5不会有空格
  • %20.30c category名称<20补空格,并且右对齐,>30字符,就从左边交远销出的字符截掉

3.7 PatternLayout源码

PatternLayout这个实现类,可以根据特定的占位符进行转换,类似于JUL,但是又不一样,如下是他的构造器:

public PatternLayout(String pattern) {
    this.pattern = pattern;
    head = createPatternParser((pattern == null) ? DEFAULT_CONVERSION_PATTERN :
                               pattern).parse();
}

传入一个pattern字符,他会根据pattern字符串创建一个链表

接着查看解析器:

protected PatternParser createPatternParser(String pattern) {
    return new PatternParser(pattern);
}

查看parse()方法:

// 如下是这个方法的简略版
public
    PatternConverter parse() {
    char c;
    i = 0;
    while(i < patternLength) {
        c = pattern.charAt(i++);
        switch(state) {
         .....
                            finalizeConverter(c);  // 这个方法最重要的就是这个
         ......
    return head;
}

下面是finalizeConverter()方法:

protected
  void finalizeConverter(char c) {
    PatternConverter pc = null;
    switch(c) {
    case 'c':
      pc = new CategoryPatternConverter(formattingInfo,
					extractPrecisionOption());
      //LogLog.debug("CATEGORY converter.");
      //formattingInfo.dump();
      currentLiteral.setLength(0);
      break;
    // 处理类名的转换器
    case 'C':
      pc = new ClassNamePatternConverter(formattingInfo,
					 extractPrecisionOption());
      //LogLog.debug("CLASS_NAME converter.");
      //formattingInfo.dump();
      currentLiteral.setLength(0);
      break;
    // 处理时间的转换器        
    case 'd':
      String dateFormatStr = AbsoluteTimeDateFormat.ISO8601_DATE_FORMAT;
      DateFormat df;
      String dOpt = extractOption();
      if(dOpt != null)
	dateFormatStr = dOpt;

      if(dateFormatStr.equalsIgnoreCase(
                                    AbsoluteTimeDateFormat.ISO8601_DATE_FORMAT))
	df = new  ISO8601DateFormat();
      else if(dateFormatStr.equalsIgnoreCase(
                                   AbsoluteTimeDateFormat.ABS_TIME_DATE_FORMAT))
	df = new AbsoluteTimeDateFormat();
      else if(dateFormatStr.equalsIgnoreCase(
                              AbsoluteTimeDateFormat.DATE_AND_TIME_DATE_FORMAT))
	df = new DateTimeDateFormat();
      else {
	try {
	  df = new SimpleDateFormat(dateFormatStr);
	}
	catch (IllegalArgumentException e) {
	  LogLog.error("Could not instantiate SimpleDateFormat with " +
		       dateFormatStr, e);
	  df = (DateFormat) OptionConverter.instantiateByClassName(
			           "org.apache.log4j.helpers.ISO8601DateFormat",
				   DateFormat.class, null);
	}
      }
      pc = new DatePatternConverter(formattingInfo, df);
      //LogLog.debug("DATE converter {"+dateFormatStr+"}.");
      //formattingInfo.dump();
      currentLiteral.setLength(0);
      break;
    // 输出日志时间发生的位置,包括类名、线程、及代码所在行数        
    case 'F':
      pc = new LocationPatternConverter(formattingInfo,
					FILE_LOCATION_CONVERTER);
      //LogLog.debug("File name converter.");
      //formattingInfo.dump();
      currentLiteral.setLength(0);
      break;
    case 'l':
      pc = new LocationPatternConverter(formattingInfo,
					FULL_LOCATION_CONVERTER);
      //LogLog.debug("Location converter.");
      //formattingInfo.dump();
      currentLiteral.setLength(0);
      break;
    case 'L':
      pc = new LocationPatternConverter(formattingInfo,
					LINE_LOCATION_CONVERTER);
      //LogLog.debug("LINE NUMBER converter.");
      //formattingInfo.dump();
      currentLiteral.setLength(0);
      break;
    case 'm':
      pc = new BasicPatternConverter(formattingInfo, MESSAGE_CONVERTER);
      //LogLog.debug("MESSAGE converter.");
      //formattingInfo.dump();
      currentLiteral.setLength(0);
      break;
    case 'M':
      pc = new LocationPatternConverter(formattingInfo,
					METHOD_LOCATION_CONVERTER);
      //LogLog.debug("METHOD converter.");
      //formattingInfo.dump();
      currentLiteral.setLength(0);
      break;
    case 'p':
      pc = new BasicPatternConverter(formattingInfo, LEVEL_CONVERTER);
      //LogLog.debug("LEVEL converter.");
      //formattingInfo.dump();
      currentLiteral.setLength(0);
      break;
    case 'r':
      pc = new BasicPatternConverter(formattingInfo,
					 RELATIVE_TIME_CONVERTER);
      //LogLog.debug("RELATIVE time converter.");
      //formattingInfo.dump();
      currentLiteral.setLength(0);
      break;
    case 't':
      pc = new BasicPatternConverter(formattingInfo, THREAD_CONVERTER);
      //LogLog.debug("THREAD converter.");
      //formattingInfo.dump();
      currentLiteral.setLength(0);
      break;
      /*case 'u':
      if(i < patternLength) {
	char cNext = pattern.charAt(i);
	if(cNext >= '0' && cNext <= '9') {
	  pc = new UserFieldPatternConverter(formattingInfo, cNext - '0');
	  LogLog.debug("USER converter ["+cNext+"].");
	  formattingInfo.dump();
	  currentLiteral.setLength(0);
	  i++;
	}
	else
	  LogLog.error("Unexpected char" +cNext+" at position "+i);
      }
      break;*/
    case 'x':
      pc = new BasicPatternConverter(formattingInfo, NDC_CONVERTER);
      //LogLog.debug("NDC converter.");
      currentLiteral.setLength(0);
      break;
    case 'X':
      String xOpt = extractOption();
      pc = new MDCPatternConverter(formattingInfo, xOpt);
      currentLiteral.setLength(0);
      break;
    default:
      LogLog.error("Unexpected char [" +c+"] at position "+i
		   +" in conversion patterrn.");
      pc = new LiteralPatternConverter(currentLiteral.toString());
      currentLiteral.setLength(0);
    }

    addConverter(pc);
  }

下面就是一个典型的链表结构的创建:

protected void addConverter(PatternConverter pc) {
    currentLiteral.setLength(0);
    // Add the pattern converter to the list.
    addToList(pc);
    // Next pattern is assumed to be a literal.
    state = LITERAL_STATE;
    // Reset formatting info
    formattingInfo.reset();
}
private void  addToList(PatternConverter pc) {
    if(head == null) {
        head = tail = pc;
    } else {
        tail.next = pc;
        tail = pc;
    }
}

构建完转换器链表之后,就是循环这个链表,依次处理对应的占位符,他的核心格式化的方法也是format方法,在format方法中通过一个转换器链来完成转化:

public static format(LoggingEvent event){
    // 在format方法中是通过转换器链来完成的
    PatternConverter c = head;
    
    while(c != null){
        // 这一句是核心,第一个参数是StringBuilder,第二个参数是LoggingEvent
        c.format(sbuf, event);
        c = c.next;
    }
    return sbuf.toString;
} 

这里就是通过一个pattern字符串,这个字符串可能张这个样子(%-d{yyyy-MM-dd HH:mm:ss​} [%t:%r] -[%p] %m%n),使用createPatternParser().parse()构建第一个处理器的链表,这个每个处理器处理一个占位符,例如:%d

进入 c.format()方法,我们会进入一个抽象类 PatternConverter 中的format方法,该方法的核心代码如下:

public void format(StringBuilder sbuf, LoggingEvent e){
	// 核心语句如下
	String s = convert(e);
}

3.10 日志分割

在Log4j中,可以通过配置来实现日志的分割,即将日志按照一定的规则拆分成多个文件,以便于管理和查看。

Log4j提供了多种方式来实现日志的分割,其中常用的方式包括:

  • 按时间分割:可以按照日期、小时、分钟等时间单位来拆分日志文件。例如,每天生成一个新的日志文件,或者每小时生成一个新的日志文件。
  • 按文件大小分割:可以按照文件大小来拆分日志文件。例如,当日志文件达到一定大小时,自动创建一个新的日志文件。
  • 按日志级别分割:可以按照日志级别来拆分日志文件。例如,将不同级别的日志分别记录到不同的文件中

源码中,FileAppender类有几个子类,用于实现日志文件的滚动、分割:

  • DailyRollingFileAppender:按照日期滚动的Appender。它可以按照一定的时间间隔(如每天、每周、每月)生成新的日志文件
  • RollingFileAppender:按照文件大小滚动的Appender。它可以在日志文件达到一定大小时生成新的日志文件
    • ExternallyRolledFileAppende:外部滚动的Appender。它允许外部程序控制日志文件的滚动。可以通过外部程序的信号或命令来触发日志文件的滚动。

如下是使用DailyRollingFileAppenderRollingFileAppender来实现日志文件的分割和滚动:

# 设置根日志级别为INFO
log4j.rootLogger=INFO, dailyRollingFile, rollingFile

# 配置DailyRollingFileAppender
log4j.appender.dailyRollingFile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.dailyRollingFile.File=logs/application.log
# 指定按照日期滚动的规则
log4j.appender.dailyRollingFile.DatePattern='.'yyyy-MM-dd
log4j.appender.dailyRollingFile.layout=org.apache.log4j.PatternLayout
log4j.appender.dailyRollingFile.layout.ConversionPattern=%d{HH:mm:ss.SSS} [%t] %-5p %c{1} - %m%n

# 配置RollingFileAppender
log4j.appender.rollingFile=org.apache.log4j.RollingFileAppender
log4j.appender.rollingFile.File=logs/application.log
# 日志文件到达10MB时生成新文件
log4j.appender.rollingFile.MaxFileSize=10MB
# 保留最多5个备份文件
log4j.appender.rollingFile.MaxBackupIndex=5
log4j.appender.rollingFile.layout=org.apache.log4j.PatternLayout
log4j.appender.rollingFile.layout.ConversionPattern=%d{HH:mm:ss.SSS} [%t] %-5p %c{1} - %m%n

# 设置日志输出格式
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{HH:mm:ss.SSS} [%t] %-5p %c{1} - %m%n

# 配置日志级别
log4j.logger.com.example=DEBUG

# 关闭Log4j内部日志输出
log4j.logger.org.apache.log4j=OFF

​ 在Java源代码中,Log4j的日志分割功能由RollingFileAppender类实现。它通过检查日志文件的大小或时间来决定是否需要生成新的日志文件。

例如:

import org.apache.log4j.RollingFileAppender;
import org.apache.log4j.PatternLayout;

RollingFileAppender appender = new RollingFileAppender();
appender.setFile("logs/application.log");
appender.setLayout(new PatternLayout("%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"));
appender.setMaxFileSize("10MB");
appender.setMaxBackupIndex(5);
appender.activateOptions();

Logger logger = Logger.getLogger("com.clear.MyClass");
logger.addAppender(appender);

3.6 自定义Logger

log4j.properties

# 设置根Logger的日志级别和输出目标
log4j.rootLogger=INFO, console

# 设置自定义Logger的日志级别和输出目标
log4j.logger.com.clear=DEBUG, myappFile
# 禁止将myapp Logger的日志事件传递给父Logger(在自定义Logger时需要配置,否则会重复打印日志)
log4j.additivity.com.clear=false

# 设置输出目标为控制台
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.myappFile=org.apache.log4j.FileAppender
log4j.appender.myappFile.File=D:/test/myapp.log
log4j.appender.myappFile.layout=org.apache.log4j.PatternLayout
log4j.appender.myappFile.layout.ConversionPattern=%d [%t] %-5p %c - %m%n

测试

public static final Logger LOGGER = Logger.getLogger(DIY.class);
@Test
public void test(){
    // 测试自定义Logger
    // 我们配置文件中设置了DEBUG级别以及上的才输出到日志文件
    LOGGER.trace("trace");
    LOGGER.debug("debug");
    LOGGER.info("info");
    LOGGER.warn("warn");
    LOGGER.error("error");
    LOGGER.fatal("fatal");
}

结果

D:\test\myapp.log 内容如下,确实只打印了DEBUG及以上的日志

2023-07-21 21:33:00,514 [main] DEBUG com.clear.DIY - debug
2023-07-21 21:33:00,515 [main] INFO  com.clear.DIY - info
2023-07-21 21:33:00,515 [main] WARN  com.clear.DIY - warn
2023-07-21 21:33:00,515 [main] ERROR com.clear.DIY - error
2023-07-21 21:33:00,515 [main] FATAL com.clear.DIY - fatal

4 日志门面

日志门面(Logging Facade)是一种设计模式,用于在应用程序中实现日志记录的抽象层。它的作用是提供一个统一的接口,使应用程序可以使用不同的日志库进行日志记录,而不需要直接依赖于特定的日志库。

日志门面的作用有以下几个方面:

  • 解耦应用程序和具体的日志库:通过使用日志门面,应用程序可以将日志记录的实现细节与具体的日志库解耦。这意味着应用程序可以在不修改代码的情况下切换或替换不同的日志库,以满足不同的需求或适应不同的环境。
  • 统一日志记录接口:日志门面提供了一个统一的接口,使应用程序可以使用相同的日志记录方法和语法,无论使用哪个具体的日志库。这简化了日志记录的代码编写和维护,并提高了代码的可读性和可维护性。
  • 支持多个日志级别和日志输出目标:日志门面通常支持多个日志级别(如调试、信息、警告、错误等)和多个日志输出目标(如控制台、文件、数据库等)。这使得应用程序可以根据需要选择适当的日志级别和输出目标,以满足不同的调试、监控和故障排查需求。
  • 提供日志记录的扩展和定制能力:通过日志门面,应用程序可以使用日志库提供的扩展和定制功能,如日志过滤、格式化、异步记录等。这使得应用程序可以根据具体需求对日志记录进行定制和优化,以满足特定的业务需求和性能要求。

总之,日志门面的作用是提供一个抽象层,使应用程序可以灵活地使用不同的日志库,并提供统一的日志记录接口和功能,以提高代码的可维护性、可读性和灵活性

4.1 阿里日志规约

  • 【强制】应用中不可直接使用日志系统(Log4、Logback)中的API ,而应依赖使用日志框架(SLF4)、JCL–Jakarta Commons Logging)中的API,使用门面模式的日志框架 ,有利于维护和各个类的日志处理方式统一。

    • 说明:日志框架 (SLF4)JCL–Jakarta Commons Logging)的使用方式(推荐使用 SLF4J)

    • // 使用 SLF4J:
      import org.slf4j Logger;
      import org slf4j. LoggerFactory;
      private static final Logger logger = LoggerFactory getLogger(Test.class);
      
      // 使用 JCL:
      import org.apache.commons.logging.Log;
      import org.apache.commons.logging. LogFactory;
      private static final Log log = LogFactory getLog(Test.class);
      
  • 【强制】所有日志文件至少保存 15 天,因为有些异常具备以“周” 为频次发生的特点。对于当天日志,以“应用名.log〞 来保存,保存在/home/admin/应用名/logs/目录下,过往日志格式为:{logname.log}.{保存日期},日期格式:yyyy-MM-dd

  • 正例:以aap 应用为例,日志保存在/home/admin/aapserver/logs/aap.log,历史日志名称为aap.log.2016-08-01

  • 【强制】根据国家法律,网络运行状态、网络安全事件、个人敏感信息操作等相关记录,留存的日志不少于六个月,并且进行网络多机备份。

  • 【强制】应用中的扩展日志(如打点、临时监控、访问日志等) 命名方式:

    • appName log Type Iog Name.log。 logType:日志类型

    • 如stats/monitor/access 等; logName:日志描述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。

    • 说明:推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。

    • 正例:mppserver 应用中单独监控时区转换异常,如:mppserver_monitor_timeZoneConvert.log

  • 【强制】在日志输出时 ,字符串变量之间的拼接使用占位符的方式

    • 说明:因为String 字符串的拼接会使用 StringBuilder 的append0方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。

    • 正例:

    ```java
    logger.debug("Processing trade with id: ( and symbol: (", id, symbol);
    ```
    
  • 【强制】对于 trace/debue/info 级别的日志输出,必须进行日志级别的开关判断

    • 说明:虽然在 debug(参数的方法体内第一行代码 isDisabled(Level.DEBUGLINT)为真时( S1f4j的常见实现Log4j 和Logback),就直接return ,但是参数可能会进行字符串拼接运算。此外,如果 debug(getNameo这种参数内有getName0方法调用,无谓浪费方法调用的开销。

    • 正例:

      // 如果判断为真,那么可以输出 trace 和 debug 级别的日志
      if (logger.isDebugEnabled0) {
          logger.debug(Current ID is: & and name is: , id, getNamel);
      }
      
  • 【强制】避免重复打印日志,浪费磁密空间 ,务必在日志配置文件中设置 additivity =false

    • 正例:

      <logger name=”com.taobao.dubbo.config” additivity="false>
      
  • 【强制】生产环境禁止直接使用 System.out 或System.err 输出日志或使用e.printStackTrace(打印昇常堆桟。

    • 说明:标准日志输出与标准错误输出文件每次 Jboss 重启时才滚动,如果大量输出送往这两个文件,容易造成文件大小超过操作系统大小限制。
  • 【强制】异常信息应该包括两类信息 :案发现场信息和异常堆栈信息。如果不处理,那么通过关键字 throws 往上抛出。

    • 正例:

      logger.error("inputParams:{} and errorMessage:{}”,各类参数或者对系 toString(), e.getMessage(), e);
      
  • 【强制】日志打印时禁止直接用 JSON 工具将对象转换成 String

    • 说明:如果对象里某些 get 方法被覆写,存在抛出异常的情况,则可能会因为打印日志而影响正常业务流程的执行。
    • 正例:打印日志时仅打印出业务相关属性值或者调用其对象的 toString() 方法。
  • 【推荐】递慎地记录日志。生产环境禁止输出 debug 日志:有选择地输出 info 日志:如果使用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。

    • 说明:大量地输出无效日志 ,不利于系统性能提升 ,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
  • 【推荐】可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。如非必要,请不要在此场景打出 error级别,避免频繁报警。

    • 说明:注意日志输出的级别,error 级别只记录系统逻辑出错、异常或者重要的错误信息
  • 【推荐】尽量用英文来描述日志错误信息,如果日志中的错误信息用英文描述不清楚的话使用中文描述即可,否则容易产生歧义。

    • 说明:国际化团队或海外部署的服务器由于字符集问题,使用全英文来注释和描述日志错误信息。

为什么要使用日志规约:

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

4.2 SLF4J

​ 简单日志门面(Simple Logging Facade For Java)SLF4J主要是为了给Java日志访问提供一套标准、规范的API框架,其意义主要是提供接口,具体的实现交由日志框架,例如log4j、logback等。当然slf4j 自己也提供了功能较为简单的日志实现,但是我们一般不使用。

对于一般的Java项目而言,我们会选择 slf4j-api 作为门面,再配上具体的日志实现(log4j、logback等),中间使用桥接器完成桥接。

SLF4J主要提供了两大功能:

  • 日志框架的绑定
  • 日志框架的桥接

4.2.1 使用SLF4J入门(slf4j与simple整合)

​ SLF4J(Simple Logging Facade for Java)是一个日志门面框架,它提供了一组简单的接口,用于在Java应用程序中进行日志记录。SLF4J本身提供了一个简单的日志实现,称为SimpleLogger。SimpleLogger是SLF4J的默认日志实现,它不需要额外的依赖,可以直接在项目中使用

要使用SimpleLogger,只需按照以下步骤进行配置:

1)添加SLF4J依赖:在项目的构建文件(如pom.xml)中添加SLF4J的依赖。

pom.xml

<dependencies>
    
    
    <dependency>
        <groupId>org.slf4jgroupId>
        <artifactId>slf4j-apiartifactId>
        <version>1.7.32version>
    dependency>
	
    <dependency>
        <groupId>org.slf4jgroupId>
        <artifactId>slf4j-simpleartifactId>
        <version>1.7.32version>
    dependency>
dependencies>

2)配置日志级别:在项目的根目录下创建一个名为simplelogger.properties的文件,并添加以下内容:

这一步可以不配置

org.slf4j.simpleLogger.defaultLogLevel=debug

上述配置将日志级别设置为DEBUG,你可以根据需要将其调整为其他级别,如INFO、WARN、ERROR等。

3)在代码中使用SLF4J接口进行日志记录:在需要记录日志的地方,使用SLF4J提供的接口进行日志记录。

例如:

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

public class SLF4JTest {
    // 获取Logger实例
    public static final Logger logger = LoggerFactory.getLogger(SLF4JTest.class);
    public static void main(String[] args) {
        logger.debug("Debug message");
        logger.info("Info message");
        logger.warn("Warning message");
        logger.error("Error message");
    }
}

​ 通过以上步骤,就可以在Java应用程序中使用SLF4J的SimpleLogger进行日志记录。SimpleLogger的实现非常简单,适用于简单的应用程序或测试环境。如果需要更高级的日志功能,可以考虑使用其他日志实现框架,如Logback或Log4j2

4.2.2 绑定其他日志的实现(Binding)

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

使用SLF4J的日志绑定流程:

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

绑定jul的实现(jul是JCL门面的实现),及与jdk14整合

<dependencies>
    
    
    <dependency>
        <groupId>org.slf4jgroupId>
        <artifactId>slf4j-apiartifactId>
        <version>1.7.32version>
    dependency>

    <dependency>
        <groupId>org.slf4jgroupId>
        
        <artifactId>slf4j-jdk14artifactId>
        <version>1.7.32version>
    dependency>
dependencies>

绑定log4j的实现(slf4j与log4j整合)

<dependencies>
    
    
    <dependency>
        <groupId>org.slf4jgroupId>
        <artifactId>slf4j-apiartifactId>
        <version>1.7.32version>
    dependency>

    <dependency>
        <groupId>org.slf4jgroupId>
        <artifactId>slf4j-log4j12artifactId>
        <version>1.7.32version>
    dependency>
    <dependency>
        <groupId>log4jgroupId>
        <artifactId>log4jartifactId>
        <version>1.2.17version>
    dependency>
dependencies>

说明

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

​ SLF4J不依赖于任何特殊的类加载。实际上,每个SLF4J绑定在编译时都是硬连线的,以使用一个且一个特定的日志记录框架。例如, slf4j-log4j12-1.7.32.jar绑定在编译时绑定以使用log4j

4.2.3 桥接旧的日志框架(Birdging)

​ SLF4J提供了日志桥接(Logging Bridges)的功能,用于将其他日志框架的日志记录转发到SLF4J接口。这样可以在项目中使用SLF4J进行日志记录,同时仍然能够使用其他日志框架的功能

​ 通常,您依赖的某些组件依赖于SLF4J以外的日志记录APl。您也可以假设这些組件在不久的将来不会切换到SLF4J。为了解决这种情况,SLF4J附带了几个桥接模块,这些模共将对log4j,JCL和 java.util.logging API 的调用重定向,就好像它们是对SLF4J API一样。
​ 就是你还用log4j的api写代码,但是具体的实现给你抽离了,我们依赖了一个中间层,这个层其实是用旧的的api操作slf4j,而不是操作具体的实现。

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

  • 1)先去除之前老的日志框架的依赖,必须去掉。
  • 2)添加SLF4J提供的桥接组件,这个组件就是模仿之前老的日志与了一套相同的api,只不过这个api是在调用slf4j的api。
  • 3)为项目添加SLF4J的具体实现。

SLF4J提供的桥接组件




<dependency>
    <groupId>org.slf4jgroupId>
    <artifactId>log4j-over-slf4jartifactId>
    <version>1.7.32version>
dependency>


<dependency>
    <groupId>org.slf4jgroupId>
    <artifactId>jul-to-slf4jartifactId>
    <version>1.7.32version>
dependency>


<dependency>
    <groupId>org.slf4jgroupId>
    <artifactId>jcl-over-slf4jartifactId>
    <version>1.7.32version>
dependency>

注意:

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

4.2.3 SLF4J原理剖析

4.3 JCL

全称为Jakarta Conmons Logging,是Apache提供的一个通用日志API。该日志门面的使用并不是很广泛。

它是为“所有的Java日志实现"提供一个统一的接口,它自身也提供一个日志的实现,但是功能非常的弱 (Simplelog)。所以一般不会单独使用它。他允许开发人员使用不同的具体日志实现工具:Log4j、Jdk自带的日志 (JUL)
JCL有两个基本的抽象类:Log(基本记录器)LogFactory(负责创建Log实例)

简单使用

1)添加依赖

pom.xml

<dependencies>
    <dependency>
        <groupId>commons-logginggroupId>
        <artifactId>commons-loggingartifactId>
        <version>1.2version>
    dependency>
dependencies>

2)编写代码

public class JCLTest {
    // 创建Logger实例
    public static final Log log = LogFactory.getLog(JCLTest.class);

    public static void main(String[] args) {
        log.fatal("fatal");
        log.error("error");
        log.warn("warn");
        log.info("info");
        log.debug("debug");
    }

4.4 日志发展历史

Java日志系统历史从入门到崩溃 - 个人文章 - SegmentFault 思否

5 Logback

​ Logback是一个功能强大的Java日志框架,是Log4j框架的继任者(Logback是由Log4j创始人编写的)。它提供了灵活的配置选项和高性能的日志记录功能,被广泛用于Java应用程序的日志管理。

以下是一些Logback的特点和功能:

  1. 灵活的配置:Logback使用XML或Groovy配置文件来定义日志记录器、日志级别、输出格式等。它支持继承和覆盖配置,可以根据不同的环境和需求进行灵活的配置。
  2. 多种日志级别:Logback支持多种日志级别,包括TRACE、DEBUG、INFO、WARN、ERROR等。开发人员可以根据需要选择适当的日志级别来记录不同类型的信息。
  3. 多种输出方式:Logback支持多种输出方式,包括控制台输出、文件输出、远程日志服务器等。开发人员可以根据需求配置多个输出目标,以便在不同环境下进行日志记录和分析。
  4. 异步日志记录:Logback支持异步日志记录,可以提高日志记录的性能。它使用多线程机制将日志事件异步写入目标输出,避免了阻塞应用程序的性能问题。
  5. 运行时配置更新:Logback支持在运行时动态更新配置,无需重启应用程序。这使得开发人员可以在不停止应用程序的情况下修改日志配置,方便调试和故障排查。
  6. 插件支持:Logback提供了丰富的插件支持,可以扩展其功能。例如,可以使用Logstash插件将日志数据发送到Elasticsearch进行分析和可视化。

5.1 三大模块

Logback框架包含三个主要的模块:logback-core、logback-classic 和 logback-access。

  • logback-core:logback-core是Logback框架的核心模块,提供了日志记录和输出的基本功能。

    • 它定义了Logger、Appender、Layout等核心组件的接口和实现,并提供了日志事件的处理和传递机制。
    • logback-core模块是其他两个模块的基础,它不依赖于任何特定的日志实现。
  • logback-classic:logback-classic是Logback框架的经典模块,是logback-core模块的扩展。

    • 它是Log4j的改良版,实现了完整的SLF4J(Simple Logging Facade for Java)的API,并提供了与SLF4J的适配器,使得应用程序可以无缝地迁移和使用Logback作为日志实现。
    • logback-classic模块还提供了一些额外的功能,如MDC(Mapped Diagnostic Context)和LoggerContext等。
  • logback-access:logback-access是Logback框架的访问模块,用于记录和访问HTTP请求的日志。

    • 它提供了与Servlet容器集成的功能,可以记录HTTP请求的详细信息,如请求URL、请求参数、响应状态等。logback-access模块可以与logback-classic模块一起使用,实现全面的日志记录和访问功能。

这三个模块共同构成了Logback框架的核心功能,开发人员可以根据需要选择和配置相应的模块,以满足应用程序的日志管理需求。

5.2 Logbook的使用

1)要想使用Logbook框架,至少需要在pom中导入如下三个依赖:

  • slf4j-api:日志门面
  • logbook-core
  • logback-classic

pom.xml

<dependencies>
    
    
    <dependency>
        <groupId>org.slf4jgroupId>
        <artifactId>slf4j-apiartifactId>
        <version>1.7.32version>
    dependency>

    
    <dependency>
        <groupId>ch.qos.logbackgroupId>
        <artifactId>logback-coreartifactId>
        <version>1.2.3version>
    dependency>

    
    <dependency>
        <groupId>ch.qos.logbackgroupId>
        <artifactId>logback-classicartifactId>
        <version>1.2.3version>
    dependency>
dependencies>

2)在模块的src目录下创建核心配置文件 logback.xml


<configuration>
    
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        
        <target>System.outtarget>
        <encoder>
            
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level]  %c [%thread] : %msg%npattern>
        encoder>
    appender>

    
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%npattern>
            <charset>utf-8charset>
        encoder>
        
        <file>C:/code/data.logfile>
        
        <rollingPolicy
                class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            
            <fileNamePattern>C:/code/data-%d{yyyy-MMdd}.log%i.gzfileNamePattern>
            
            <maxFileSize>1MBmaxFileSize>
        rollingPolicy>
    appender>

    
    <root level="ALL">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE" />
    root>
configuration>

3)创建Logback框架提供的Logger对象,然后用Logger对象调用其提供的方法就可以记录系统的日志信息。

public static final Logger LOGGER = LoggerFactory.getLogger("类名");

简单演示:

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

public class LogBackTest {
    // 创建Logger日志对象
    public static final Logger LOGGER = LoggerFactory.getLogger("LogBackTest");
    public static void main(String[] args) {
        try {
            LOGGER.info("div方法开始执行~~~");
            div(10, 0);
            LOGGER.info("div方法执行成功~~~");
        } catch (ArithmeticException e) {
            LOGGER.error("div方法执行失败!!!");
        }
    }

    public static void div(int a, int b) {
        LOGGER.debug("参数a: " + a);
        LOGGER.debug("参数b: " + b);
        System.out.println(a + " / " + b + " = " + a / b);
        LOGGER.info(a + " / " + b + " = " + a / b);
    }
}

结果

16:08:44.962 [main] INFO LogBackTest - div方法开始执行~~~
16:08:44.964 [main] DEBUG LogBackTest - 参数a: 10
16:08:44.964 [main] DEBUG LogBackTest - 参数b: 0
16:08:44.964 [main] ERROR LogBackTest - div方法执行失败!!!

5.3 核心配置文件logback.xml

  • 对LogBack日志框架进行控制

日志的输出位置、输出格式的设置

  • 通常可以设置2个输出日志的位置:一个是控制台、一个是系统文件中
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
  • 开启(ALL)、关闭日志(OFF)
<root level="ALL">
	<appender-ref ref="CONSOLE"/> 
    <appender-ref ref="FILE" />
root>

如下是一个logback.xml的模板

<configuration>

    
    <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>

    
    <property name="LOG_DIR" value="/path/to/log/directory"/>

    
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    root>

    
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${LOG_PATTERN}pattern>
        encoder>
    appender>

    
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_DIR}/application.logfile>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_DIR}/application.%d{yyyy-MM-dd}.%i.logfileNamePattern>
            <maxHistory>30maxHistory>
        rollingPolicy>
        <encoder>
            <pattern>${LOG_PATTERN}pattern>
        encoder>
    appender>

configuration>
  • 定义日志输出的格式:通过元素定义一个名为LOG_PATTERN的属性,可以在后面的配置中引用该属性。
  • 定义日志输出的目录:通过元素定义一个名为LOG_DIR的属性,可以在后面的配置中引用该属性。
  • 定义根日志级别:通过元素定义根日志的级别,以及要使用的日志输出器(appender)。
  • 定义控制台输出的日志:通过元素定义一个名为CONSOLE的控制台输出器,使用ConsoleAppender类,并配置日志格式。
  • 定义文件输出的日志:通过元素定义一个名为FILE的文件输出器,使用RollingFileAppender类,并配置日志文件路径、滚动策略、日志格式等。

打印日志到控制台

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    
    <target>System.outtarget>
    <encoder>
        
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level]  %c [%thread] : %msg%npattern>
    encoder>
appender>
  • ConsoleAppender类表示日志信息输出到控制台

  • %d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] %c [%thread] : %msg%n中:

    • 表示输出格式
    • %d{yyyy-MM-dd HH:mm:ss.SSS} 表示日期年月日时分秒
    • %-5level 表示日志输出的级别
    • %c 表示当前所在类(打印日志的类)
    • %thread 表示当前线程
    • %msg 表示日志信息
    • %n 表示换行

打印日志到文件

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%npattern>
        <charset>utf-8charset>
    encoder>
    
    <file>C:/code/data.logfile>
    
    <rollingPolicy
                   class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        
        <fileNamePattern>C:/code/data-%d{yyyy-MMdd}.log%i.gzfileNamePattern>
        
        <maxFileSize>1MBmaxFileSize>
    rollingPolicy>
appender>
  • RollingFileAppender类表示日志信息输出到文件

  • utf-8 记录写入到文件中的字符集编码为UTF-8

  • C:/code/data.log 控制文件输出的路径

5.4 Logback日志级别的设置

以下是Logback中定义的日志级别:(从小到大)

  • TRACE:最低的日志级别,用于输出最详细的日志信息,通常用于调试目的。(追踪,指明程序运行轨迹)
  • DEBUG:用于输出调试信息,比TRACE级别更高,通常用于开发和调试阶段。
  • INFO:用于输出一般的信息性消息,表示程序正常运行的状态。
  • WARN:用于输出警告信息,表示潜在的问题或不符合预期的情况,但不会影响程序的正常运行。
  • ERROR:用于输出错误信息,表示严重的问题或错误,可能会导致程序的异常终止或不可恢复的错误。

除了以上五个标准的日志级别,Logback还支持自定义的日志级别。可以根据具体的需求定义自己的日志级别,并在配置文件中进行配置。

​ 在Logback中,可以通过配置文件(通常是logback.xmllogback.groovy)来设置日志级别。可以为不同的日志记录器(Logger)设置不同的日志级别,以控制不同部分的日志输出。

例如,可以使用以下配置将日志级别设置为DEBUG

  • 只有日志级别是大于或等于核心配置文件配置的日志级别,才会别记录,否则不会被记录
<root level="DEBUG">
    <appender-ref ref="CONSOLE"/>
    <appender-ref ref="FILE" />
root>

5.5 日志拆分

在 Logback 中,可以通过配置来实现日志的拆分。拆分日志可以按照时间、文件大小或者其他条件进行

  • 时间拆分:使用 TimeBasedRollingPolicy 可以按照时间来拆分日志文件。可以通过 指定拆分后的日志文件名格式,例如 logfile-%d{yyyy-MM-dd}.%i.log,其中 %d{yyyy-MM-dd} 表示按照日期进行拆分,%i 表示拆分后的文件索引。可以通过 指定保留的历史日志文件的数量。
  • 大小拆分:使用 SizeBasedTriggeringPolicy 可以按照文件大小来拆分日志文件。可以通过 指定每个日志文件的最大大小,例如 10MB。当日志文件达到指定大小时,会自动拆分为新的日志文件。
  • 条件拆分:使用 OnMarkerEvaluator 可以根据自定义的条件来拆分日志文件。可以通过 配置一个自定义的 OnMarkerEvaluator,并在代码中使用 Marker 来触发日志拆分。

演示:

将日志按照时间和文件大小进行拆分

<configuration>
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>/path/to/logfile.logfile>
        
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            
            <fileNamePattern>/path/to/logfile-%d{yyyy-MM-dd}.%i.logfileNamePattern>
            
            <maxHistory>30maxHistory>
        rollingPolicy>
        
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            
            <maxFileSize>10MBmaxFileSize>
        triggeringPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%npattern>
        encoder>
    appender>

    <root level="INFO">
        <appender-ref ref="FILE" />
    root>
configuration>

上面配置中,日志文件将按照日期进行拆分,每天生成一个新的日志文件。同时,当日志文件达到 10MB 时,也会触发拆分为新的日志文件

5.6 Logback过滤器

在 Logback 中,可以使用过滤器来控制日志的输出。过滤器可以根据不同的条件来决定是否输出某条日志。

Logback 提供了多种类型的过滤器,常用的过滤器包括:

  • LevelFilter根据日志级别进行过滤。可以通过设置 level 属性来指定过滤的日志级别,只有达到指定级别的日志才会被输出。
  • ThresholdFilter:根据日志级别的阈值进行过滤。可以通过设置 level 属性来指定阈值,只有达到或超过指定级别的日志才会被输出。
  • EvaluatorFilter:根据自定义的评估器进行过滤。可以通过设置 evaluator 属性来指定一个自定义的评估器,根据评估器的结果来决定是否输出日志。
  • MarkerFilter:根据 Marker 进行过滤。可以通过设置 marker 属性来指定一个 Marker,只有包含指定 Marker 的日志才会被输出。
  • TurboFilter:自定义的高性能过滤器。可以通过继承 ch.qos.logback.core.spi.TurboFilter 类来实现自定义的过滤器。

演示:

使用 LevelFilter 过滤器只输出 INFO 级别的日志

<configuration>
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%npattern>
    encoder>
  appender>
  
  <root level="DEBUG">
    <appender-ref ref="CONSOLE" />
  root>
  
  <logger name="com.clear" level="DEBUG" />
  
  <filter class="ch.qos.logback.classic.filter.LevelFilter">
    
    <level>INFOlevel>
    <onMatch>ACCEPTonMatch>
    <onMismatch>DENYonMismatch>
  filter>
configuration>

在上述配置中,所有的日志都会输出到控制台。但是通过 LevelFilter 过滤器,只有 INFO 级别的日志才会被接受(输出),其他级别的日志会被拒绝(不输出)

5.7 Logback异步日志

打印日志到文件中需要涉及到大量的文件IO,性能比较低,Logback之所以高效就是因为它支持异步日志

在 Logback 中,可以使用异步日志来提高日志的性能和吞吐量。异步日志将日志的写入操作放在一个独立的线程中进行,不会阻塞主线程的执行。

使用方式:

​ 要启用异步日志,可以使用 Logback 提供的 AsyncAppenderAsyncAppender 是一个包装器,可以将其他的 Appender 转换为异步的。

演示:

<configuration>
    <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
        
        <appender-ref ref="FILE" />
        
        <queueSize>512queueSize>
        
        
        
        
        <discardingThreshold>0discardingThreshold>
    appender>

    
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>logs/myapp.logfile>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%npattern>
        encoder>
    appender>

    <root level="DEBUG">
        
        <appender-ref ref="ASYNC_FILE" />
    root>
configuration>

6 LOG4J2

​ Log4j 2 是一个用于 Java 应用程序的日志框架,它提供了灵活的配置和强大的日志功能。与 Logback 类似,Log4j 2 也支持异步日志,可以提高日志的性能和吞吐量(其实就是参考了Logback而设计出来的)

6.1 与Log4J的关系

LOG4J和LOG4J2都是Java的日志框架,它们都提供了强大的日志记录和管理功能。然而,它们在设计和实现上有一些区别。

  • 设计理念:LOG4J是LOG4J2的前身,它的设计目标是提供一个简单、灵活的日志框架。LOG4J2则是LOG4J的升级版,它在LOG4J的基础上进行了重构和改进,旨在提供更高的性能和更丰富的功能。
  • 性能:LOG4J2相对于LOG4J在性能上有所提升。LOG4J2使用了异步日志记录机制,可以在不阻塞应用程序的情况下进行日志记录,从而提高了应用程序的性能。
  • 配置方式:LOG4J使用基于属性的配置文件(如log4j.properties)或基于XML的配置文件(如log4j.xml)来配置日志记录器和输出目标。LOG4J2则引入了新的配置方式,支持使用XML、JSON、YAML等格式的配置文件,同时也支持使用编程API进行配置。
  • 插件支持:LOG4J2引入了插件机制,可以通过插件扩展日志框架的功能。LOG4J2提供了一些内置的插件,如异步日志记录器、日志事件过滤器等,同时也支持自定义插件。
  • 日志框架迁移:由于LOG4J2在设计和实现上与LOG4J有所不同,因此在迁移项目中使用LOG4J2时,可能需要进行一些代码和配置的调整。

总的来说,LOG4J2是LOG4J的升级版,它在性能、配置方式和功能扩展方面有所改进。如果你正在开始一个新的项目,或者希望提升现有项目的日志性能和功能,那么LOG4J2可能是一个更好的选择。但如果你已经在使用LOG4J,并且没有特别的需求,那么继续使用LOG4J也是可以的。

6.2 默认配置

DefaultConfiguration类提供的默认配置

private volatile Configuration configuration = new DefaultConfiguration();

6.3 Log4j2的使用

6.3.1 使用log4j-api做门面

要使用 Log4j 2,需要在项目中添加 Log4j 2 的依赖,并配置 Log4j 2 的配置文件

1)pom.xml

<dependencies>
    
    <dependency>
        <groupId>org.apache.logging.log4jgroupId>
        <artifactId>log4j-apiartifactId>
        <version>2.14.1version>
    dependency>
    
    <dependency>
        <groupId>org.apache.logging.log4jgroupId>
        <artifactId>log4j-coreartifactId>
        <version>2.14.1version>
    dependency>
dependencies>

2)配置 Log4j 2 的配置文件,位于类路径的根目录下。

log4j2.xml


<Configuration status="INFO">
    <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>

3)代码中使用

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

public class Log4j2Test {
    private static final Logger LOGGER = LogManager.getLogger(Log4j2Test.class);
    @Test
    public void test(){
        LOGGER.fatal("fatal");
        LOGGER.error("error");
        LOGGER.warn("warn");
        LOGGER.info("info");
        LOGGER.debug("debug");
        LOGGER.trace("trace");
    }
}

结果:

21:24:15.027 [main] FATAL com.clear.Log4j2Test - fatal
21:24:15.027 [main] ERROR com.clear.Log4j2Test - error
21:24:15.027 [main] WARN  com.clear.Log4j2Test - warn
21:24:15.027 [main] INFO  com.clear.Log4j2Test - info
21:24:15.027 [main] DEBUG com.clear.Log4j2Test - debug

6.3.2 使用slf4j做门面

使用slf4j作为日志的门面,使用log4j2作为日志的实现

1)pom.xml

<dependencies>
    
    <dependency>
        <groupId>org.apache.logging.log4jgroupId>
        <artifactId>log4j-apiartifactId>
        <version>2.14.1version>
    dependency>
    
    <dependency>
        <groupId>org.apache.logging.log4jgroupId>
        <artifactId>log4j-coreartifactId>
        <version>2.14.1version>
    dependency>
    
    <dependency>
        <groupId>org.slf4jgroupId>
        <artifactId>slf4j-apiartifactId>
        <version>1.7.32version>
    dependency>
    
    <dependency>
        <groupId>org.apache.logging.log4jgroupId>
        <artifactId>log4j-slf4j-implartifactId>
        <version>2.12.1version>
    dependency>
dependencies>

6.4 log4j2异步日志

要使用 Log4j 2 的异步日志功能,你需要进行以下配置:

1)添加 Log4j 2 的异步日志依赖

pom.xml

<dependency>
    <groupId>org.apache.logging.log4jgroupId>
    <artifactId>log4j-asyncartifactId>
    <version>2.14.1version>
dependency>

2)创建 Log4j 2 的配置文件 log4j2.xml,并将其放置在类路径下

log4j2.xml


<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="info">
            <AppenderRef ref="Console"/>
        Root>
    Loggers>
Configuration>

3)在代码中使用log4j的API记录日志

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

public class Log4j2Test {
    private static final Logger LOGGER = LogManager.getLogger(Log4j2Test.class);
    @Test
    public void test(){
        LOGGER.fatal("fatal");
        LOGGER.error("error");
        LOGGER.warn("warn");
        LOGGER.info("info");
        LOGGER.debug("debug");
        LOGGER.trace("trace");
    }
}

注意:

​ 异步日志可以提高性能,但也可能会导致日志消息的顺序不一致。如果你需要确保日志消息的顺序,请使用同步日志。

7 日志的打印

基本格式

必须使用参数化信息的方式:

logger.debug("Processing ing trade with id:[{}] and symbol:[{}]",id,symbol);

不要进行字符串拼接,那样会产生多个String对象,占用空间,影响性能。

错误示例:

logger.debug("Processing ing trade with id:“ + id + ” symbol:" + symbol);

ERROR,影响到程序正常运行、当前请求正常运行的异常情况:

  • 打开配置文件失败

  • 所有第三方对接的异常(包括第三方返回异常码)

  • 所有影响功能使用的异常,包括 SQLException 和除了业务异常之外的所有异常(RuntimeException和Exception)

  • 不应该出现的情况,比如使用阿里云传图片,但是未响应

  • 如果有Throwable信息,需要记录完整的堆栈信息

log.error("获取用户[{}]的用户信息时出错", userName, e);

说明:

​ 如果进行了抛出异常操作,请不要记录error日志,由最终处理方进行处理

反例(错误示例):

try{
	...
}catch(Exception e){
	String errorMessage = String.format("Error while reading information of user [%s]",userName);
    logger.error(errorMessage, e);
    throw new UserServiceException(errorMessage, e);
}

WARN,不应该出现但是不影响程序、当前请求正常运行的异常情况:

有容错机制的时候出现的错误情况

找不到配置文件,但是系统能自动创建配置文件,比如我们第一次格式化Hadoop时就会出现WARN,并帮我们创建日志

即将接近临界点的时候,比如缓存池占用达到警戒线,业务异常的记录,比如用户锁定异常

INFO,系统运行信息

  • Service方法中对于系统/业务状态的变更
  • 主要逻辑的分步骤:1.初始化什么 2.加载什么
  • 外部接口的部分
  • 客户端请求参数(REST/WS)
  • 调用第三方时的调用参数和调用结果
  • 对于复杂的业务逻辑,需要进入日志打点,以及埋点记录
  • 调用其它第三方服务时,所有的出参和入参是必须要记录的(因为我们很难追溯第三方的错误)

说明

​ 并不是所有的service都进行出入口大点记录,单一、简单service是没有意义的(job除外,job需要记录开始、结束)

反例:

public List listByBaseType(Integer baseTypeId){
	log.info("开始查询基地");
    BaseExample e = new BaseExample();
    BaseExample.Criteria ctr = e.createCriteria();
    ctr.andIsDeleteEqualTo(IsDelete.USE.getValue());
    Options.doIfPresent(baseTypeId, ctr::andIsDeleteEqualTo);
    log.info("查询基地结束");
    return baseRepository.selectByExample(e);
}

DEBUG,可以填写所有的想知道的信息(但是也需要填写有意义的信息

  • 生成环境需要关闭DEBUG信息

  • 如果在生产环境情况下需要开启DEBUG,需要使用开关进行管理,不能一直开启

说明

​ 如果代码中出现以下代码,可以进行优化:

​ 1.获取用户基本薪资

​ 2.获取用户休假情况

​ 3.计算用户所得薪资

logger.debug("开始获取员工[{}] [{}]年基本薪资", employee, year);	
logger.debug("获取员工[{}] [{}]年基本薪资为[{}]", employee, year, basicSalary);	
logger.debug("开始获取员工[{}] [{}]年[{}]月休假情况", employee, year, month);	
logger.debug("员工[{}] [{}]年[{}]月年假/病假/事假为[{}]/[{}]/[{}]", employee, year, month, annualLeaveDays, sickLeaveDays, noPayLeaveDays);	
logger.debug("开始计算员工[{}] [{}]年[{}]月应得薪资", employee, year, month);	
logger.debug("员工[{}] [{}]年[{}]月应得薪资为[{}]", employee, year, month, actualSaraly);

TRACE,特别详细的系统运行完全信息,业务代码中,不要使用(除非有特殊用意,否则使用DEBUG代替)

你可能感兴趣的:(Java日志框架,java,开发语言,log4j)