轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java

    • 核心

    • 并发

    • 经验

    • JVM

    • 企业应用

      • Freemarker模板实战指南
      • Servlet与JSP指南
      • Java日志系统
        • 引言
          • 1. 日志系统的重要性
          • 为什么需要日志系统
          • 日志系统的发展历程
          • 2. 日志系统的基本功能
          • 核心功能
          • 高级功能
        • Java内置日志工具:Java Util Logging(JUL)
          • 1. 日志记录器(Logger)
          • 创建Logger
          • Logger层级结构
          • 2. 日志级别
          • 标准日志级别
          • 特殊级别
          • 使用示例
          • 3. 日志处理器(Handler)
          • 内置Handler
          • 使用内置Handler
          • 创建自定义Handler
          • 4. 日志格式化器
          • 5. 日志管理器
          • 6. JUL的优点和缺点
        • Apache Commons Logging(JCL)
          • JCL的设计理念
          • 为什么不推荐使用JCL
        • 简单日志门面:SLF4J
          • 1. SLF4J的概念和基本架构
          • SLF4J架构设计
          • 核心组件
          • 2. SLF4J的主要特性
          • 参数化日志(最重要特性)
          • 其他核心特性
          • 3. SLF4J的配置和使用
          • Maven依赖配置
          • 代码使用示例
          • SLF4J日志级别
          • 4. SLF4J的特点
        • Apache Log4j
        • Logback
          • 1. 不同的Appender与日志滚动
          • 2. 滚动策略
          • 3. MDC
        • Apache Log4j2
          • Log4j2的核心优势
          • Maven依赖配置
          • 配置文件示例
          • 基础配置(log4j2.xml)
          • 异步日志配置
          • 代码使用示例
        • 主流日志库的性能对比
          • 性能基准测试结果
          • 性能优化建议
        • 桥接与适配
        • 实战案例:Spring Boot项目日志配置
          • 使用Logback的完整配置
          • application.yml配置
        • 常见问题与解决方案
          • 1. 日志框架冲突
          • 2. 日志文件过大
          • 3. 敏感信息泄露
          • 4. 日志性能问题
        • 最佳实践总结
          • 选择建议
          • 配置原则
          • 编码规范
        • 总结
      • Java JSON处理
      • Java XML处理
      • Java对象池技术
      • Java程序的单元测试(Junit)
      • Thymeleaf官方文档(中文版)
      • Mockito应用
      • Java中的空安全
  • Spring

  • 其他语言

  • 工具

  • 后端
  • Java
  • 企业应用
轩辕李
2023-05-28
目录

Java日志系统

# 引言

# 1. 日志系统的重要性

日志系统是软件开发中至关重要的组成部分之一,它记录了软件运行时的关键信息,包括错误、警告、调试信息和其他有用的运行时数据。

# 为什么需要日志系统

  • 故障排查:快速定位和解决生产环境中的问题,通过日志追踪错误发生的上下文和堆栈信息
  • 性能监控:记录关键业务操作的执行时间,识别性能瓶颈和优化点
  • 业务审计:记录用户操作轨迹,满足合规性要求和安全审计需求
  • 数据分析:收集用户行为数据,为产品优化和决策提供数据支持
  • 系统健康度:实时监控系统运行状态,及时发现和预防潜在问题

# 日志系统的发展历程

Java日志系统经历了多个阶段的演进:

  1. 早期阶段(1996-2002):使用 System.out.println() 进行简单调试
  2. JUL时代(2002):Java 1.4引入 java.util.logging,提供标准日志API
  3. Log4j崛起(1999-2012):Apache Log4j成为事实标准,提供强大的配置能力
  4. 门面模式(2005):JCL和SLF4J出现,解决日志框架绑定问题
  5. 现代框架(2012-至今):Logback和Log4j2提供更高性能和更多特性

# 2. 日志系统的基本功能

日志系统通常提供以下基本功能:

# 核心功能

  1. 记录日志:将关键信息记录到日志文件或其他持久化存储介质中,支持多种输出格式(文本、JSON、XML等)
  2. 日志级别控制:通过设置日志级别,控制记录哪些级别的日志信息
    • ERROR:错误事件,应用程序仍可继续运行
    • WARN:警告信息,潜在的有害情况
    • INFO:信息性消息,粗粒度级别上的应用程序进度
    • DEBUG:细粒度的调试信息
    • TRACE:更细粒度的调试信息

# 高级功能

  1. 日志格式化:支持自定义日志格式,包括时间戳、日志级别、线程信息、类名、方法名、行号等
  2. 日志过滤:基于级别、包名、类名等多维度过滤日志
  3. 日志输出:支持多种输出目标
    • 控制台输出(Console)
    • 文件输出(File)
    • 数据库存储(Database)
    • 远程日志服务(Syslog、Kafka等)
  4. 日志滚动:自动管理日志文件
    • 基于文件大小滚动
    • 基于时间滚动(每日、每小时等)
    • 文件数量限制
  5. 日志归档:压缩和存储历史日志文件
  6. 异步日志:提高应用性能,避免日志操作阻塞业务逻辑
  7. 上下文信息:MDC(Mapped Diagnostic Context)和NDC(Nested Diagnostic Context)支持

# Java内置日志工具:Java Util Logging(JUL)

Java Util Logging(JUL)是Java平台自带的日志工具,在Java 1.4版本中引入,作为Java的默认日志系统,无需引入额外的依赖。虽然功能相对简单,但对于小型项目或简单应用来说已经足够。

# 1. 日志记录器(Logger)

java.util.logging.Logger 是JUL的核心类,负责记录应用程序消息。Logger采用层级结构,类似于Java包的命名空间。

# 创建Logger

import java.util.logging.Logger;

public class MyClass {
    // 推荐:使用类名作为Logger名称
    private static final Logger logger = Logger.getLogger(MyClass.class.getName());
    
    // 或者使用自定义名称
    private static final Logger customLogger = Logger.getLogger("com.example.custom");
}

# Logger层级结构

Logger遵循父子层级关系,子Logger会继承父Logger的配置:

// 根Logger
Logger rootLogger = Logger.getLogger("");

// com.example的Logger是com.example.service的父Logger
Logger parentLogger = Logger.getLogger("com.example");
Logger childLogger = Logger.getLogger("com.example.service");

# 2. 日志级别

java.util.logging.Level 定义了JUL的日志级别。日志级别按严重程度从高到低排列:

# 标准日志级别

级别 数值 用途 对应其他框架
SEVERE 1000 严重错误,可能导致程序终止 ERROR
WARNING 900 警告信息,潜在问题 WARN
INFO 800 重要的业务信息 INFO
CONFIG 700 配置信息 -
FINE 500 调试信息(粗粒度) DEBUG
FINER 400 调试信息(中粒度) DEBUG
FINEST 300 调试信息(细粒度) TRACE

# 特殊级别

  • OFF (Integer.MAX_VALUE):关闭所有日志
  • ALL (Integer.MIN_VALUE):记录所有日志

# 使用示例

import java.util.logging.Level;
import java.util.logging.Logger;

public class LogLevelExample {
    private static final Logger logger = Logger.getLogger(LogLevelExample.class.getName());
    
    public static void main(String[] args) {
        // 设置日志级别
        logger.setLevel(Level.INFO);
        
        // 不同级别的日志记录
        logger.severe("严重错误:数据库连接失败");
        logger.warning("警告:内存使用超过80%");
        logger.info("信息:用户登录成功");
        logger.config("配置:加载配置文件完成");
        logger.fine("调试:进入processOrder方法");
        logger.finer("详细调试:订单ID=" + orderId);
        logger.finest("极详细调试:SQL=" + sql);
        
        // 条件日志记录(性能优化)
        if (logger.isLoggable(Level.FINE)) {
            logger.fine("复杂对象:" + expensiveToString());
        }
    }
}

注意:日志将只记录级别等于或高于Logger设置级别的消息。例如,如果设置为 INFO,则只记录 INFO、WARNING 和 SEVERE 级别的日志。

# 3. 日志处理器(Handler)

Handler负责将日志消息输出到指定目标。一个Logger可以添加多个Handler,实现日志的多路输出。

# 内置Handler

JUL提供了几种内置的Handler:

Handler类型 用途 默认格式
ConsoleHandler 输出到控制台(System.err) SimpleFormatter
FileHandler 输出到文件 XMLFormatter
SocketHandler 输出到网络Socket XMLFormatter
MemoryHandler 缓存日志消息 -
StreamHandler 输出到任意OutputStream SimpleFormatter

# 使用内置Handler

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

public class HandlerExample {
    private static final Logger logger = Logger.getLogger(HandlerExample.class.getName());
    
    public static void main(String[] args) throws IOException {
        // 移除默认Handler
        logger.setUseParentHandlers(false);
        
        // 添加ConsoleHandler
        ConsoleHandler consoleHandler = new ConsoleHandler();
        consoleHandler.setLevel(Level.INFO);
        consoleHandler.setFormatter(new SimpleFormatter());
        logger.addHandler(consoleHandler);
        
        // 添加FileHandler
        FileHandler fileHandler = new FileHandler("app.log", true);
        fileHandler.setLevel(Level.ALL);
        fileHandler.setFormatter(new XMLFormatter());
        logger.addHandler(fileHandler);
        
        // 记录日志
        logger.info("同时输出到控制台和文件");
    }
}

# 创建自定义Handler

要创建自定义Handler,需要继承 Handler 类或其子类:

package com.journaldev.log;

import java.util.logging.LogRecord;
import java.util.logging.StreamHandler;

public class MyHandler extends StreamHandler {

    @Override
    public void publish(LogRecord record) {
        // 添加自定义逻辑以发布日志记录
        super.publish(record);
    }


    @Override
    public void flush() {
        super.flush();
    }


    @Override
    public void close() throws SecurityException {
        super.close();
    }

}

# 4. 日志格式化器

格式化器用于格式化日志消息。Java日志记录API提供了两种可用的格式化器。

  • SimpleFormatter:此格式化器生成带有基本信息的文本消息。ConsoleHandler使用此格式化器类将日志消息打印到控制台。
  • XMLFormatter:此格式化器为日志生成XML消息,FileHandler使用XMLFormatter作为默认格式化器。

我们可以通过扩展java.util.logging.Formatter类创建自定义的格式化器类,并将其附加到任何处理器上。以下是简单自定义格式化器类的示例。

package com.journaldev.log;

import java.util.Date;
import java.util.logging.Formatter;
import java.util.logging.LogRecord;

public class MyFormatter extends Formatter {

    @Override
    public String format(LogRecord record) {
        return record.getThreadID() + "::" + record.getSourceClassName() + "::"
                + record.getSourceMethodName() + "::"
                + new Date(record.getMillis()) + "::"
                + record.getMessage() + "\n";
    }

}

# 5. 日志管理器

java.util.logging.LogManager是读取日志配置、创建和维护日志记录器实例的类。我们可以使用此类设置我们自己的应用程序特定配置。

LogManager.getLogManager().readConfiguration(new FileInputStream("mylogging.properties"));

以下是Java日志记录API配置文件的示例。如果我们没有指定任何配置,它将从JRE Home lib/logging.properties文件中读取。

mylogging.properties

handlers= java.util.logging.ConsoleHandler

.level= FINE

# 默认文件输出位于用户的主目录中。
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter

# 将打印在控制台上的消息限制为INFO及以上级别。
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

com.journaldev.files = SEVERE

以下是一个显示Java中Logger用法的简单Java程序。

package com.journaldev.log;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.logging.ConsoleHandler;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;

public class LoggingExample {

    static Logger logger = Logger.getLogger(LoggingExample.class.getName());

    public static void main(String[] args) {
        try {
            LogManager.getLogManager().readConfiguration(new FileInputStream("mylogging.properties"));
        } catch (SecurityException | IOException e1) {
            e1.printStackTrace();
        }
        logger.setLevel(Level.FINE);
        logger.addHandler(new ConsoleHandler());
        // 添加自定义处理器
        logger.addHandler(new MyHandler());
        try {
            // FileHandler文件名具有最大大小和日志文件限制数
            Handler fileHandler = new FileHandler("/Users/pankaj/tmp/logger.log", 2000, 5);
            fileHandler.setFormatter(new MyFormatter());
            // 为FileHandler设置自定义过滤器
            fileHandler.setFilter(new MyFilter());
            logger.addHandler(fileHandler);

            for (int i = 0; i < 1000; i++) {
                // 记录日志消息
                logger.log(Level.INFO, "Msg" + i);
            }
            logger.log(Level.CONFIG, "Config data");
        } catch (SecurityException | IOException e) {
            e.printStackTrace();
        }
    }

}

当您运行上述Java日志记录器示例程序时,您会注意到CONFIG日志不会打印到文件中,这是因为MyFilter类的设置。

package com.journaldev.log;

import java.util.logging.Filter;
import java.util.logging.Level;
import java.util.logging.LogRecord;

public class MyFilter implements Filter {

    @Override
    public boolean isLoggable(LogRecord log) {
        // 不记录CONFIG级别的日志到文件中
        if (log.getLevel() == Level.CONFIG)
            return false;
        return true;
    }

}

此外,输出格式将与MyFormatter类定义的格式相同。

1::com.journaldev.log.LoggingExample::main::Sat Dec 15 01:42:43 PST 2012::Msg977
1::com.journaldev.log.LoggingExample::main::Sat Dec 15 01:42:43 PST 2012::Msg978
1::com.journaldev.log.LoggingExample::main::Sat Dec 15 01:42:43 PST 2012::Msg979
1::com.journaldev.log.LoggingExample::main::Sat Dec 15 01:42:43 PST 2012::Msg980

如果我们没有将自定义的Formatter类添加到FileHandler中,日志消息将以以下方式打印。

<record>
  <date>2012-12-14T17:03:13</date>
  <millis>1355533393319</millis>
  <sequence>996</sequence>
  <logger>com.journaldev.log.LoggingExample</logger>
  <level>INFO</level>
  <class>com.journaldev.log.LoggingExample</class>
  <method>main</method>
  <thread>1</thread>
  <message>Msg996</message>
</record>

控制台日志消息的格式如下:

Dec 15, 2012 1:42:43 AM com.journaldev.log.LoggingExample main
INFO: Msg997
Dec 15, 2012 1:42:43 AM com.journaldev.log.LoggingExample main
INFO: Msg998
Dec 15, 2012 1:42:43 AM com.journaldev.log.LoggingExample main
INFO: Msg998

下图显示了最终的Java Logger示例项目:
image

您可以从链接下载该项目。

# 6. JUL的优点和缺点

JUL的优点包括:

  • 内置于Java平台,无需额外依赖。
  • 简单易用,适合快速开发和小规模项目。
  • 与Java平台紧密集成,无需额外学习成本。
  • 配置灵活,可以通过配置文件或代码进行配置。

然而,JUL也有一些缺点:

  • 功能相对有限,不支持一些高级特性,如异步日志写入、动态日志级别修改等。
  • 配置相对繁琐,需要通过配置文件或代码进行配置。
  • 缺乏一些常见日志系统的特性和扩展性。

由于JUL的限制和缺点,许多开发人员更倾向于使用第三方的日志系统,如Log4j、Logback,以满足更复杂的日志需求。这些日志系统提供了更多的功能和扩展性,并广泛应用于Java开发领域。

# Apache Commons Logging(JCL)

Apache Commons Logging(JCL,原名Jakarta Commons Logging)是Apache提供的日志门面框架,发布于2002年。它是第一个流行的日志抽象层,旨在解决不同日志实现之间的耦合问题。

# JCL的设计理念

JCL采用动态绑定机制,在运行时自动发现并绑定日志实现:

  1. 首先查找用户配置的日志实现
  2. 其次查找Log4j
  3. 然后尝试JUL
  4. 最后使用内置的SimpleLog

# 为什么不推荐使用JCL

尽管JCL曾经很流行,但现在已不推荐使用,主要原因:

  • 类加载问题:动态发现机制在复杂的类加载环境(如应用服务器)中容易出现问题
  • 性能开销:运行时查找日志实现带来额外开销
  • 功能有限:相比SLF4J,缺少参数化日志等现代特性
  • 维护停滞:项目更新缓慢,社区已转向SLF4J

建议:新项目应直接使用SLF4J作为日志门面,它提供了更好的性能和更丰富的功能。

# 简单日志门面:SLF4J

# 1. SLF4J的概念和基本架构

Simple Logging Facade for Java(SLF4J)是目前最流行的Java日志门面框架,由Log4j的作者Ceki Gülcü创建。它提供了简单、高效的日志抽象层,解决了不同日志框架之间的兼容性问题。

# SLF4J架构设计

应用程序
    ↓
SLF4J API (slf4j-api.jar)
    ↓
绑定层 (Binding)
    ↓
具体实现 (Logback/Log4j2/JUL等)

# 核心组件

  1. Logger(日志记录器)

    • 应用程序使用的主要接口
    • 通过 LoggerFactory.getLogger() 获取实例
    • 提供各种日志级别的记录方法
  2. LoggerFactory(日志工厂)

    • 负责创建Logger实例
    • 在类加载时绑定具体实现
    • 采用静态绑定机制,避免运行时查找
  3. Binding(绑定)

    • 连接SLF4J API和具体实现
    • 编译时依赖,运行时绑定
    • 一个应用只能有一个绑定

# 2. SLF4J的主要特性

# 参数化日志(最重要特性)

SLF4J的参数化日志避免了字符串拼接的性能开销:

// 传统方式(性能差)
logger.debug("User " + userId + " logged in at " + timestamp);

// SLF4J参数化(性能好)
logger.debug("User {} logged in at {}", userId, timestamp);

// 多参数支持
logger.info("Order {} for user {} with amount {} processed", 
            orderId, userId, amount);

# 其他核心特性

  • 延迟求值:只有日志级别启用时才会构造日志消息
  • MDC支持:提供线程安全的诊断上下文
  • 标记(Marker):为日志添加额外的过滤维度
  • 流式API(SLF4J 2.0+):支持链式调用
  • 事件API(SLF4J 2.0+):支持结构化日志

# 3. SLF4J的配置和使用

# Maven依赖配置

<!-- SLF4J API -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.9</version>
</dependency>

<!-- 选择一个绑定实现(以下选其一) -->

<!-- 选项1:Logback(推荐) -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.4.11</version>
</dependency>

<!-- 选项2:Log4j2 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j2-impl</artifactId>
    <version>2.21.1</version>
</dependency>

<!-- 选项3:JUL -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jdk14</artifactId>
    <version>2.0.9</version>
</dependency>

# 代码使用示例

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;

public class SLF4JExample {
    private static final Logger logger = LoggerFactory.getLogger(SLF4JExample.class);
    private static final Marker SECURITY = MarkerFactory.getMarker("SECURITY");
    
    public void demonstrateFeatures() {
        // 基本日志记录
        logger.info("Application started");
        
        // 参数化日志
        String username = "john";
        logger.debug("User {} attempting login", username);
        
        // 多参数
        logger.info("User {} performed {} at {}", username, "LOGIN", new Date());
        
        // 异常日志
        try {
            riskyOperation();
        } catch (Exception e) {
            logger.error("Operation failed for user {}", username, e);
        }
        
        // 使用Marker
        logger.warn(SECURITY, "Suspicious activity detected from user {}", username);
        
        // 使用MDC
        MDC.put("userId", "12345");
        MDC.put("requestId", UUID.randomUUID().toString());
        logger.info("Processing user request");
        MDC.clear();
        
        // 条件日志(性能优化)
        if (logger.isDebugEnabled()) {
            logger.debug("Complex debug info: {}", generateExpensiveDebugInfo());
        }
    }
}

# SLF4J日志级别

级别 用途 使用场景
ERROR 错误事件 异常、系统故障、数据丢失
WARN 警告信息 性能问题、使用废弃API、可恢复的错误
INFO 重要信息 系统启动/停止、配置信息、业务流程节点
DEBUG 调试信息 变量值、执行流程、SQL语句
TRACE 详细追踪 方法入参/返回值、循环细节

# 4. SLF4J的特点

SLF4J的特点包括:

  • 提供了统一的日志API,方便开发人员在应用程序中使用不同的日志实现。
  • 轻量级,对应用程序的性能影响较小。
  • 易于配置和使用,与多个日志实现框架兼容。

注意:SLF4J本身并不是一个日志实现框架,而是一个日志门面系统,需要与具体的日志实现框架(如Log4j2、Logback)配合使用。

# Apache Log4j

Log4j是Apache的一个开源项目,是一个功能强大的日志组件,可以灵活地进行日志配置,支持多种日志级别,支持多种输出目的地,支持自定义日志格式,支持多种配置方式,支持多种过滤器等。

Log4j可以作为JCL和SLF4J的实现,但本身性能较差,已经不再维护,推荐使用Logback和Log4j2。

# Logback

Logback是一个强大的日志框架,被认为是Log4j的继任者。它提供了高度可配置的日志功能,并具有出色的性能。

logback-classic是SLF4J的一个标准实现,可以轻松地记录和管理日志信息。

下面是一个简单的Logback使用示例:

  1. 引入依赖:在项目的构建文件中引入Logback的依赖(如Maven或Gradle)。示例中使用Maven:
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>
  1. 配置Logback:创建一个Logback的配置文件,通常命名为logback.xml,并放置在类路径下。示例配置文件如下:
<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

上述配置创建了一个名为CONSOLE的ConsoleAppender,定义了日志输出的格式。然后,将CONSOLE Appender添加到根Logger中,并设置日志级别为INFO。

  1. 在代码中使用Logback:在需要记录日志的类中,通过SLF4J获取Logger实例,并使用不同级别的方法记录日志。示例代码如下:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyClass {
    private static final Logger logger = LoggerFactory.getLogger(MyClass.class);
    
    public void doSomething() {
        logger.info("Doing something...");
        logger.error("An error occurred!");
    }
}

在示例中,使用LoggerFactory.getLogger()方法获取Logger实例,并传入当前类的Class作为参数。然后,使用logger的info()和error()方法记录相应级别的日志信息。

# 1. 不同的Appender与日志滚动

<configuration>
    <!-- Console Appender -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
        <filter class="ch.qos.logback.core.filter.LevelFilter">
            <level>WARN</level>
            <onMatch>DENY</onMatch>
        </filter>
    </appender>
    
    <!-- File Appender -->
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>logs/application.log</file>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <!-- Rolling File Appender -->
    <appender name="ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/application.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <root level="INFO">
        <!-- Use both Console and File appenders -->
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
    </root>
    
    <logger name="com.example" level="DEBUG">
        <!-- Use Rolling File appender for com.example package -->
        <appender-ref ref="ROLLING_FILE" />
    </logger>
</configuration>

在上述示例中,我们定义了三个不同的Appender:

  1. Console Appender:将日志输出到控制台,这里使用了过滤器来限制输出级别。

  2. File Appender:将日志输出到指定的文件(application.log)。

  3. Rolling File Appender:将日志输出到文件,并支持日志文件的滚动和归档。

在根Logger中,我们使用了<appender-ref>指令将Console和File appenders添加到根Logger中,以便同时输出日志到控制台和文件。

此外,我们还针对com.example包设置了一个专门的Logger,并将Rolling File appender添加到该Logger中。这意味着该Logger下的日志将单独输出到Rolling File appender指定的文件。

# 2. 滚动策略

下面是RollingFileAppender的几种用法示例:

1. 基于时间的滚动(TimeBasedRollingPolicy)示例:

<configuration>
    <appender name="ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>/path/to/logs/app-%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

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

上述示例中,我们使用TimeBasedRollingPolicy配置了RollingFileAppender。日志文件的命名模式为每天一个文件,使用%d{yyyy-MM-dd}表示日期部分,例如"app-2023-05-30.log"。<maxHistory>指定了保留的日志文件历史天数。

2. 基于文件大小的滚动(SizeBasedTriggeringPolicy)示例:

<configuration>
    <appender name="ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
            <fileNamePattern>/path/to/logs/app.%i.log</fileNamePattern>
            <minIndex>1</minIndex>
            <maxIndex>10</maxIndex>
        </rollingPolicy>
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <maxFileSize>10MB</maxFileSize>
        </triggeringPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

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

上述示例中,我们使用SizeBasedTriggeringPolicy来触发日志文件的滚动。当日志文件的大小达到指定的大小(10MB)时,将滚动到下一个文件。FixedWindowRollingPolicy用于限制滚动的文件数量,保留10个日志文件。

3. 组合滚动策略(CompositeTriggeringPolicy)示例:

<configuration>
    <appender name="ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>/path/to/logs/app-%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <triggeringPolicy class="ch.qos.logback.core.rolling.CompositeTriggeringPolicy">
            <policies>
                <sizeBasedTriggeringPolicy>
                    <maxFileSize>10MB</maxFileSize>
                </sizeBasedTriggeringPolicy>
                <timeBasedTriggeringPolicy />
            </policies>
        </triggeringPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

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

上述示例中,我们使用CompositeTriggeringPolicy来组合多个滚动策略。在这个示例中,我们同时使用了SizeBasedTriggeringPolicy和TimeBasedRollingPolicy。当日志文件的大小达到指定的大小(10MB)或者按照时间规则触发滚动时,都会滚动到下一个文件。

# 3. MDC

当我们在应用程序中记录日志时,有时我们需要关联一些上下文信息,例如请求ID、用户ID、会话ID等。这种关联上下文信息的需求可以通过使用MDC(Mapped Diagnostic Context)来实现。

MDC是SLF4J(Simple Logging Facade for Java)提供的一个功能,它允许我们在应用程序的不同线程中存储和访问上下文信息。MDC使用一个类似于Map的结构来存储键值对,其中键是上下文的名称,值是对应的上下文信息。

以下是MDC的基本用法:

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

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

    public void doSomething() {
        MDC.put("username", "john_doe");
        logger.info("Doing something...");
        // 在使用完MDC后,应该及时清除MDC中的值,以防止潜在的内存泄漏。
        MDC.clear();
    }
}

在日志事件中,我们可以通过在日志消息中使用MDC占位符来引用MDC的值。例如,使用%X{key}来引用名为"key"的MDC值。

使用MDC可以帮助我们将上下文信息与特定的日志事件关联起来,从而更好地追踪和分析日志。例如,在多线程环境中,我们可以在请求开始时设置请求ID,并在整个请求处理过程中记录该请求ID。这样,我们就可以根据请求ID轻松地筛选和分析与特定请求相关的日志。

如果我们想要在日志消息中显示MDC的值,需要相应地修改PatternLayout,例如:

<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} [%X{username}] - %msg%n</pattern>

在这种情况下,输出的日志消息将类似于:

2023-05-30 10:30:15 [main] INFO  com.example.MyClass [john_doe] - Doing something...

在这个日志消息中,我们可以看到MDC的值 "john_doe" 在方括号内与日志消息一起显示。这样,我们就可以将上下文信息与日志事件关联起来,并在日志中进行显示。

# Apache Log4j2

Apache Log4j2是Apache软件基金会提供的新一代高性能日志框架,完全重写了Log4j 1.x。它不仅修复了Log4j 1.x的架构问题,还提供了许多现代化的特性。

# Log4j2的核心优势

  1. 异步日志:基于LMAX Disruptor实现,性能提升10倍以上
  2. 无垃圾模式:减少GC压力,适合低延迟应用
  3. 插件架构:所有组件都是插件,易于扩展
  4. 丰富的过滤器:支持复杂的日志过滤逻辑
  5. 自动重载配置:无需重启应用即可更新配置

# Maven依赖配置

<!-- Log4j2核心 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.21.1</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.21.1</version>
</dependency>

<!-- 异步日志支持(可选但推荐) -->
<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.4.4</version>
</dependency>

<!-- SLF4J桥接(如果使用SLF4J) -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j2-impl</artifactId>
    <version>2.21.1</version>
</dependency>

# 配置文件示例

# 基础配置(log4j2.xml)

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
    <Properties>
        <Property name="LOG_PATTERN">
            %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
        </Property>
        <Property name="LOG_DIR">logs</Property>
    </Properties>
    
    <Appenders>
        <!-- 控制台输出 -->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Filters>
                <ThresholdFilter level="INFO"/>
            </Filters>
        </Console>
        
        <!-- 文件输出 -->
        <RollingFile name="RollingFile" 
                     fileName="${LOG_DIR}/app.log"
                     filePattern="${LOG_DIR}/app-%d{yyyy-MM-dd}-%i.log.gz">
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="1"/>
                <SizeBasedTriggeringPolicy size="100MB"/>
            </Policies>
            <DefaultRolloverStrategy max="30"/>
        </RollingFile>
        
        <!-- 错误日志单独输出 -->
        <RollingFile name="ErrorFile" 
                     fileName="${LOG_DIR}/error.log"
                     filePattern="${LOG_DIR}/error-%d{yyyy-MM-dd}.log.gz">
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Filters>
                <ThresholdFilter level="ERROR"/>
            </Filters>
            <Policies>
                <TimeBasedTriggeringPolicy interval="1"/>
            </Policies>
        </RollingFile>
        
        <!-- 异步Appender -->
        <Async name="AsyncAppender">
            <AppenderRef ref="RollingFile"/>
        </Async>
    </Appenders>
    
    <Loggers>
        <!-- 特定包的日志级别 -->
        <Logger name="com.example.dao" level="DEBUG" additivity="false">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="RollingFile"/>
        </Logger>
        
        <!-- 根Logger -->
        <Root level="INFO">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="AsyncAppender"/>
            <AppenderRef ref="ErrorFile"/>
        </Root>
    </Loggers>
</Configuration>

# 异步日志配置

<!-- 全异步模式:在log4j2.component.properties中设置 -->
<!-- Log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector -->

<Configuration>
    <Appenders>
        <RollingFile name="AsyncFile" 
                     fileName="logs/async.log"
                     filePattern="logs/async-%d{yyyy-MM-dd}.log">
            <PatternLayout>
                <Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
            </PatternLayout>
            <Policies>
                <TimeBasedTriggeringPolicy/>
            </Policies>
        </RollingFile>
    </Appenders>
    
    <Loggers>
        <!-- 混合异步模式 -->
        <AsyncLogger name="com.example" level="INFO">
            <AppenderRef ref="AsyncFile"/>
        </AsyncLogger>
        
        <Root level="INFO">
            <AppenderRef ref="AsyncFile"/>
        </Root>
    </Loggers>
</Configuration>

# 代码使用示例

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.MarkerManager;
import org.apache.logging.log4j.ThreadContext;

public class Log4j2Example {
    private static final Logger logger = LogManager.getLogger(Log4j2Example.class);
    private static final Marker SQL_MARKER = MarkerManager.getMarker("SQL");
    
    public void demonstrateFeatures() {
        // 基本日志
        logger.info("Application started");
        
        // 参数化日志(避免字符串拼接)
        logger.debug("Processing user: {}", userId);
        
        // 使用Marker
        logger.info(SQL_MARKER, "Executing query: {}", sql);
        
        // 使用ThreadContext(类似MDC)
        ThreadContext.put("userId", "12345");
        ThreadContext.put("requestId", UUID.randomUUID().toString());
        logger.info("Processing request");
        ThreadContext.clearAll();
        
        // Lambda延迟求值(Log4j2 2.4+)
        logger.debug("Result: {}", () -> expensiveOperation());
        
        // 结构化日志(使用Message)
        logger.info(new MapMessage()
            .with("event", "user_login")
            .with("userId", userId)
            .with("timestamp", System.currentTimeMillis()));
    }
}

# 主流日志库的性能对比

# 性能基准测试结果

image

基于 官方基准测试 (opens new window) 的结果显示:

日志框架 同步吞吐量 异步吞吐量 延迟(P99) 内存占用
Log4j2 (Async) ~180万/秒 ~1800万/秒 <1ms 低
Logback ~150万/秒 ~500万/秒 2-5ms 中
Log4j 1.x ~50万/秒 - 10-20ms 高
JUL ~40万/秒 - 15-30ms 低

# 性能优化建议

  1. 使用异步日志

    • Log4j2: 使用AsyncLogger或AsyncAppender
    • Logback: 使用AsyncAppender
  2. 避免日志中的字符串拼接

    // 错误示例
    logger.debug("User " + user.getName() + " logged in");
    
    // 正确示例
    logger.debug("User {} logged in", user.getName());
    
  3. 使用条件日志

    if (logger.isDebugEnabled()) {
        logger.debug("Debug info: {}", expensiveOperation());
    }
    
  4. 合理设置日志级别

    • 生产环境:INFO或WARN
    • 测试环境:DEBUG
    • 开发环境:DEBUG或TRACE

# 桥接与适配

在Spring Boot官方文档 (opens new window)中,有这么一段话:

Spring Boot将Commons Logging用于所有内部日志记录。为Java Util Logging、Log4j2和Logback提供了默认配置。在每种情况下,记录器都被预先配置为使用控制台输出,可选的文件输出也可用。
默认情况下,使用Logback进行日志记录。还包括适当的Logback路由,以确保使用Java Util Logging、Commons Logging、Log4J或SLF4J的依赖库都能正常工作。

Spring Boot默认使用了Logback作为日志框架,同时保证了对其他日志框架的兼容性。这是如何实现的呢?

查看spring-boot-starter-logging,我们发现它依赖了jul-to-slf4j(org.slf4j)和log4j-to-slf4j(org.apache.logging.log4j)。

这些包都是为了实现不同日志框架之间的互操作性而存在的。它们是SLF4J(Simple Logging Facade for Java)提供的桥接器,用于连接不同日志框架的API和SLF4J的API,以便统一使用SLF4J作为日志门面接口。

SLF4J提供了以下桥接器:

  1. jcl-over-slf4j:将Apache Common Logging(JCL)的API转发到SLF4J,通过使用jcl-over-slf4j桥接器,开发人员可以在应用程序中继续使用JCL的API,但实际日志将由SLF4J进行记录。
  2. jul-to-slf4j:将Java Util Logging(JUL)的API转发到SLF4J。它允许开发人员使用JUL的API进行日志记录,但实际日志将由SLF4J进行输出。
  3. log4j-over-slf4j:将Log4j的API转发到SLF4J。开发人员可以使用Log4j的API进行日志记录,但实际日志将由SLF4J进行输出。这个桥接器允许在现有的Log4j应用程序中逐步迁移到SLF4J。

此外,还提供了一些适配器:

  1. slf4j-jcl:提供了JCL的SLF4J适配器,使得通过SLF4J的API使用JCL进行日志记录成为可能。开发人员可以使用SLF4J的API,但实际日志将由JCL进行记录。
  2. slf4j-jdk14:提供了JDK 1.4 Logger的SLF4J适配器,使得通过SLF4J的API使用JUL进行日志记录成为可能。开发人员可以使用SLF4J的API,但实际日志将由JUL进行记录。
  3. slf4j-log4j12:提供了Log4j 1.x的SLF4J适配器,使得通过SLF4J的API使用Log4j进行日志记录成为可能。开发人员可以使用SLF4J的API,但实际日志将由Log4j进行输出。

这些适配器或桥接器的存在使得开发人员可以使用统一的SLF4J API,并且可以在应用程序中使用不同的日志框架,而无需修改现有的日志记录代码。这为日志系统的切换、升级和统一提供了方便。

# 实战案例:Spring Boot项目日志配置

# 使用Logback的完整配置

<!-- src/main/resources/logback-spring.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 引入Spring Boot默认配置 -->
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    
    <!-- 定义变量 -->
    <property name="LOG_PATH" value="${LOG_PATH:-logs}"/>
    <property name="APP_NAME" value="myapp"/>
    
    <!-- 控制台输出(开发环境) -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>
    
    <!-- 按日期和大小滚动的文件输出 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/${APP_NAME}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/${APP_NAME}-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
            <maxFileSize>100MB</maxFileSize>
            <maxHistory>30</maxHistory>
            <totalSizeCap>3GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <!-- 异步输出 -->
    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <queueSize>512</queueSize>
        <appender-ref ref="FILE"/>
    </appender>
    
    <!-- Spring Profile配置 -->
    <springProfile name="dev">
        <root level="DEBUG">
            <appender-ref ref="CONSOLE"/>
        </root>
    </springProfile>
    
    <springProfile name="prod">
        <root level="INFO">
            <appender-ref ref="ASYNC"/>
        </root>
    </springProfile>
    
    <!-- 第三方库日志级别 -->
    <logger name="org.springframework" level="INFO"/>
    <logger name="org.hibernate" level="WARN"/>
    <logger name="com.zaxxer.hikari" level="INFO"/>
</configuration>

# application.yml配置

logging:
  level:
    root: INFO
    com.example: DEBUG
    org.springframework.web: DEBUG
    org.hibernate.SQL: DEBUG
  file:
    name: logs/application.log
    max-size: 100MB
    max-history: 30
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
    file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

# 常见问题与解决方案

# 1. 日志框架冲突

问题:项目中存在多个日志实现导致冲突

<!-- 解决方案:排除冲突的依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <exclusions>
        <exclusion>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>

# 2. 日志文件过大

问题:日志文件快速增长占满磁盘

<!-- 解决方案:配置滚动策略和文件大小限制 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
    <fileNamePattern>logs/app-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
    <maxFileSize>100MB</maxFileSize>
    <maxHistory>7</maxHistory>
    <totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>

# 3. 敏感信息泄露

问题:日志中包含密码、密钥等敏感信息

// 解决方案:使用自定义转换器脱敏
public class MaskingPatternLayout extends PatternLayout {
    private static final Pattern PASSWORD_PATTERN = 
        Pattern.compile("password[=\":\\s]+([^\\s\"]+)");
    
    @Override
    public String doLayout(ILoggingEvent event) {
        String message = super.doLayout(event);
        Matcher matcher = PASSWORD_PATTERN.matcher(message);
        if (matcher.find()) {
            message = matcher.replaceAll("password=******");
        }
        return message;
    }
}

# 4. 日志性能问题

问题:日志操作影响应用性能

// 解决方案1:使用异步日志
// Log4j2: 配置AsyncLogger
// Logback: 使用AsyncAppender

// 解决方案2:条件日志
if (logger.isDebugEnabled()) {
    logger.debug("Complex object: {}", complexObject.toString());
}

// 解决方案3:使用Lambda延迟求值(Log4j2)
logger.debug("Result: {}", () -> expensiveComputation());

# 最佳实践总结

# 选择建议

  1. 新项目:使用 SLF4J + Logback 或 SLF4J + Log4j2
  2. 高性能要求:使用 Log4j2 的异步模式
  3. 简单应用:使用 JUL 足够
  4. Spring Boot项目:默认的 Logback 配置即可

# 配置原则

  1. 开发环境:DEBUG级别,输出到控制台
  2. 测试环境:INFO级别,输出到文件和控制台
  3. 生产环境:WARN级别,异步输出到文件,配置告警

# 编码规范

  1. 使用参数化日志避免字符串拼接
  2. 合理使用日志级别
  3. 避免在循环中记录日志
  4. 异常日志要包含完整堆栈
  5. 使用MDC记录请求上下文

# 总结

Java日志系统经历了从简单到复杂、从单一到多样的发展过程。在实际项目中,建议:

  1. 统一使用SLF4J作为日志门面,便于切换底层实现
  2. 根据项目特点选择合适的日志实现:
    • 一般项目使用Logback
    • 高性能需求使用Log4j2
    • 简单项目使用JUL
  3. 重视日志配置,合理设置级别、输出格式和滚动策略
  4. 关注日志性能,必要时使用异步日志
  5. 建立日志规范,统一团队的日志使用方式

选择合适的日志系统并正确使用,不仅能提高开发效率,还能在生产环境中快速定位和解决问题,是构建可维护系统的重要基础。

祝你变得更强!

编辑 (opens new window)
#Java日志#Java Loggin
上次更新: 2025/08/15
Servlet与JSP指南
Java JSON处理

← Servlet与JSP指南 Java JSON处理→

最近更新
01
AI时代的编程心得
09-11
02
Claude Code与Codex的协同工作
09-01
03
Claude Code实战之供应商切换工具
08-18
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式