大家好,我是
晓凡

一、日志概念

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

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

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

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

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软件基金会开发,相比
Log4j

Log4j2
在性能上有显著提升,同时保持了丰富的功能,支持异步日志处理,适合高性能需求的场景

  • Logback

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

三、JUL日志框架

3.1 主要组件

  1. Logger
    :日志记录器,是日志系统的核心,用来生成日志记录。
  2. Handler
    :日志处理器,负责将日志信息输出到不同的目的地,比如控制台、文件等。可以为每个Logger配置一个或多个
    Handler
  3. Formatter
    :日志格式化器,负责定义日志的输出格式。比如时间戳、日志级别、消息等。
  4. Level
    :设置日志级别,常见的级别有
    SEVERE

    WARNING

    INFO

    CONFIG

    FINE

    FINER

    FINEST
    等。
  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获取
    :首先获取一个名为
    LoggingExample

    Logger
    实例。
  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
String

format
方法

String

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

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(){

    //初始化日志配置信息,不需要配置文件
    BasicConfigurator.configure();
    //获取日志记录器
    Logger logger = Logger.getLogger(Log4jTest.class);
    //通过各种日志级别打印日志
    logger.fatal("这是一条致命的信息");  // 严重错误,一般会造成系统崩溃
    logger.error("这是一条错误的信息");  // 出现错误时,比如出错了但是不影响系统继续运行
    logger.warn("这是一条警告的信息");   // 警告级别,比如要告警的时候
    logger.info("这是一条普通的信息");  // 一般信息,比如记录普通的方法执行
    logger.debug("这是一条调试的信息"); // 调试信息,比如调试的时候打印的信息
    logger.trace("这是一条追踪的信息");  // 追踪信息,比如追踪程序运行路径
}
//输出
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(){
    //初始化日志配置信息,不需要配置文件
    BasicConfigurator.configure();
    //获取日志记录器
    Logger logger = Logger.getLogger(Log4jTest.class);
    Layout patternLayout = new PatternLayout("%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n");// 将自定义的Layout应用到控制台Appender上
    ConsoleAppender consoleAppender = new ConsoleAppender(patternLayout);
    logger.addAppender(consoleAppender);
    // 记录日志
    logger.info("这是一条自定义格式的日志信息");

}
//输出
2024-08-04 13:55:35 [INFO] - Log4jTest.testLog4jLayout(Log4jTest.java:44) - 这是一条自定义格式的日志信息
占位符 说明
%m 输出代码中指定的日志信息
%p 输出优先级
%n 换行符
%r 输出自应用启用到输出log信息消耗的毫秒数
%c 输出语句所属的类全名
%t 输出线程全名
%d 输出服务器当前时间,%d
%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

    等等

日志管理器源码

# 指定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{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n

或者

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%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{yyyy-MM-dd HH:mm:ss} [%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{yyyy-MM-dd HH:mm:ss} [%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{yyyy-MM-dd HH:mm:ss} [%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(){
    //初始化日志配置信息,不需要配置文件
    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{yyyy-MM-dd HH:mm:ss}','%p','%c','%F','%t','%L','%l','%m')");

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

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

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

五、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(){

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

    log.error("这是一条error");
    log.warn("这是一条warn");
    log.info("这是一条info");
    log.debug("这是一条debug");
    log.trace("这是一条trace");
}

日志输出

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{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n

③ 测试日志输出

@Test
public void testJclLog4j(){

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

日志输出如下:

log4j日志输出

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

六、SLF4j日志门面

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

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

log4j
等使用

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(){
    //打印日志
    logger.error("这是error日志");
    logger.warn("这是warn日志");
    logger.info("这是info日志");
    logger.debug("这是debug日志");
    logger.trace("这是trace日志");

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

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

}

上面代码输出日志如下

日志输出

6.2 SLF4j 日志绑定功能

6.2.1 日志绑定原理

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

logback

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

一脸懵逼

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

slf4j实现原理图

  • 只导入日志门面,没导入日志实现,不会进行日志输出
  • logback

    simplelog

    no-operation
    框架遵循
    SLF4j
    规范 导入jar包即可使用
  • log4j

    JUL
    属于比较古老日志框架,不遵循
    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{yyyy-MM-dd HH:mm:ss} [%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
等老的日志框架,但你想迁移到使用
SLF4J

API
,这时候你可以使用
SLF4J

Log4j 1.x
桥接器来平滑过渡

6.3.2 桥接原理

桥接器原理

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

jul-to-slf4j

jcl-over-slf4j

就可以实现在
不改变原有代码
的情况下,将日志从
log4j

jul

jcl
迁移到
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 {

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

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

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的类型和参数等。配置可以通过
    XML

    JSON
    或者
    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{yyyy-MM-dd HH:mm:ss.SSS} %c [%thread]%-5level %msg%n" />

    <!--输出到控制台-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--引用上面配置好的pattern属性-->
            <pattern>${pattern}</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{yyyy-MM-dd HH:mm:ss.SSS} %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>${pattern}</pattern>
        </encoder>
        <!--被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。-->
        <file>${log_file}/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{yyyy-MM-dd HH:mm:ss}%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>${pattern}</pattern>
            </layout>
        </encoder>
        <!--被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。-->
        <file>${log_file}/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{yyyy-MM-dd HH:mm:ss} %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>${pattern}</pattern>
        </encoder>
        <!--被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。-->
        <file>${log_file}/roll_logback.log</file>
        <!--滚动记录文件:根据时间来制定滚动策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <fileNamePattern>${log_file}/roll_logback.%d{yyyy-MM-dd}.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(){

        //日志输出
        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("欢迎关注晓凡,持续输出干货!!!!!");
    }

输出结果:

未加异步日志

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

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/

Log4j2

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

主要体现在以下几个方面

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

    Logback
    高出10倍

  • 异步日志

    Log4j2
    支持异步日志记录,可以通过
    AsyncAppender

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

  • 自动重载配置

    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 {

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

    }
}

日志输出结果如下

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 {
	//这里我们换成了slf4j的门面接口
    private static final Logger logger = LoggerFactory.getLogger(Log4j2Test.class);

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

    }
}

日志输出效果

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">
    <!--集中配置属性进行管理    使用时通过:${name}  -->
    <properties>
        <property name="LOG_HOME">D:/logs</property>
    </properties>
    <!--日志处理 -->
    <Appenders>
        <!--控制台输出 appender,SYSTEM_OUT输出黑色,SYSTEM_ERR输出红色 -->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n" />
        </Console>
        <!--日志文件输出 appender -->
        <File name="file"  fileName="${LOG_HOME}/file.log">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %m%n" />
        </File>
        <!-- 使用随机读写流的日志文件输出 appender,性能提高 -->
        <RandomAccessFile name="accessFile" fileName="${LOG_HOME}/access.log">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %m%n" />
        </RandomAccessFile>
        <!--按照一定规则拆分的日志文件的appender -->
        <!-- 拆分后的文件 -->
        <!-- 拆分后的日志文件命名规则:log-debug.log、log-info.log、log-error.log -->
        <RollingFile name="rollingFile" fileName="${LOG_HOME}/rolling.log"
                     filePattern="${LOG_HOME}/$${date:yyyy-MM-dd}/rolling-%d{yyyy-MM-dd}-%i.log.gz">
            <!-- 日志级别过滤器 -->
            <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY" />
            <!-- 日志消息格式 -->
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %msg%n" />
            <Policies>
                <!-- 在系统启动时,出发拆分规则,生产一个新的日志文件 -->
                <OnStartupTriggeringPolicy  />
                <!-- 按照文件大小拆分,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)

性能简直将
Logback

Log4j
日志框架甩了一条街。

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

日志框架性能比较

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">
    <!--集中配置属性进行管理    使用时通过:${name}    -->
    <properties>
        <property name="LOG_HOME">D:/logs</property>
    </properties>
    <!--日志处理 -->
    <Appenders>
        <!--控制台输出 appender,SYSTEM_OUT输出黑色,SYSTEM_ERR输出红色 -->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n" />
        </Console>
        <!--日志文件输出 appender -->
        <File name="file" fileName="${LOG_HOME}/file.log">
            <!-- <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %m%n" />-->
            <PatternLayout pattern="%d %p %c{1.} [%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开发手册中的日志规约

❶【
强制
】应用中不可直接使用日志系统(
Log4j

Logback
)中的
API
,而应依赖使用日志框架(
SLF4J

JCL--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 字符串的拼接会使用
StringBuilder

append()
方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。

正例:

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

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

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

正例:

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

❼【
强制
】避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置
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
级别只记录系统逻辑出错、异常或者重要的错误信息。

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

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

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

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

标签: none

添加新评论