日志是软件开发中的关键工具,用于调试、监控和分析应用程序的行为。在 Java 生态系统中,从早期的 Log4j 到现代的 SLF4JLogback,日志框架不断演进以适应开发者的需求。本文将详细介绍 Java 日志体系的历史、框架分类、SLF4J 的运行原理以及 Logback 的配置与使用。


Java 日志体系的历史

Java 日志体系的发展是一个逐步迭代和优化的过程:

  1. 1996 年:Log4j

    • 由 Ceki Gülcü 开发,最初是一个项目内部的追踪工具。
    • 后来成为 Apache 基金会的一部分,并迅速成为 Java 社区的事实标准。
  2. 2002 年:JUL (Java Util Logging)

    • Java 1.4 引入,是 Sun 官方日志实现。
    • 由于 Log4j 已经成熟,JUL 的采纳率不如预期。
  3. 2006 年:SLF4JLogback

    • Ceki 离开 Apache 后创建了这两个项目。
    • SLF4J 是日志门面,Logback 是高性能实现。
  4. 2012 年:Log4j2

  • Apache 重写了 Log4j,加入了异步日志处理和更多现代化特性。

这些框架共同构成了 Java 日志体系的发展轨迹。

日志框架的分类与选型

日志框架主要分为两类:

  1. 门面型日志框架

    • 提供统一的接口,支持切换底层实现。
    • 代表框架:SLF4J 和 JCL
      • SLF4J 是目前的主流选择,具有简单、灵活的特点。
  2. 记录型日志框架

    • 提供具体的日志实现。
    • 代表框架:Log4j、Logback 和 JUL

日志框架的作用

  • 控制日志输出内容与格式。
  • 管理日志输出位置,如控制台、文件等。
  • 支持异步操作、日志归档和压缩。
  • 提供灵活的日志级别控制。

框架选型建议

  • 新项目:推荐 SLF4J + Logback
  • 遗留系统:可通过桥接器逐步迁移至现代方案。

日志的使用实例

使用 Log4j

早期项目中通常使用 Log4j 直接记录日志,代码示例如下:

1
2
3
4
5
6
7
8
9
10
import org.apache.log4j.Logger;

public class Test {
private static final Logger logger = Logger.getLogger(Test.class);

public static void main(String[] args) {
logger.info("信息日志");
logger.error("错误日志");
}
}

使用 JUL

JUL 是 Java 自带的日志框架,使用方式如下:

1
2
3
4
5
6
7
8
9
10
import java.util.logging.Logger;

public class Test {
private static final Logger logger = Logger.getLogger(Test.class.getName());

public static void main(String[] args) {
logger.info("信息日志");
logger.warning("警告日志");
}
}

使用 JCL

JCL 提供了一种抽象方式,可以在运行时选择具体的日志实现:

1
2
3
4
5
6
7
8
9
10
11
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class Test {
private static final Log log = LogFactory.getLog(Test.class);

public static void main(String[] args) {
log.info("信息日志");
log.debug("调试日志");
}
}

使用 SLF4J

SLF4J 提供统一的接口,结合现代实现框架(如 Logback):

1
2
3
4
5
6
7
8
9
10
11
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Test {
private static final Logger logger = LoggerFactory.getLogger(Test.class);

public static void main(String[] args) {
logger.info("信息日志");
logger.debug("调试日志");
}
}

SLF4J快速开始

为什么使用 SLF4J?

  1. 日志实现的松耦合:通过 SLF4J,应用程序不需要直接依赖具体的日志实现,可以根据需求切换不同的日志框架。
  2. 统一接口:简化了开发者面对多种日志库的学习成本,提供了标准化的日志 API。
  3. 性能优化:通过延迟字符串拼接(使用占位符),避免了不必要的性能开销。

添加依赖

SLF4J 本身只提供接口,需搭配具体的日志实现才能正常工作。如 slf4j-nop.jar slf4j-simple.jar, slf4j-log4j12.jar, slf4j-jdk14.jar or logback-classic.jar 中的任意一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependencies>
<!-- SLF4J API -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>

<!-- 日志实现,例如 Logback -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.11</version>
</dependency>
</dependencies>

SLF4J 的基本使用

SLF4J 的核心接口是 org.slf4j.Loggerorg.slf4j.LoggerFactory。以下是常见的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Slf4jExample {
private static final Logger logger = LoggerFactory.getLogger(Slf4jExample.class);

public static void main(String[] args) {
logger.info("This is an INFO message");
logger.debug("This is a DEBUG message");
logger.error("This is an ERROR message");
}
}

占位符替换

LF4J 提供了占位符 {} 替换机制,避免字符串拼接的性能开销。如果日志级别被设置为 WARN 或更高,logger.info() 的占位符会被跳过,避免了字符串拼接造成的性能浪费。

1
2
3
String user = "Alice";
logger.info("User {} has logged in", user);

异常信息记录

SLF4J 支持直接记录异常堆栈信息。

1
2
3
4
5
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
logger.error("An exception occurred: {}", e.getMessage(), e);
}
1
2
3
4
An exception occurred: / by zero
java.lang.ArithmeticException: / by zero
at Slf4jExample.main(Slf4jExample.java:10)

动态绑定日志实现

SLF4J 支持动态绑定不同的日志实现,只需引入相应的依赖即可。如果需要切换到 Log4j,只需替换依赖为:

1
2
3
4
5
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.20.0</version>
</dependency>

日志上下文

某些日志实现(如 Logback)支持使用 MDC(Mapped Diagnostic Context)或 NDC(Nested Diagnostic Context)来记录上下文信息。

配置日志输出格式:

1
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} [userId=%X{userId}] - %msg%n</pattern>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

public class Slf4jMdcExample {
private static final Logger logger = LoggerFactory.getLogger(Slf4jMdcExample.class);

public static void main(String[] args) {
MDC.put("userId", "12345");
logger.info("User action executed");
MDC.clear();
}
}

// 2025-01-01 12:00:00 [main] INFO Slf4jMdcExample [userId=12345] - User action executed

延迟日志(Lambda 表达式)

在 SLF4J 2.x 中,可以使用 Lambda 表达式延迟生成日志内容。

1
2
3
4
5
6
7
8
9
10
11
// 如果当前日志级别低于 DEBUG,performExpensiveComputation() 不会被执行,提升性能。
logger.debug("Expensive computation result: {}", () -> performExpensiveComputation());

public String performExpensiveComputation() {
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "Expensive Result";
}

直接输出与延迟日志的区别

  • 直接输出是指在日志方法调用之前就完成了日志消息的生成,即使日志最终不会被输出(例如当前日志级别不符合条件),也会执行字符串拼接或复杂的计算操作。

    1
    2
    3
    4
    String result = performExpensiveComputation();
    logger.debug("Computation result: " + result);
    // 性能问题:即使当前日志级别高于 DEBUG,performExpensiveComputation() 仍然会执行,浪费 1 秒时间。。
    // 如果 performExpensiveComputation() 是一个耗时操作(如数据库查询、大量数据计算),会造成性能浪费。
  • 延迟日志使用 Lambda 表达式,将日志消息的生成推迟到真正需要输出时才执行。

    1. logger.debug() 方法会先检查当前日志级别是否允许输出。
    2. 如果日志级别不符合(如当前级别为 INFO),Lambda 表达式 () -> performExpensiveComputation() 不会被执行。
    3. 只有当日志级别符合条件(DEBUG 或更低级别)时,才会调用 Lambda 表达式,并执行 performExpensiveComputation()
    1
    2
    logger.debug("Computation result: {}", () -> performExpensiveComputation());

SLF4j与日志依赖

SLF4J+SLF4J自带的简单实现

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.7</version>
</dependency>

SLF4J+logback

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.3.11</version>
</dependency>

只留下logback,那么slf4j门面使用的就是logback日志实现。值得一提的是,这一次没有多余的提示信息,所以在实际应用的时候,我们一般情况下,仅仅只是做一种日志实现的集成就可以了。

SLF4J+nop

使用slf4j-nop,表示不记录日志。这个实现依赖与logback和simple是属于一类的,通过集成依赖的顺序而定,所以如果想要让nop发挥效果,禁止所有日志的打印,那么就必须要将slf4j-nop的依赖放在所有日志实现依赖的上方。

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>slf4j-nop</artifactId>
<version>2.0.7</version>
</dependency>

SLF4J+log4j

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<!-- 导入log4j适配器依赖 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

加入配置 log4j.properties配置文件。

1
2
3
4
5
6
7
8
# Root logger option
log4j.rootLogger=INFO, stdout

# Direct log messages to stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n

SLF4J+JUL

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>2.0.7</version>
</dependency>

SLF4J+logback+slf4j-simple

在真实生产环境中,slf4j只要绑定一个日志实现框架就可以了,如果绑定多个日志实现,默认使用导入依赖的第一个,而且会产生没有必要的警告信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.3.11</version>
</dependency>

所以此时虽然集成了logback,可以看到提示你包含了多个SLF4J的绑定,并且此时实际的绑定是SimpleLogger。

可以调整导入的位置,先导入logback-classic,此时实际绑定的是logback

SLF4J 的设计理念与运行原理

SLF4J (Simple Logging Facade for Java) 是一种日志门面框架,其主要目标是为 Java 应用提供统一的日志接口,同时支持灵活的底层日志实现替换。

核心功能

  • 统一接口:抽象日志操作,避免直接依赖具体实现。

  • 桥接与适配:支持将其他日志框架(如 Log4j 和 JUL)的输出重定向至 SLF4J。最上层表示桥接层,下层表示具体的实现层,中间是接口层。

    ![](1-全面解析 Java 日志体系/3ca7cd611cacf1aeb2a50ade833d8413.png)

运行原理

SLF4J 通过以下方式工作:

  1. LoggerFactory 创建日志记录器:LoggerFactory 会通过 StaticLoggerBinder 绑定具体的日志实现。

    1
    Logger logger = LoggerFactory.getLogger(MyClass.class);
  2. 绑定实现

    • 在编译时,SLF4J 会查找 org/slf4j/impl/StaticLoggerBinder.class,并加载对应的日志实现(如 Logback)。

      1
      2
      3
      4
      5
      6
      public class StaticLoggerBinder {
      public static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
      public ILoggerFactory getLoggerFactory() {
      return new LogbackLoggerFactory();
      }
      }
  3. 桥接器与适配器

    • 桥接器(Bridge):将其他日志框架的输出重定向到 SLF4J。

      • 例如:log4j-over-slf4jjul-to-slf4jjcl-over-slf4j
    • 适配器(Adapter):连接 SLF4J 和具体实现。

      • 例如:slf4j-log4j12slf4j-jdk14
  4. 避免冲突的注意事项

    • 同一项目中只能存在一个具体实现,否则可能导致冲突。错误配置:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>log4j-over-slf4j</artifactId>
      <version>2.0.7</version>
      </dependency>
      <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>2.0.7</version>
      </dependency>

使用桥接器统一日志

假设项目中有模块 A 使用 Log4j,模块 B 使用 JUL,需要统一到 SLF4J

  1. 添加桥接器依赖:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>2.0.7</version>
    </dependency>
    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jul-to-slf4j</artifactId>
    <version>2.0.7</version>
    </dependency>
  2. 配置具体实现(如 Logback):

    1
    2
    3
    4
    5
    <dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.3.11</version>
    </dependency>
  3. 初始化桥接器(针对 JUL):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import org.slf4j.bridge.SLF4JBridgeHandler;

    public class LogSetup {
    static {
    // 清除了 JUL 的根记录器(root logger)上的所有默认处理器,避免重复日志输出。
    SLF4JBridgeHandler.removeHandlersForRootLogger();
    // 为 JUL 的根记录器安装一个新的处理器,该处理器会将 JUL 的日志事件重定向到 SLF4J。
    SLF4JBridgeHandler.install();
    }
    }

这样,所有日志都将通过 SLF4J 统一输出到 Logback。

Logback 的配置详解

Logback 是 SLF4J 的默认实现,具有高性能和丰富的特性。其配置主要通过 logback.xml 文件完成。

配置文件结构

Logback 的配置通常由 XML 配置文件控制,文件名为 logback.xml,放置在类路径中。Logback 配置文件的结构由以下几个部分组成:

  • 日志级别:指定日志的输出级别。
  • Appender:日志输出的目标,Logback 支持多种类型的输出方式(如控制台、文件、数据库等)。
  • Logger:用于实际记录日志的组件,日志记录器可以配置为不同的日志级别。
  • Encoder:日志消息格式化的方式。

配置文件详解

Logback 配置文件的核心是 <configuration> 元素,它包含多个子元素,如 <appender><logger><root> 等。

  1. appender

    • 定义日志输出方式,如控制台或文件。
  2. logger

    • 为特定包或类设置日志级别和输出。
  3. root

    • 定义默认的日志输出。

一个简单的 logback.xml 配置文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>

<!-- 控制台日志输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern>
</encoder>
</appender>

<!-- 文件日志输出 -->
<appender name="file" class="ch.qos.logback.core.FileAppender">
<file>logs/app.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern>
</encoder>
</appender>

<!-- 设置日志级别为 DEBUG -->
<logger name="com.example" level="DEBUG"/>

<!-- 配置 root logger -->
<root level="INFO">
<appender-ref ref="console"/>
<appender-ref ref="file"/>
</root>

</configuration>

关键元素解析

  • <configuration>:配置文件的根元素。

  • <appender>:定义日志输出目标。Logback 支持不同类型的 Appender,比如:

    • ConsoleAppender:输出到控制台。
    • FileAppender:输出到文件。
    • RollingFileAppender:输出到滚动文件(用于文件分割)。

    每个 Appender 可以设置多个参数,如输出格式(<encoder>)和文件路径(<file>)。

  • <logger>:定义一个日志记录器。每个日志记录器有一个名称,可以指定其输出级别。通常,Logger 使用包名作为其名称。通过设置 level 属性,可以控制该 Logger 的日志级别。

  • <root>:定义根日志记录器(Root Logger),它控制默认的日志级别和输出目标。根日志记录器是最上层的记录器,所有未明确配置的日志记录器都会继承该设置。

日志级别

Logback 支持以下日志级别(按优先级递增):

  • TRACE:最详细的日志级别,用于开发过程中调试和诊断。
  • DEBUG:用于开发过程中的调试信息。
  • INFO:用于一般的信息输出,常用于生产环境中。
  • WARN:警告信息,可能不影响程序运行,但需要注意。
  • ERROR:错误信息,通常表示程序发生了异常或错误。
  • OFF:关闭所有日志。

日志滚动策略

RollingFileAppender 可以在文件达到一定大小时自动进行日志滚动。上述配置将日志按日期和大小滚动存储,每个文件的最大大小为 10MB,最多保存 30 天的日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<appender name="rollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 滚动日志文件的命名模式 -->
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 单个日志文件的最大大小 -->
<maxFileSize>10MB</maxFileSize>
<!-- 保存的历史日志文件数量 -->
<maxHistory>30</maxHistory>
<!-- 日志文件最大存储总大小 -->
<totalSizeCap>2GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>

日志输出的格式

logback 中,encoder.pattern 用于定义日志输出的格式。常见的配置项包括日志的时间、日志级别、日志信息、线程名等。以下是一些常用的配置项及其含义:

  1. %d{yyyy-MM-dd HH:mm:ss}
  • 含义:表示日志事件的时间。yyyy-MM-dd HH:mm:ss 是日期和时间的格式,可以根据需要自定义格式。
  • 示例2025-01-01 12:34:56
  1. %thread
  • 含义:表示输出日志的线程名。
  • 示例main
  1. %level
  • 含义:表示日志级别(例如:DEBUGINFOWARNERROR)。
  • 示例INFO
  1. %logger,%logger{36}:打印记录器的名称,最多 36 个字符
  • 含义:表示日志记录器的名称,通常是类名。
  • 示例com.example.MyClass
  1. %msg
  • 含义:表示日志消息的内容,即调用日志方法时传入的消息。
  • 示例Something went wrong
  1. %n
  • 含义:表示换行符。通常用于日志输出后进行换行。
  • 示例:换行
  1. %M
  • 含义:表示调用日志方法的类方法名。
  • 示例main(方法名)
  1. %file
  • 含义:表示日志所在的源文件名。
  • 示例MyClass.java
  1. %line
  • 含义:表示日志调用所在的代码行号。
  • 示例42
  1. %replace
  • 含义:用于替换日志消息中的某些内容,可以进行正则替换。
  • 示例%replace(%msg){'foo':'bar'} 将日志消息中的 foo 替换为 bar
  1. %ex
  • 含义:表示异常信息(如果有的话)。
  • 示例java.lang.NullPointerException
  1. %logger{length}
  • 含义:表示日志记录器的名称,length 指定输出的最小字符长度。如果日志名称短于这个长度,会进行截取。
  • 示例%logger{15} 输出长度为 15 的日志记录器名称。
  1. %contextName
  • 含义:表示日志上下文的名称。
  • 示例myApp
  1. %X{key}
  • 含义:表示从 MDC(Mapped Diagnostic Context)中获取键为 key 的值,MDC 是用于存储和传递日志上下文信息的工具。
  • 示例%X{userId} 输出当前线程中 userId 的值。
  1. %uuid
  • 含义:表示日志事件的唯一标识符。
  • 示例6bcfc019-d208-4960-a9ea-88d970124b45

Logback高级配置

AsyncAppender 异步日志输出

AsyncAppender 可用于提高日志记录性能,通过异步方式将日志写入目标文件。

  • queueSize:指定日志队列的大小。

  • discardingThreshold:控制在队列接近满时丢弃的日志级别。

1
2
3
4
5
<appender name="async" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>500</queueSize> <!-- 日志队列大小 -->
<discardingThreshold>0</discardingThreshold> <!-- 丢弃日志的阈值 -->
<appender-ref ref="file"/>
</appender>

SMTPAppender 邮件通知

当系统发生严重错误时,可以通过 SMTPAppender 发送电子邮件通知。

  • smtpHostsmtpPort:邮件服务器的地址和端口。
  • usernamepassword:邮件服务器的登录信息。
  • filter:用于指定只有 ERROR 级别的日志会触发邮件发送。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<appender name="email" class="ch.qos.logback.classic.net.SMTPAppender">
<smtpHost>smtp.example.com</smtpHost>
<smtpPort>587</smtpPort>
<username>your_email@example.com</username>
<password>your_password</password>
<to>admin@example.com</to>
<from>no-reply@example.com</from>
<subject>Critical Error Alert</subject>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>

日志上下文变量

通过 <property> 标签设置变量,可以让配置文件更灵活。

1
2
3
4
5
6
7
8
9
10
11
12
<configuration>
<!-- 设置变量 -->
<property name="logDir" value="logs"/>
<property name="appName" value="MyApp"/>

<appender name="file" class="ch.qos.logback.core.FileAppender">
<file>${logDir}/${appName}.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern>
</encoder>
</appender>
</configuration>

在实际应用中,可以通过 -D 系统属性传递变量:

1
java -DlogDir=/var/logs -DappName=MyApp -jar app.jar

动态日志级别调整

Logback 支持在运行时动态调整日志级别,方便调试和排查问题。

  1. 在配置文件中启用 JMX 支持:

    1
    2
    3
    4
    <configuration>
    <jmxConfigurator/>
    ...
    </configuration>
  2. 通过 JMX 工具(如 JConsole)可以动态调整日志级别。启动jconsole, 连接应用,选择MBean,找到ch.qos.logback.classic.default目录下的类ch.qos.logback.classic.jmx.JMXConfigurator,选择操作,选择setLoggerLevel,输入参数,第一个是loggerName,第二个是loggerLevel,点击按钮

基于环境的日志配置

通过条件判断实现不同环境的日志配置,比如开发环境和生产环境。,运行应用时通过 -Denv=dev-Denv=prod 指定环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<configuration>
<if condition='property("env") == "dev"'>
<then>
<root level="DEBUG">
<appender-ref ref="console"/>
</root>
</then>
</if>
<if condition='property("env") == "prod"'>
<then>
<root level="INFO">
<appender-ref ref="file"/>
</root>
</then>
</if>
</configuration>

过滤器 (Filters)

过滤器允许对日志进行更精细的控制。

LevelFilter 是一种简单的过滤器,用于按日志级别控制日志的输出。

1
2
3
4
5
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch> <!-- 匹配成功时的动作 -->
<onMismatch>DENY</onMismatch> <!-- 匹配失败时的动作 -->
</filter>

阈值过滤器 (ThresholdFilter) , eg: 只输出级别大于等于 WARN 的日志。

1
2
3
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>

正则过滤器 (RegexFilter): 匹配包含特定关键字的日志消息。

1
2
3
4
5
6
7
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator class="ch.qos.logback.classic.boolex.JaninoEventEvaluator">
<expression>return message.contains("specialKeyword");</expression>
</evaluator>
<OnMatch>ACCEPT</OnMatch>
<OnMismatch>DENY</OnMismatch>
</filter>

EvaluatorFilter 是 Logback 提供的高级过滤器,支持基于条件表达式动态控制日志的输出。

EvaluatorFilter 中,不支持直接定义多个 <expression> 标签,可以配置多个 EvaluatorFilter, 可以通过在单个 <expression> 中组合多个条件来实现复杂逻辑,使用 Java 的逻辑运算符(如 &&||)即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator class="ch.qos.logback.classic.boolex.JaninoEventEvaluator">
<!-- 自定义条件表达式 -->
<expression>return logger.equals("com.example.MyClass") && level.toString().equals("ERROR");</expression>
<!-- 过滤包含 "CriticalError" 的 ERROR 日志 -->
<expression>return level.toString().equals("ERROR") && message.contains("CriticalError");</expression>
<!-- 组合多个条件 -->
<expression>
return (logger.equals("com.example.MyClass") && level.toString().equals("ERROR"))
|| message.contains("CriticalError");
</expression>
</evaluator>
<OnMatch>ACCEPT</OnMatch>
<OnMismatch>DENY</OnMismatch>
</filter>

JSON 格式化日志

对于需要将日志以 JSON 格式输出的场景,可以使用第三方库如 logstash-logback-encoder

1
2
3
4
5
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>

配置 JSON Encoder

1
2
3
4
5
6
7
8
9
10
11
12
<appender name="jsonFile" class="ch.qos.logback.core.FileAppender">
<file>logs/json.log</file>
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp/>
<level/>
<loggerName/>
<message/>
<context/>
</providers>
</encoder>
</appender>

总结

Java 日志体系通过不断演化,提供了多种解决方案以满足不同场景的需求。从 SLF4J 的灵活接口到 Logback 的高性能实现,开发者可以根据项目需求选择合适的框架和配置方式。在实际开发中,建议优先采用现代化的日志方案,如 SLF4J + Logback,以获得最佳性能和维护性。