嘘~ 小破站正在拼命加载中 . . .

聊聊Java的日志框架


大家好,我是晓凡

一、日志概念

日志的重要性不用我多说了,日志,简单来说就是记录。

用来记录程序运行时发生的事情。比如,程序启动了、执行了某个操作、遇到了问题等等,这些都可以通过日志记录下来。

想象一下,你开了一家店,每天的营业额、顾客的反馈、商品的进出、库存等等,你都会记录下来。这就像是程序的日志。比如:

  • 电商网站:记录用户的登录、浏览、购买行为,监控交易过程,及时发现异常交易;通过日志分析你的浏览记录,实现精准推送等等
  • 服务器:记录服务器的启动、运行、关闭状态,以及发生的各种错误,帮助管理员及时发现并解决问题。

1.1 日志的作用

  1. 调试帮助:当程序出现问题时,通过查看日志,可以快速定位问题发生的地方和原因。
  2. 监控运行状态:通过日志可以了解程序的运行状态,比如用户的操作、系统的性能等。
  3. 安全审计:在需要记录用户行为或系统操作的场合,日志可以作为审计的依据。

1.2 具体示例

public class SimpleApp {
    public static void main(String[] args) {
        System.out.println("程序启动");

        // 假设这里是用户输入数据
        String userInput = "Hello, World!";
        System.out.println("用户输入了: " + userInput);

        // 处理数据
        String result = processInput(userInput);
        System.out.println("处理结果: " + result);
        try {
            //可能异常的逻辑代码
        }catch(Exception e){
            e.printStackTrace()
        }

        // 程序结束
        System.out.println("程序结束");
    }

    private static String processInput(String input) {
        // 这里是处理逻辑
        return "Processed: " + input;
    }
}

上面的代码我们不陌生了吧,我们使用System.out.println来打印程序的运行状态,使用e.printStackTrace()来打印信息和错误

这就是没有日志框架时,最简单直接的日志打印方式

这种方式简单直接,但也有一些缺点:

  • 灵活性差:不能方便地控制日志的输出格式、级别等。
  • 性能问题:大量日志输出可能会影响程序性能。
  • 不易管理:日志信息混在标准输出中,不易于查找和分析。

所以我们要引入各种功能强大的日志框架进行日志管理

二、主流日志框架

日志框架由日志门面和日志实现构成,具体如下图所示

主流日志框架

2.1 日志门面

顾名思义,日志门面,就像是一个团队的领导者一样,只负责制定规则,安排任务,而具体干活的则交给苦逼的打工人(日志具体实现)即可。

日志门面提供了一套标准的日志记录接口,而具体的日志记录工作则由不同的日志框架来完成。

这样做的好处是,可以在不修改代码的情况下,通过配置来切换不同的日志框架。

正如职场中,一个打工人跑路了,在不需要太多成本,不用做太多改变的情况下,新招一个更便宜的打工人也可完成同样的任务实现快速切换,好像有点扯远了

主流的日志门面框架主要有:

  • **SLF4J**:这是一个非常流行的日志门面,它提供了一套简单的日志记录接口,并且可以与多种日志框架(如Log4j、Logback等)配合使用。
  • **JCL**:这是早期的一个日志门面

2.2 日志实现

通过是实现日志门面接口来完成日志记录,实实在在的打工人无疑了

主流的日志实现框架有:

  • JUL

    Java自带的日志框架 ,功能相对基础,性能一般,但对于简单的日志需求来说足够用了。

  • Log4j

​ 个非常老牌的日志框架,功能非常强大,可以自定义很多日志的细节,比如日志级别、输出格式、输出目的地等。现由Apache软件基金会维护

  • Log4j2

​ 也是Apache软件基金会开发,相比Log4jLog4j2在性能上有显著提升,同时保持了丰富的功能,支持异步日志处理,适合高性能需求的场景

  • Logback

​ 由Log4j的原开发者之一主导开发,Spring Boot默认日志,轻量级,性能优秀,功能也比较全面

三、JUL日志框架

3.1 主要组件

  1. Logger:日志记录器,是日志系统的核心,用来生成日志记录。
  2. Handler:日志处理器,负责将日志信息输出到不同的目的地,比如控制台、文件等。可以为每个Logger配置一个或多个Handler
  3. Formatter:日志格式化器,负责定义日志的输出格式。比如时间戳、日志级别、消息等。
  4. Level:设置日志级别,常见的级别有SEVEREWARNINGINFOCONFIGFINEFINERFINEST等。
  5. Filter: 这个组件用来过滤日志记录。你可以设置一些规则,只有满足这些规则的日志才会被记录。
  6. Log Record:这是日志记录本身,包含了日志的所有信息,比如时间、日志级别、消息等

3.2 使用步骤

  1. 获取Logger实例。
  2. 添加Handler
  3. 为上一步添加的Handler 设置日志级别(Level)和格式输出(Formatter
  4. 创建Filter过滤器
  5. Logger实例添加日志处理器(Handler)和日志过滤器(Filter
  6. 记录日志。

jul使用步骤

3.3 入门案例

public class LogQuickTest {
    @Test
    public void testLogQuick(){
        //创建日志记录对象
        Logger logger = Logger.getLogger("com.xiezhr");
        //日志记录输出
        logger.info("这是一个info日志");
        logger.log(Level.INFO,"这是一个info日志");

        String name="程序员晓凡";
        Integer age=18;
        logger.log(Level.INFO,"姓名:{0},年龄:{1}",new Object[]{name,age});

    }
}

JUT入门案例

3.4 日志级别

日志级别系统,用来区分日志的重要性

3.4.1 日志级别
  1. SEVERE(严重):这是最高级别的日志,用来记录严重错误,比如系统崩溃、数据丢失等。这类日志通常需要立即关注和处理。
  2. WARNING(警告):用来记录可能不会立即影响系统运行,但可能表明潜在问题的信息。比如,某个操作没有达到预期效果,或者系统资源接近耗尽。
  3. INFO(信息):用来记录一般性的信息,比如程序运行的状态、重要的操作步骤等。这类信息对于了解程序的运行情况很有帮助,但通常不需要立即处理。
  4. CONFIG(配置):用来记录配置信息,比如程序启动时加载的配置文件、初始化的参数等。这类日志有助于调试和验证程序的配置是否正确。
  5. FINE(详细):用来记录更详细的信息,比如程序内部的执行细节、变量的值等。这类日志对于开发者在调试程序时了解程序的内部状态非常有用。
  6. FINER(更详细):比FINE级别更细的日志,记录更深入的执行细节。通常用于深入分析程序的运行情况。
  7. FINEST(最详细):这是最低级别的日志,记录最详细的信息,包括程序的每一步执行细节。这类日志可能会产生大量的输出,通常只在需要非常详细的调试信息时使用。
3.4.2 级别关系

SEVERE > WARNING > INFO > CONFIG > FINE > FINER > FINEST

日志级别越高,记录的信息越重要。当你设置一个日志级别时,比如INFO,那么INFO级别以及以上的日志(SEVERE和WARNING)都会被记录,而FINE、FINER和FINEST级别的日志则会被忽略

3.5 详细使用案例(硬编码)

这里我们按照上面的步骤创建一个日志记录器,将日志文件分别输出到控制台和文件中

public class LoggingExampleTest {

    @Test
    public void testLogging() {
        // 获取日志记录器
        Logger logger = Logger.getLogger("LoggingExample");

        // 设置日志级别为INFO,这意味着INFO级别及以上的日志会被记录
        logger.setLevel(Level.INFO);

        // 创建控制台Handler 将日志输出到控制台
        // 并设置其日志级别和Formatter
        ConsoleHandler consoleHandler = new ConsoleHandler();
        consoleHandler.setLevel(Level.WARNING); // 控制台只输出WARNING及以上级别的日志
        consoleHandler.setFormatter(new SimpleFormatter() {
            @Override
            public synchronized String format(LogRecord record) {
                // 自定义日志格式
                return String.format("%1$tF %1$tT [%2$s] %3$s %n", record.getMillis(), record.getLevel(), record.getMessage());
            }
        });
        logger.addHandler(consoleHandler);

        // 创建文件Handler 将日志输出到文件
        // 并设置其日志级别和Formatter
        try {
            FileHandler fileHandler = new FileHandler("app.log", true);
            fileHandler.setLevel(Level.ALL); // 文件将记录所有级别的日志
            fileHandler.setFormatter(new SimpleFormatter() {
                @Override
                public synchronized String format(LogRecord record) {
                    // 自定义日志格式
                    return String.format("%1$tF %1$tT [%2$s] %3$s %n", record.getMillis(), record.getLevel(), record.getMessage());
                }
            });
            logger.addHandler(fileHandler);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 创建并设置Filter
        Filter filter = new Filter() {
            @Override
            public boolean isLoggable(LogRecord record) {
                // 这里可以添加过滤逻辑,例如只记录包含特定字符串的日志
                return record.getMessage().contains("important");
            }
        };

        // 将Filter应用到Logger
        //logger.setFilter(filter);

        // 记录不同级别的日志
        logger.severe("严重错误信息 - 应记录到控制台和文件");
        logger.warning("警告信息 - 应记录到控制台和文件");
        logger.info("常规信息 - 只记录到文件");
        logger.config("配置信息 - 只记录到文件");
        logger.fine("详细日志 - 只记录到文件");


        // 这条日志将被Filter过滤掉,不会记录
        logger.info("这条信息不重要,将被过滤");

        // 这条日志将被记录,因为消息中包含"important"
        logger.info("这条信息很重要,将被记录到控制台和文件");
    }
}       

① 控制台日志输出

1、控制台输出结果

②日志文件输出 app.log内容

2、文件中输出日志

代码解释

  1. Logger获取:首先获取一个名为LoggingExampleLogger实例。
  2. 设置日志级别:将Logger的日志级别设置为INFO,这意味着INFO及以上级别的日志将被记录。
  3. 控制台Handler:创建一个ConsoleHandler实例,设置其日志级别为WARNING,并且自定义了日志的输出格式。
  4. 文件Handler:尝试创建一个FileHandler实例,将日志写入到app.log文件中,并设置其日志级别为ALL,意味着所有级别的日志都将被记录到文件。
  5. 自定义Formatter:为Handler创建自定义的SimpleFormatter,用于定义日志的输出格式。
  6. Filter设置:创建一个实现了Filter接口的匿名内部类,并重写isLoggable方法,实现过滤逻辑,这里只记录消息中包含”important”字符串的日志。
  7. 应用Filter:将创建的Filter应用到Logger上。
  8. 记录日志:记录不同级别的日志,展示不同级别的日志如何被Handler和Filter处理。
  9. 日志记录:一些日志将根据设置的日志级别、Handler和Filter的规则被记录到控制台或文件,或者被忽略。

3.6 日志配置文件

以上3.4小节通过硬编码的方式打印输出日志,这样的方式很不利于后期的管理与维护,这小节我们将使用配置文件的方式进行日志输出

① 在resources下面新建logconfig.properties文件,内容如下

# 指定日志处理器为:ConsoleHandler,FileHandler 表示同时使用控制台和文件处理器
handlers= java.util.logging.ConsoleHandler,java.util.logging.FileHandler

#设置默认的日志级别为:ALL
.level= ALL

# 配置自定义 Logger
com.xiezhr.handlers = com.xiezhr.DefConsoleHandler
com.xiezhr.level = CONFIG

# 如果想要使用自定义配置,需要关闭默认配置
com.xiezhr.useParentHanlders =true

# 向日志文件输出的 handler 对象
# 指定日志文件路径 当文件数为1时 日志为/logs/java0.log
java.util.logging.FileHandler.pattern = /logs/java%u.log
# 指定日志文件内容大小,下面配置表示日志文件达到 50000 字节时,自动创建新的日志文件
java.util.logging.FileHandler.limit = 50000
# 指定日志文件数量,下面配置表示只保留 1 个日志文件
java.util.logging.FileHandler.count = 1
# 指定 handler 对象日志消息格式对象
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# 指定 handler 对象的字符集为 UTF-8 ,防止出现乱码
java.util.logging.FileHandler.encoding = UTF-8
# 指定向文件中写入日志消息时,是否追加到文件末尾,true 表示追加,false 表示覆盖
java.util.logging.FileHandler.append = true


# 向控制台输出的 handler 对象
# 指定 handler 对象的日志级别
java.util.logging.ConsoleHandler.level =WARNING
# 指定 handler 对象的日志消息格式对象
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# 指定 handler 对象的字符集
java.util.logging.ConsoleHandler.encoding = UTF-8

# 指定日志消息格式
java.util.logging.SimpleFormatter.format = [%1$tF %1$tT] %4$s: %5$s %n

注意: 设置日志消息格式中(后面一小节会详细讲解)

  • %1$tF:这个占位符表示日志记录的时间,格式为 yyyy-MM-dd,其中 1$ 表示这是第一个参数tF 是日期的格式化代码
  • %1$tT:这个占位符表示日志记录的时间,格式为 HH:mm:ss.SSS,即小时:分钟:秒.毫秒 1$ 表示这是第一个参数,tT 是时间的格式化代码
  • %4$s: 表示日志级别,level =WARNING 输出警告 level =INFO 输出消息
  • %5$s: 表示日志消息
  • %n:这个占位符表示换行符,每条日志记录之后会有一个换行,以便在查看日志时能够清晰地区分每条记录。

② 日志测试

@Test
public void testLogProperties()throws Exception{

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

    // 4、创建日志记录器
    Logger logger = Logger.getLogger("com.xiezhr");

    // 5、记录不同级别的日志
    logger.severe("这是一条severe级别信息");
    logger.warning("这是一条warning级别信息");


}

执行上面代码后

控制台输出

控制台输出

java0.log文件输出:

ava0.log文件输出

3.7 日志格式化

上面两个小节中,不管是通过编码或者配置文件 都对日志进行了格式化

① 编码设置日志格式

fileHandler.setFormatter(new SimpleFormatter() {
    @Override
    public synchronized String format(LogRecord record) {
        // 自定义日志格式
        return String.format("%1$tF %1$tT [%2$s] %3$s %n", record.getMillis(), record.getLevel(), record.getMessage());
    }
});

② 配置文件指定日志格式

# 指定日志消息格式
java.util.logging.SimpleFormatter.format = [%1$tF %1$tT] %4$s: %5$s %n

上面设置的日志格式设置你看懂了么?

不管是哪种方式设置日志格式,我们看源码最终都是通过String.format函数来实现的,所有我们有必要学一学String类提供的format这个方法的使用

new SimpleFormatter

3.7.1 Stringformat方法

Stringformat方法用来格式化字符串。

format方法就像是一个模板,你可以在这个模板里插入你想要的数据,然后它就会帮你生成一个格式化好的字符串。

我们先来看看下面这个简单例子

@Test
public void testStringFormatter()throws Exception{
    String name = "晓凡";
    Integer age = 18;

    // 使用String.format()方法格式化字符串
    String xiaofan = String.format("%s今年%d岁", name, age);
    System.out.println(xiaofan);
}
//输出
晓凡今年18
3.7.2 常用占位符

%s%d 为占位符,不同类型需要不同占位符,那么还有哪些常用转换符呢?

占位符 详细说明 示例
%s 字符串类型**** “喜欢晓凡请关注”
%c 字符类型 ‘x’
%b 布尔类型 true
%d 整数类型(十进制) 666
%x 整数类型(十六进制) FF
%o 整数类型(八进制) 77
%f 浮点类型 8.88
%a 十六进制浮点类型 FF.34
%e 指数类型 1.28e+5
%n 换行符
%tx 日期和时间类型(x代表不同的日期与时间转换符)
3.7.3 特殊符号搭配使用
符号 说明 示例 结果
0 指定数字、字符前面补0,用于对齐 (”%04d”,6) 0006
空格 指定数字、字符前面补空格,用于对齐 (“[% 4s]”,x) [ x]
以“,”对数字分组显示(常用于金额) (“%,f,666666.66”) 666,666.6600

注意: 默认情况下,可变参数是按照顺序依次替换,但是我们可以通过“数字$”来重复利用可变参数

@Test
public void testStringFormatter()throws Exception{
    String name = "晓凡";
    Integer age = 18;

    // 使用String.format()方法格式化字符串
    String xiaofan = String.format("%s今年%d岁", name, age);
    System.out.println(xiaofan);
    //
    String xiaofan1 = String.format("%s今年%d岁,%1$s的公众号是:程序员晓凡", name, age);
    System.out.println(xiaofan1);
}
//输出
晓凡今年18岁
晓凡今年18,晓凡的公众号是:程序员晓凡

上面例子中我们通过%1$s重复使用第一个参数name

3.7.4 日期格式化

上面我们说到%tx,x代表日期转换符,其具体含义如下

符号 描述 示例
c 包含全部日期和时间信息 周六 8月 03 17:16:37 CST 2024
F “年-月-日” 格式 2024-08-03
D “月/日/年”格式 08/03/24
d 03
r HH:MM:SS PM”格式(12小时制) 05:16:37 下午
R HH:MM”格式(24小时制) 17:16
T HH:MM:SS ”格式(24小时制) 17:16:37
b 月份本地化 8月
y 两位年 24
Y 四位年 2024
m 08
H 时(24小时制) 17
I 时(12小时制) 05
M 16
S 37
s 秒为单位的时间戳 1722677530
p 上午还是下午 下午

四、Log4j日志框架

Log4j 是Apache软件基金组织旗下的一款开源日志框架,是一款比较老的日志框架,目前已出log4j2,它在log4j上做了很大改动,性能提升了不少。但是有些老项目还会在使用,所以我们也来说一说

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

注意: 从官网,我们可以看到项目管理委员会宣布Log4j 1. x已终止使用。建议用户升级到 Log4j 2

log4j官网

4.1 快速入门

4.1.1 添加依赖
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
<!--为了方便测试,我们引入junit-->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>
4.1.2 log4j入门代码
@Test
public void testLog4jQuick()&#123;

    //初始化日志配置信息,不需要配置文件
    BasicConfigurator.configure();
    //获取日志记录器
    Logger logger = Logger.getLogger(Log4jTest.class);
    //通过各种日志级别打印日志
    logger.fatal("这是一条致命的信息");  // 严重错误,一般会造成系统崩溃
    logger.error("这是一条错误的信息");  // 出现错误时,比如出错了但是不影响系统继续运行
    logger.warn("这是一条警告的信息");   // 警告级别,比如要告警的时候
    logger.info("这是一条普通的信息");  // 一般信息,比如记录普通的方法执行
    logger.debug("这是一条调试的信息"); // 调试信息,比如调试的时候打印的信息
    logger.trace("这是一条追踪的信息");  // 追踪信息,比如追踪程序运行路径
&#125;
//输出
0 [main] FATAL Log4jTest  - 这是一条致命的信息
0 [main] ERROR Log4jTest  - 这是一条错误的信息
0 [main] WARN Log4jTest  - 这是一条警告的信息
0 [main] INFO Log4jTest  - 这是一条普通的信息
0 [main] DEBUG Log4jTest  - 这是一条调试的信息

注意: BasicConfigurator.configure(); 为log4j在不添加配置文件的情况下初始化默认日志配置信息,如果既没有默认配置信息,也没有配置文件

会报下面错误

未配置报错

4.2 日志级别

日志级别,就好比是日记本里的不同标记,用来区分信息的重要性。在log4j中,日志级别从低到高分为以下几种:

  1. TRACE:追踪级别,通常用来记录程序运行的详细轨迹,比如方法调用的顺序等。这个级别非常详细,一般在开发阶段或者调试时用得比较多。
  2. DEBUG:调试级别,用来记录程序的运行状态,比如变量的值、程序的流程等。当你需要深入了解程序的内部工作时,DEBUG级别就非常有用。
  3. INFO:信息级别,用来记录程序的正常运行状态,比如程序启动、配置信息、正常结束等。INFO级别的日志对用户和开发者了解程序的运行情况很有帮助。
  4. WARN:警告级别,用来记录一些可能引起问题的情况,但程序仍然可以继续运行。比如,程序遇到了一个不常见的情况,或者某个操作失败了但不影响大局。
  5. ERROR:错误级别,用来记录程序运行中的错误,这些错误通常会影响程序的正常功能,但程序可能还能继续运行。
  6. FATAL:致命级别,用来记录非常严重的错误,这些错误会导致程序完全无法继续运行。比如,程序的某个关键部分失败了,整个应用可能需要重启。

出了上面的,还有以下两个特殊级别

1. **OFF**: 用来关闭日志记录
1. **ALL**: 启用所有消息的日志记录

4.3 Log4j组件

  1. **Logger**:这个组件就像是日志的大脑,负责记录日志信息。你可以想象它是一个日记本的主人,决定哪些事情值得记录,哪些事情可以忽略。
  2. **Appender**:Appender就像是日记本的笔,它决定了日志信息要写到哪里。可以是控制台、文件、数据库,甚至是通过网络发送到远程服务器。每种Appender都有不同的用途和特点。
  3. **Layout**:Layout决定了日志的外观,也就是日志的格式。比如,你可以选择日志中包含时间、日志级别、发生日志的类名和方法名,以及日志的具体内容等。Layout就像是给日记本设计外观样式。
4.3.1 Logger

Log4j中有一个特殊的logger叫做root,它是logger的根,其他的logger都会直接或者间接的继承自root

入门示例中,我们通过Logger.getLogger(Log4jTest.class); 获取的就是root logger

name为org.apache.commons 的logger会继承name为org.apache的logger

4.3.2 Appender

用来指定日志记录到哪儿,主要有以下几种

Appender类型 作用
ConsoleAppender 将日志输出到控制台
FileAppender 将日志输出到文件中
DailyRollingFileAppender 将日志输出到文件中,并且每天输出到一个日志文件中
RollingFileAppender 将日志输出到文件中,并且指定文件的大小,当文件大于指定大小,会生成一个新的日志文件
JDBCAppender 将日志保存到数据库中
4.3.3 Layout

用于控制日志内容输出格式,Log4j常用的有以下几种输出格式

日志格式器 说明
HTMLLayout 将日志以html表格形式输出
SimpleLayout 简单的日志格式输出,例如(info-message)
PatternLayout 最强大的格式化器,也是我们使用最多的一种,我们可以自定义输出格式

示例:下面我们通过PatternLayout 格式化日志

@Test
public void testLog4jLayout()&#123;
    //初始化日志配置信息,不需要配置文件
    BasicConfigurator.configure();
    //获取日志记录器
    Logger logger = Logger.getLogger(Log4jTest.class);
    Layout patternLayout = new PatternLayout("%d&#123;yyyy-MM-dd HH:mm:ss&#125; [%p] - %l - %m%n");// 将自定义的Layout应用到控制台Appender上
    ConsoleAppender consoleAppender = new ConsoleAppender(patternLayout);
    logger.addAppender(consoleAppender);
    // 记录日志
    logger.info("这是一条自定义格式的日志信息");

&#125;
//输出
2024-08-04 13:55:35 [INFO] - Log4jTest.testLog4jLayout(Log4jTest.java:44) - 这是一条自定义格式的日志信息
占位符 说明
%m 输出代码中指定的日志信息
%p 输出优先级
%n 换行符
%r 输出自应用启用到输出log信息消耗的毫秒数
%c 输出语句所属的类全名
%t 输出线程全名
%d 输出服务器当前时间,%d{yyyy-MM-dd HH:mm:ss}
%l 输出日志时间发生的位置,包括类名、线程、及在代码中的函数 例如: Log4jTest.testLog4jLayout(Log4jTest.java:44)
%F 输出日志消息产生时所在的文件夹名称
%L 输出代码中的行号
%5c category名称不足5位时,左边补充空格,即右对齐
%-5c category名称不足5位时,右边补充空格,即左对齐
.5c category名称大于5位时,会将左边多出的字符截取掉,小于5位时,以空格补充

4.4 通过配置文件配置日志

BasicConfigurator.configure(); 上面代码中通过这段代码初始化日志配置信息,这一小节,我们通过配置文件来配置

通过看LogManager日志管理器源码,我们知道可以默认加载如下几种格式的配置文件(其中log4j.xml log4j.properties 是我们最常用的)

  • log4j.properties

  • log4j.xml

  • og4j.configuration

    等等

`LogManager`日志管理器源码

# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,Console
# 指定控制台日志输出appender
log4j.appender.Console = org.apache.log4j.ConsoleAppender
# 指定消息格式器 layout
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
# 指定消息内容格式
log4j.appender.Console.layout.conversionPattern =%d&#123;yyyy-MM-dd HH:mm:ss&#125; [%p] - %l - %m%n

或者

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d&#123;yyyy-MM-dd HH:mm:ss&#125; [%p] - %l - %m%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="INFO">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

4.5 各种日志输出示例

上面小节中已经说了控制台输出配置,由于篇幅原因,这里不再赘述

① 文件输出配置

# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,File
# 指定文件日志输出appender
log4j.appender.File = org.apache.log4j.FileAppender
#  指定日志文件名
log4j.appender.File.File=D:/logs/testxiezhr.log
#  指定是否在原有日志的基础添加新日志
log4j.appender.File.Append=true
# 指定消息格式器 layout
log4j.appender.File.layout=org.apache.log4j.PatternLayout
# 指定消息内容格式
log4j.appender.File.layout.conversionPattern =%d&#123;yyyy-MM-dd HH:mm:ss&#125; [%p] - %l - %m%n
# 指定日志文件编码格式
log4j.appender.File.encoding=UTF-8

②日志文件根据大小分割输出

# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,RollingFile
# 指定文件日志根据大小分割输出appender
log4j.appender.RollingFile = org.apache.log4j.RollingFileAppender
#  指定日志文件名
log4j.appender.RollingFile.File=D:/logs/testxiezhr.log
#  设置是否在重新启动服务时,在原有日志的基础添加新日志
log4j.appender.RollingFile.Append=true
# 设置最多保存的日志文件个数
log4j.appender.RollingFile.MaxBackupIndex=5
# 设置文件大小,超过这个值,就会再产生一个文件
log4j.appender.RollingFile.maximumFileSize=1

# 指定消息格式器 layout
log4j.appender.RollingFile.layout=org.apache.log4j.PatternLayout
# 指定消息内容格式
log4j.appender.RollingFile.layout.conversionPattern =%d&#123;yyyy-MM-dd HH:mm:ss&#125; [%p] - %l - %m%n
# 指定日志文件编码格式
log4j.appender.RollingFile.encoding=UTF-8

最终生成日志效果如下所示

按照日志大小切割文件

③ 日志文件根据日期分割

# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,DailyRollingFile
# 指定文件日志根据日期分割输出appender
log4j.appender.DailyRollingFile = org.apache.log4j.DailyRollingFileAppender
#  指定日志文件名
log4j.appender.DailyRollingFile.File=D:/logs/testxiezhr.log
#  设置是否在重新启动服务时,在原有日志的基础添加新日志
log4j.appender.DailyRollingFile.Append=true

# 指定消息格式器 layout
log4j.appender.DailyRollingFile.layout=org.apache.log4j.PatternLayout
# 指定消息内容格式
log4j.appender.DailyRollingFile.layout.conversionPattern =%d&#123;yyyy-MM-dd HH:mm:ss&#125; [%p] - %l - %m%n
# 指定日志文件编码格式
log4j.appender.DailyRollingFile.encoding=UTF-8

最终生成日志效果如下所示

日志文件根据日期进行分割

④ 自定义日志配置

当我们想定义自己的日志配置时,可以按照如下配置添加.例如:添加com.xiezhr,它也是继承自rootLogger,所以我们必须要添加

log4j.additivity.com.xiezhr=false 避免日志打印重复

# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,DailyRollingFile

# 自定义日志配置
log4j.logger.com.xiezhr=DEBUG,Console
# 设置日志叠加,这一句配置一定要添加,否则日志会重复输出
log4j.additivity.com.xiezhr=false

⑤ 将日志信息存入数据库

首先,我们新建一个testlog数据库,并在数据库下新建log日志表

CREATE TABLE `log` (
  `log_id` int(11) NOT NULL AUTO_INCREMENT,
  `project_name` varchar(255) DEFAULT NULL COMMENT '目项名',
  `create_date` varchar(255) DEFAULT NULL COMMENT '创建时间',
  `level` varchar(255) DEFAULT NULL COMMENT '优先级',
  `category` varchar(255) DEFAULT NULL COMMENT '所在类的全名',
  `file_name` varchar(255) DEFAULT NULL COMMENT '输出日志消息产生时所在的文件名称 ',
  `thread_name` varchar(255) DEFAULT NULL COMMENT '日志事件的线程名',
  `line` varchar(255) DEFAULT NULL COMMENT '号行',
  `all_category` varchar(255) DEFAULT NULL COMMENT '日志事件的发生位置',
  `message` varchar(4000) DEFAULT NULL COMMENT '输出代码中指定的消息',
  PRIMARY KEY (`log_id`)
);

其次,新建JDBCAppender,并且为JDBCAppender 设置数据库连接信息,具体代码如下

@Test
public void testLog4j2db()&#123;
    //初始化日志配置信息,不需要配置文件
    BasicConfigurator.configure();
    //获取日志记录器
    Logger logger = Logger.getLogger(Log4jTest.class);
    // 新建JDBCAppender
    JDBCAppender jdbcAppender = new JDBCAppender();
    jdbcAppender.setDriver("com.mysql.cj.jdbc.Driver");
    jdbcAppender.setURL("jdbc:mysql://localhost:3308/testlog?useSSL=false&serverTimezone=UTC");
    jdbcAppender.setUser("root");
    jdbcAppender.setPassword("123456");
    jdbcAppender.setSql("INSERT INTO log(project_name,create_date,level,category,file_name,thread_name,line,all_category,message) values('晓凡日志测试','%d&#123;yyyy-MM-dd HH:mm:ss&#125;','%p','%c','%F','%t','%L','%l','%m')");

    logger.addAppender(jdbcAppender);
    // 记录日志
    logger.info("这是一条自定义格式的日志信息");
    logger.error("这是一条自定义格式的错误日志信息");
&#125;

最后,运行代码,来看一下效果

日志信息已经村到数据库中了

五、JCL日志门面

何为日志门面,我们在第二小节中已经介绍过了,这里就不多说了。

日志门面的引入,使得我们可以面向接口开发,不再依赖具体的实现类,减小代码耦合。

JCL全称Jakarta Commons Logging是Apache提供的一个通用日志API,JCL中自带一个日志实现simplelog,不过这个功能非常简单

jcl实现图

5.1 JCL快速入门

① LCL的两个抽象类

  • Log: 基本日志记录器
  • LogFactory: 负责创建Log具体实例,如果时log4j,则创建log4j的实例,如果时jul则创建jul实例

② 示例代码

引入依赖

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

基本代码

我们没有导入任何日志实现,所以这里默认使用jdk自带JUL来实现日志

@Test
public void test()&#123;

    Log log = LogFactory.getLog(JclTest.class);

    log.error("这是一条error");
    log.warn("这是一条warn");
    log.info("这是一条info");
    log.debug("这是一条debug");
    log.trace("这是一条trace");
&#125;

日志输出

5.2 快速切换Log4j日志框架

① 导入log4j日志依赖

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

② 添加log4j.properties配置文件

# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,Console
# 指定控制台日志输出appender
log4j.appender.Console = org.apache.log4j.ConsoleAppender
# 指定消息格式器 layout
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
# 指定消息内容格式
log4j.appender.Console.layout.conversionPattern =%d&#123;yyyy-MM-dd HH:mm:ss&#125; [%p] - %l - %m%n

③ 测试日志输出

@Test
public void testJclLog4j()&#123;

    Log log = LogFactory.getLog(JclLog4jTest.class);
    log.error("这是一条error");
    log.warn("这是一条warn");
    log.info("这是一条info");
    log.debug("这是一条debug");
    log.trace("这是一条trace");
&#125;

日志输出如下:

log4j日志输出

我们可以看到,使用了JCL日志门面之后,我们从simplelog日志框架切换到log4j日志框架,没有改过代码。

六、SLF4j日志门面

SLF4j 全称是Simple Logging Facade For JavaJava简单的日志门面 和上一小节说到的JCL干的一样的活。

在现目前的大多数Java项目中,日志框架基本上会选择slf4j-api 作为门面,配上具体实现框架logbacklog4j 等使用

SLF4j是目前市面上最流行的日志门面,主要提供了以下两个功能

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

6.1 快速入门

① 添加依赖

<!--添加日志门面sl4j-->
 <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>2.0.13</version>
 </dependency>
 <!--添加slf4j 自带的简单日志实现-->
 <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-simple</artifactId>
      <version>2.0.13</version>
  </dependency>

②日志输出

 //申明日志对象
public final  static Logger logger = LoggerFactory.getLogger(Slf4jTest.class);
@Test
public void testSlf4j()&#123;
    //打印日志
    logger.error("这是error日志");
    logger.warn("这是warn日志");
    logger.info("这是info日志");
    logger.debug("这是debug日志");
    logger.trace("这是trace日志");

    //使用占位符输出日志信息
    String name = "晓凡";
    Integer age = 18;
    logger.info("&#123;&#125;,今年&#123;&#125;岁", name, age);

    //将系统异常写入日志
    try &#123;
        int i = 1/0;
    &#125;catch (Exception e)&#123;
        logger.error("执行出错", e);
    &#125;

&#125;

上面代码输出日志如下

日志输出

6.2 SLF4j 日志绑定功能

6.2.1 日志绑定原理

下图是从官网薅下来的slf4j 日志绑定图,对了,官网在这https://www.slf4j.org/

logback

小伙伴看到上图可能会有点懵,全是英文,看不懂。

一脸懵逼

于是乎,晓凡简单翻译了一下,如下如所示

slf4j实现原理图

  • 只导入日志门面,没导入日志实现,不会进行日志输出
  • logbacksimplelogno-operation 框架遵循SLF4j规范 导入jar包即可使用
  • log4jJUL 属于比较古老日志框架,不遵循SLF4j规范,需要引入适配器才能使用
  • 当我们导入slf4j-nop后将不会使用任何日志框架
6.2.2 绑定logback日志框架

① 引入logback依赖

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.4.14</version>
</dependency>

② 日志输出

快速入门中代码不变,运行后,采用logback日志框架输入日志如下所示

logback日志输出

6.2.3 绑定slf4j-nop

① 引入依赖

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-nop</artifactId>
    <version>2.0.13</version>
</dependency>

② 此时控制台将不会输出任何日志

6.2.4 使用适配器绑定log4j日志框架

① 导入依赖

<!--log4j适配器-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>2.0.13</version>
</dependency>
<!--log4j日志框架依赖-->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

② 添加log4j.properties配置文件

# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,Console
# 指定控制台日志输出appender
log4j.appender.Console = org.apache.log4j.ConsoleAppender
# 指定消息格式器 layout
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
# 指定消息内容格式
log4j.appender.Console.layout.conversionPattern =%d&#123;yyyy-MM-dd HH:mm:ss&#125; [%p] - %l - %m%n

③ 代码不变,日志输出如下

log4j日志输出

6.2.5 使用适配器绑定JUL日志框架

① 引入依赖

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jdk14</artifactId>
    <version>2.0.13</version>
</dependency>

② 代码不变,日志输出如下

jul日志输出结果

6.3 SLF4j日志桥接

6.3.1 使用场景

如果你的项目中已经使用了Log4j 1.x等老的日志框架,但你想迁移到使用SLF4JAPI,这时候你可以使用SLF4JLog4j 1.x桥接器来平滑过渡

6.3.2 桥接原理

桥接器原理

上图为SLF4j官网提供的桥接原理图,从图中,我们可以看到,只需要引入不同的桥接器log4j-over-slf4jjul-to-slf4jjcl-over-slf4j

就可以实现在不改变原有代码的情况下,将日志从log4jjuljcl迁移到slf4j+logback日志组合

6.3.3 桥接步骤

下面以Log4j 1.x迁移到slf4j+logback日志组合为例

  1. 去除老的日志框架Log4j 1.x依赖

去除老项目中的日志依赖

  1. 添加SLF4J提供的桥接组件

    引入桥接器

  2. 为项目添加SLF4J的具体实现

    引入新的日志实现

七、Logback日志框架

官网:https://logback.qos.ch/index.html

7.1 快速入门

① 添加依赖

<!--添加日志门面SLF4j依赖-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.13</version>
</dependency>
<!--添加Logback日志实现依赖-->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.4.14</version>
</dependency>

② 打印日志代码

public class LogbackTest &#123;

    private static final Logger logger = LoggerFactory.getLogger(LogbackTest.class);

    @Test
    public void testLogbackQuick()&#123;
        logger.error("这是一个错误日志");
        logger.warn("这是一个警告日志");
        logger.info("这是一个信息日志");
        logger.debug("这是一个调试日志");
        logger.trace("这是一个跟踪日志");
    &#125;
&#125;

Logback打印日志

7.2 Logback配置

Logback可以通过编程式配置(添加配置类的方式),也可以通过配置文件配置。

配置文件是日常开发中最常用的,我们这里就以这种方式配置,如果对配置文件感兴趣的小伙伴可自行到官网查看

7.2.1 Logback 包含哪些组件?
  1. **Logger**:日志记录器,用来记录不同级别的日志信息,比如错误、警告、信息、调试和追踪。
  2. **Appender**:指定日志信息输出到不同的地方。比如,你可以设置一个Appender将日志输出到控制台,另一个Appender将日志写入文件,或者发送到远程服务器。
  3. **Encoder**:如果你使用的是文件Appender,Encoder就是用来定义日志文件内容格式的。比如,你可以选择日志的格式是简单文本还是XML。
  4. **Layout**:老版本的Logback中用来定义日志格式的组件。在新版本中,Encoder已经取代了Layout的功能。
  5. **Filter**:指定特定的规则来过滤日志信息,比如只记录错误以上的日志,或者只记录包含特定关键字的日志。
  6. **Configuration**:用来配置Logback的设置,比如设置日志级别、Appender的类型和参数等。配置可以通过XMLJSON或者Groovy脚本来完成。
7.2.2 可以有哪些文件格式进行配置?

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

  • logback.groovy

  • logback-test.xml

  • logback.xml(最常用的)

    如果均不存在会采用默认配置

7.2.3 添加一个ConsoleAppender控制台日志输出配置

配置文件

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--添加一个名字为pattern的属性 用来设置日志输出可是-->
    <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
    <property name="pattern" value="%d&#123;yyyy-MM-dd HH:mm:ss.SSS&#125; %c [%thread]%-5level %msg%n" />

    <!--输出到控制台-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--引用上面配置好的pattern属性-->
            <pattern>$&#123;pattern&#125;</pattern>
        </encoder>
    </appender>
    <!--设置日志级别-->
    <root level="ALL">
        <!--引用上面配置好的consoleAppender将日志输出到控制台-->
        <appender-ref ref="console" />
    </root>
</configuration>

日志输入如下

日志输出

日志输出格式:在前面几个日志框架中我们已经介绍过,大同小异。这里简单说下常用的几种

符号 含义
%d{pattern} 格式化日期
%m或者%msg 日志信息
%M method(方法)
%L 行号
%c 完整类名称
%thread 线程名称
%n 换行
%-5level 日志级别,并且左对齐
7.2.4 添加一个FileAppender将日志输出到文件

配置文件

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--添加一个名字为pattern的属性 用来设置日志输出可是-->
    <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
    <property name="pattern" value="%d&#123;yyyy-MM-dd HH:mm:ss.SSS&#125; %c [%thread]%-5level %msg%n" />

    <!--设置日志文件存放路径-->
    <property name="log_file" value="d:/logs"></property>

    <!--输出到文件-->
    <appender name="file" class="ch.qos.logback.core.FileAppender">
        <encoder>
            <!--引用上面配置好的pattern属性-->
            <pattern>$&#123;pattern&#125;</pattern>
        </encoder>
        <!--被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。-->
        <file>$&#123;log_file&#125;/logback.log</file>
    </appender>
    <!--设置日志级别-->
    <root level="ALL">
        <!--引用上面配置好的FileAppender将日志输出到文件-->
        <appender-ref ref="file" />
    </root>
</configuration>

日志输出如下

将日志输出到文件中

7.2.5 生成html格式appender对象
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--添加一个名字为pattern的属性 用来设置日志输出可是-->
    <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
    <property name="pattern" value="%-5level%d&#123;yyyy-MM-dd HH:mm:ss&#125;%c%M%L%thread%m"/>

    <!--设置日志文件存放路径-->
    <property name="log_file" value="d:/logs"></property>

    <!--输出到文件-->
    <appender name="htmlFile" class="ch.qos.logback.core.FileAppender">

        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">

            <layout class="ch.qos.logback.classic.html.HTMLLayout">
                <!--引用上面配置好的pattern属性-->
                <pattern>$&#123;pattern&#125;</pattern>
            </layout>
        </encoder>
        <!--被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。-->
        <file>$&#123;log_file&#125;/logback.html</file>
    </appender>
    <!--设置日志级别-->
    <root level="ALL">
        <!--引用上面配置好的FileAppender将日志输出到文件-->
        <appender-ref ref="htmlFile" />
    </root>
</configuration>

日志输出:d:/logs目录下生成一个logback.html 文件

image-20240810171050844

7.3 Logback 日志拆分压缩 ⭐

在生产环境中对日志进行按时间、日志大小拆分 且压缩日志非常非常重要,所以单独拿出来说一说

配置文件

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--添加一个名字为pattern的属性 用来设置日志输出可是-->
    <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
    <property name="pattern" value="[%-5level] %d&#123;yyyy-MM-dd HH:mm:ss&#125; %c %M %L [%thread] %m %n" />

    <!--设置日志文件存放路径-->
    <property name="log_file" value="d:/logs"></property>

    <!--输出到文件-->
    <appender name="rollFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--引用上面配置好的pattern属性-->
            <pattern>$&#123;pattern&#125;</pattern>
        </encoder>
        <!--被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。-->
        <file>$&#123;log_file&#125;/roll_logback.log</file>
        <!--滚动记录文件:根据时间来制定滚动策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <fileNamePattern>$&#123;log_file&#125;/roll_logback.%d&#123;yyyy-MM-dd&#125;.log%i.gz</fileNamePattern>
            <!--指定文件拆分大小-->
            <maxFileSize>1MB</maxFileSize>
            <!--日志文件保留天数-->
            <MaxHistory>3</MaxHistory>
        </rollingPolicy>
    </appender>
    <!--设置日志级别-->
    <root level="ALL">
        <!--引用上面配置好的FileAppender将日志输出到文件-->
        <appender-ref ref="rollFile" />
    </root>
</configuration>

日志滚动输出: 按照日期和文件大小进行拆分

按日期和文件大小进行拆分

7.4 异步日志

我们先来解释下什么是异步日志?

我们将日志输出到文件中,这样会涉及到大量io操作,非常耗时,如果需要输出大量的日志,就可能影响正常的主线程业务逻辑。

为了解决这问题,异步日志就出现了。日志信息不是直接写入到日志文件或者控制台,而是先发送到一个队列里,

然后由一个专门的线程去处理这些日志信息的写入工作。

这样做的好处是可以减少日志记录对主程序运行的影响,提高程序的效率。

7.4.1 不加异步日志
private static final Logger logger = LoggerFactory.getLogger(LogbackTest.class);

    @Test
    public void testLogbackQuick()&#123;

        //日志输出
        logger.error("这是一个错误日志");
        logger.warn("这是一个警告日志");
        logger.info("这是一个信息日志");
        logger.debug("这是一个调试日志");
        logger.trace("这是一个跟踪日志");

        //这里模拟业务逻辑
        System.out.println("晓凡今年18岁了");
        System.out.println("晓凡的个人博客是:www.xiezhrspace.cn");
        System.out.println("晓凡的个人公众号是:程序员晓凡");
        System.out.println("晓凡的个人微信是:xie_zhr");
        System.out.println("欢迎关注晓凡,持续输出干货!!!!!");
    &#125;

输出结果:

未加异步日志

从上面控制台输出看,只有当日志输出完成之后我们的业务逻辑代码才被执行。如果日志耗时比较长,非常影响效率

7.4.2 添加异步日志

我们只需在原来的配置文件中添加如下关键配置

<!--添加异步日志配置-->
<appender name="async" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="console" />
</appender>
<root level="ALL">
    <!--引用上面配置好的consoleAppender将日志输出到控制台-->
    <appender-ref ref="console" />
    <!--引用上面配置好的asyncAppender将日志输出到控制台-->
    <appender-ref ref="async" />        
</root>

日志输出效果:

异步日志输出效果

从上面日志日志输出看,不再是日志输出完再进行业务逻辑代码执行,而是异步执行了

八、Log4j2日志框架

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

Log4j2Log4j的升级版,参考了Logback的一些优秀设计,修复了一些bug,性能和功能都带来了极大提升

主要体现在以下几个方面

  • 性能提升: Log4j2 在多线程环境下表现出更高的吞吐量,比 Log4j 1.xLogback 高出10倍

  • 异步日志Log4j2 支持异步日志记录,可以通过 AsyncAppenderAsyncLogger 实现。异步日志可以减少日志记录对主程序性能的影响,尤其是在高并发场景下

  • 自动重载配置Log4j2 支持动态修改日志级别而不需要重启应用,这是借鉴了 Logback 的设计

  • 无垃圾机制Log4j2 大部分情况下使用无垃圾机制,避免因频繁的日志收集导致的 JVM GC2

  • 异常处理Log4j2 提供了异常处理机制,Appender 中的异常可以被应用感知到,而 Logback 中的异常不会被应用感知

Log4j2有这么多优势,所以在未来SLF4j+Log4j2组合

8.1 快速入门

Log4j2不仅仅是日志实现,同时也是日志门面。在快速入门中,我们就使用Log4j2作为日志门面和日志实现来快速入门

8.1.1 添加依赖
<!--添加log4j2日志门面API-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.23.1</version>
</dependency>
<!--添加log4j2日志实现-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.23.1</version>
</dependency>
8.1.2 添加日志实现代码
public class Log4j2Test &#123;

    private static final Logger logger = LogManager.getLogger(Log4j2Test.class);
    @Test
    public void Log4j2Test()&#123;
        logger.fatal("这是一条致命信息");
        logger.error("这是一条错误信息");
        logger.warn("这是一条警告信息");
        logger.info("这是一条一般信息");
        logger.debug("这是一条调试信息");
        logger.trace("这是一条追踪信息");

    &#125;
&#125;

日志输出结果如下

log4j2 日志输出结果

8.2 使用slf4j+log4j2组合

前面我们提到SLF4j+Log4j2组合会是未来日志发展的大趋势,所以接下来我们就使用这个组合来输出日志

导入依赖

<!--添加log4j2日志门面API-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.23.1</version>
</dependency>
<!--添加log4j2日志实现-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.23.1</version>
</dependency>

<!--添加slf4j作为日志门面,使用log4j2作为日志实现-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.13</version>
</dependency>
<!--添加log4j2与slf4j的桥接器-->    
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.23.1</version>
</dependency>

日志输出代码

public class Log4j2Test &#123;
    //这里我们换成了slf4j的门面接口
    private static final Logger logger = LoggerFactory.getLogger(Log4j2Test.class);

    @Test
    public void Log4j2Test()&#123;
        logger.error("这是一条错误信息");
        logger.warn("这是一条警告信息");
        logger.info("这是一条一般信息");
        logger.debug("这是一条调试信息");
        logger.trace("这是一条追踪信息");

    &#125;
&#125;

日志输出效果

slf4j+log4j2组合日志输出

8.3 Log4j2配置

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

下面通过log4j2.xml 配置文件进行测试,配置大同小异,这里就不一一说明了,给出完整的配置

<?xml version="1.0" encoding="UTF-8" ?>
<!--status="warn" 日志框架本身的输出日志级别,可以修改为debug    monitorInterval="5" 自动加载配置文件的间隔时间,不低于 5秒;生产环境中修改配置文件,是热更新,无需重启应用 -->
<configuration status="warn" monitorInterval="5">
    <!--集中配置属性进行管理    使用时通过:$&#123;name&#125;  -->
    <properties>
        <property name="LOG_HOME">D:/logs</property>
    </properties>
    <!--日志处理 -->
    <Appenders>
        <!--控制台输出 appender,SYSTEM_OUT输出黑色,SYSTEM_ERR输出红色 -->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d&#123;HH:mm:ss.SSS&#125; [%t] [%-5level] %c&#123;36&#125;:%L --- %m%n" />
        </Console>
        <!--日志文件输出 appender -->
        <File name="file"  fileName="$&#123;LOG_HOME&#125;/file.log">
            <PatternLayout pattern="[%d&#123;yyyy-MM-dd HH:mm:ss.SSS&#125;] [%-5level] %l %c&#123;36&#125; - %m%n" />
        </File>
        <!-- 使用随机读写流的日志文件输出 appender,性能提高 -->
        <RandomAccessFile name="accessFile" fileName="$&#123;LOG_HOME&#125;/access.log">
            <PatternLayout pattern="[%d&#123;yyyy-MM-dd HH:mm:ss.SSS&#125;] [%-5level] %l %c&#123;36&#125; - %m%n" />
        </RandomAccessFile>
        <!--按照一定规则拆分的日志文件的appender -->
        <!-- 拆分后的文件 -->
        <!-- 拆分后的日志文件命名规则:log-debug.log、log-info.log、log-error.log -->
        <RollingFile name="rollingFile" fileName="$&#123;LOG_HOME&#125;/rolling.log"
                     filePattern="$&#123;LOG_HOME&#125;/$$&#123;date:yyyy-MM-dd&#125;/rolling-%d&#123;yyyy-MM-dd&#125;-%i.log.gz">
            <!-- 日志级别过滤器 -->
            <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY" />
            <!-- 日志消息格式 -->
            <PatternLayout pattern="[%d&#123;yyyy-MM-dd HH:mm:ss.SSS&#125;] [%-5level] %l %c&#123;36&#125; - %msg%n" />
            <Policies>
                <!-- 在系统启动时,出发拆分规则,生产一个新的日志文件 -->
                <OnStartupTriggeringPolicy  />
                <!-- 按照文件大小拆分,1MB -->
                <SizeBasedTriggeringPolicy size="1MB" />
                <!--按照时间节点拆分,规则根据filePattern定义的 -->
                <TimeBasedTriggeringPolicy />
            </Policies>
            <!-- 在同一个目录下,文件的个限定为 10个,超过进行覆盖 -->
            <DefaultRolloverStrategy max="10" />
        </RollingFile>
    </Appenders>
    <!-- logger 定义 -->
    <Loggers>
        <!--使用 rootLogger 配置 日志级别 level="trace" -->
        <Root level="trace">
            <!--指定日志使用的处理器 -->
            <AppenderRef ref="Console" />
<!--            <AppenderRef ref="file"/>-->
            <AppenderRef ref="rollingFile" />
            <AppenderRef ref="accessFile" />
        </Root>
    </Loggers>
</configuration>

日志输出如下

日志按天拆分

下面的截图为2024-08-11的日志按日志文件大小1MB拆分成10个并进行压缩,拆分满10个文件后新日志会覆盖旧日志,其他天的类似

按日志大小和文件数进行拆分

8.4 Log4j2 异步日志

Log4j2最大的特点就是异步日志,就因为异步日志的存在,将性能提升了好多。

下图是官网给的性能对比图,从图中我们可以看出在全局异步模式(Loggers all async) 和混合异步模式(Loggers mixed sync/async)

性能简直将LogbackLog4j日志框架甩了一条街。

至于什么时全局异步模式和混合异步模式?我们会在后面详细说明

日志框架性能比较

8.4.1 陌生名词解释
  • 同步日志:想象一下你手里有一堆信件要写,每写一封信你都得亲自动手,写完后才能去做别的事情。在这个过程中,你得一封一封地写,不能同时干其他事,这就类似于同步日志。在程序中,同步日志意味着每次记录日志时,程序都得停下来,等待日志写完了才能继续执行其他任务。这样做的好处是不会丢信(日志),但坏处是写信(记录日志)这个过程如果太慢,就会耽误你做其他事情(程序运行)

  • 异步日志:如果你特别忙,你可能会找个助手来帮你写信。你只需要告诉他要写什么,然后就可以继续忙自己的事情,而助手会帮你把信写好并寄出去。这个过程就像是异步日志。在程序中,异步日志意味着程序可以把要记录的日志信息交给一个专门的“助手”(通常是另外的线程或进程),然后程序就可以继续执行其他任务,而不需要等待日志写完。这样做的好处是可以更快地处理任务,不会耽误正事儿,但偶尔可能会有一两封信(日志)因为意外情况没有寄出去。

  • 全局异步: 所有日志记录都采用异步的方式记录

  • 混合异步:以在应用中同时使用同步日志和异步日志,这使得日志配置更加灵活

8.4.2 同步日志与异步日志
  1. 同步日志流程

同步日志流程

2、异步日志流程

异步日志流程图

8.5 异步日志配置

异步日志的实现一共有两种方式

  • AsyncAppender [生产上几乎不使用,因为性能低下]

  • AsyncLogger [生产上用得多,因为性能高]

    • 全局异步
    • 混合异步

第一种方式因为用的不多性能也不够好,所以这里就不说了,我们以第二种配置来具体说一说

不管采用哪种方式,首先都要引入异步依赖

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

全局异步

只需在resources下添加log4j2.component.properties,具体内容如下

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

全局异步

日志输出结果

全局异步日志开启,

② 混合异步配置

首先,我们需要关闭全局异步配置,将上面添加的log4j2.component.properties 内容注释即可

log4j2.xml配置

<?xml version="1.0" encoding="UTF-8" ?>
<!--status="warn" 日志框架本身的输出日志级别,可以修改为debug    monitorInterval="5" 自动加载配置文件的间隔时间,不低于 5秒;生产环境中修改配置文件,是热更新,无需重启应用 -->
<configuration status="debug" monitorInterval="5">
    <!--集中配置属性进行管理    使用时通过:$&#123;name&#125;    -->
    <properties>
        <property name="LOG_HOME">D:/logs</property>
    </properties>
    <!--日志处理 -->
    <Appenders>
        <!--控制台输出 appender,SYSTEM_OUT输出黑色,SYSTEM_ERR输出红色 -->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d&#123;HH:mm:ss.SSS&#125; [%t] [%-5level] %c&#123;36&#125;:%L --- %m%n" />
        </Console>
        <!--日志文件输出 appender -->
        <File name="file" fileName="$&#123;LOG_HOME&#125;/file.log">
            <!-- <PatternLayout pattern="[%d&#123;yyyy-MM-dd HH:mm:ss.SSS&#125;] [%-5level] %l %c&#123;36&#125; - %m%n" />-->
            <PatternLayout pattern="%d %p %c&#123;1.&#125; [%t] %m%n" />
        </File>
        <Async name="Async">
            <AppenderRef ref="file" />
        </Async>
    </Appenders>
    <!--logger 定义 -->
    <Loggers>
        <!--自定义 logger 对象  includeLocation="false" 关闭日志记录的行号信息,开启的话会严重影响异步输出的性能            additivity="false" 不再继承 rootlogger对象         -->
        <AsyncLogger name="com.xiezhr" 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>

输出结果:

开启混合异步日志,控制台输出采用异步日志

注意事项:

  • 上面配置AsyncAppender 、全局配置、混合配置 不能同时出现,否则将影响日志性能
  • includeLocation="false" 关闭日志记录的行号信息 配置一定要加上,否则会降低日志性能

九、阿里巴巴日志规约

通过上面八小节我们对Java日志框架应该非常熟悉了,并且也知道怎么使用了。但在日志开发中,使用日志还是有写规约需要我们去遵守。

下面式阿里巴巴Java开发手册中的日志规约

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

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

1)使用SLF4J

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Test.class);
  1. 使用 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.2021-03-23

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

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

  • logType:日志类型,如 stats/monitor/access等;

  • logName:日志描述。

    这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。

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

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

mppserver_monitor_timeZoneConvert.log

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

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

正例:

logger.debug("Processing trade with id: &#123;&#125; and symbol: &#123;&#125;", id, symbol);

❻【强制】对于 trace/debug/info 级别的日志输出,必须进行日志级别的开关判断。

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

正例:

// 如果判断为真,那么可以输出 trace 和 debug 级别的日志
if (logger.isDebugEnabled()) &#123;
    logger.debug("Current ID is: &#123;&#125; and name is: &#123;&#125;", id, getName());
&#125;

❼【强制】避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置 additivity=false

正例:

<logger name="com.taobao.dubbo.config" additivity="false">

❽ 【强制】生产环境禁止直接使用 System.outSystem.err 输出日志或使用e.printStackTrace() 打印异常堆栈 。

说明:标准日志输出与标准错误输出文件每次Jboss重启时才滚动,如果大量输出送往这两个文件,容易造成文件大小超过操作系统大小限制。

❾ 【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字 throws 往上抛出。

正例:

logger.error("inputParams:&#123;&#125; and errorMessage:&#123;&#125;", 各类参数或者对象 toString(), e.getMessage(), e);

❿ 【强制】日志打印时禁止直接用 JSON 工具将对象转换成 String

说明:如果对象里某些 get 方法被覆写,存在抛出异常的情况,则可能会因为打印日志而影响正常业务流程的执行。

正例:

打印日志时仅打印出业务相关属性值或者调用其对象的 toString() 方法。

⓫ 【推荐】谨慎地记录日志。生产环境禁止输出 debug 日志;有选择地输出 info 日志;

如果使用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。

说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?

⓬ 【推荐】可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。

说明:如非必要,请不要在此场景打出 error 级别,避免频繁报警。 注意日志输出的级别,error 级别只记录系统逻辑出错、异常或者重要的错误信息。

⓭ 【推荐】尽量用英文来描述日志错误信息,如果日志中的错误信息用英文描述不清楚的话使用中文描述即可,否则容易产生歧义。

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

本期内容到这儿就结束了 ★,°:.☆( ̄▽ ̄)/$:.°★ 。 希望对您有所帮助

我们下期再见 ヾ(•ω•`)o (●’◡’●)


文章作者: xiezhr
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 xiezhr !
评论
  目录