Java日志系统
# 引言
# 1. 日志系统的重要性
日志系统是软件开发中至关重要的组成部分之一,它记录了软件运行时的关键信息,包括错误、警告、调试信息和其他有用的运行时数据。
# 为什么需要日志系统
- 故障排查:快速定位和解决生产环境中的问题,通过日志追踪错误发生的上下文和堆栈信息
- 性能监控:记录关键业务操作的执行时间,识别性能瓶颈和优化点
- 业务审计:记录用户操作轨迹,满足合规性要求和安全审计需求
- 数据分析:收集用户行为数据,为产品优化和决策提供数据支持
- 系统健康度:实时监控系统运行状态,及时发现和预防潜在问题
# 日志系统的发展历程
Java日志系统经历了多个阶段的演进:
- 早期阶段(1996-2002):使用
System.out.println()
进行简单调试 - JUL时代(2002):Java 1.4引入
java.util.logging
,提供标准日志API - Log4j崛起(1999-2012):Apache Log4j成为事实标准,提供强大的配置能力
- 门面模式(2005):JCL和SLF4J出现,解决日志框架绑定问题
- 现代框架(2012-至今):Logback和Log4j2提供更高性能和更多特性
# 2. 日志系统的基本功能
日志系统通常提供以下基本功能:
# 核心功能
- 记录日志:将关键信息记录到日志文件或其他持久化存储介质中,支持多种输出格式(文本、JSON、XML等)
- 日志级别控制:通过设置日志级别,控制记录哪些级别的日志信息
ERROR
:错误事件,应用程序仍可继续运行WARN
:警告信息,潜在的有害情况INFO
:信息性消息,粗粒度级别上的应用程序进度DEBUG
:细粒度的调试信息TRACE
:更细粒度的调试信息
# 高级功能
- 日志格式化:支持自定义日志格式,包括时间戳、日志级别、线程信息、类名、方法名、行号等
- 日志过滤:基于级别、包名、类名等多维度过滤日志
- 日志输出:支持多种输出目标
- 控制台输出(Console)
- 文件输出(File)
- 数据库存储(Database)
- 远程日志服务(Syslog、Kafka等)
- 日志滚动:自动管理日志文件
- 基于文件大小滚动
- 基于时间滚动(每日、每小时等)
- 文件数量限制
- 日志归档:压缩和存储历史日志文件
- 异步日志:提高应用性能,避免日志操作阻塞业务逻辑
- 上下文信息: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示例项目:
您可以从链接下载该项目。
# 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采用动态绑定机制,在运行时自动发现并绑定日志实现:
- 首先查找用户配置的日志实现
- 其次查找Log4j
- 然后尝试JUL
- 最后使用内置的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等)
# 核心组件
Logger(日志记录器)
- 应用程序使用的主要接口
- 通过
LoggerFactory.getLogger()
获取实例 - 提供各种日志级别的记录方法
LoggerFactory(日志工厂)
- 负责创建Logger实例
- 在类加载时绑定具体实现
- 采用静态绑定机制,避免运行时查找
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使用示例:
- 引入依赖:在项目的构建文件中引入Logback的依赖(如Maven或Gradle)。示例中使用Maven:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
- 配置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。
- 在代码中使用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:
Console Appender:将日志输出到控制台,这里使用了过滤器来限制输出级别。
File Appender:将日志输出到指定的文件(application.log)。
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的核心优势
- 异步日志:基于LMAX Disruptor实现,性能提升10倍以上
- 无垃圾模式:减少GC压力,适合低延迟应用
- 插件架构:所有组件都是插件,易于扩展
- 丰富的过滤器:支持复杂的日志过滤逻辑
- 自动重载配置:无需重启应用即可更新配置
# 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()));
}
}
# 主流日志库的性能对比
# 性能基准测试结果
基于 官方基准测试 (opens new window) 的结果显示:
日志框架 | 同步吞吐量 | 异步吞吐量 | 延迟(P99) | 内存占用 |
---|---|---|---|---|
Log4j2 (Async) | ~180万/秒 | ~1800万/秒 | <1ms | 低 |
Logback | ~150万/秒 | ~500万/秒 | 2-5ms | 中 |
Log4j 1.x | ~50万/秒 | - | 10-20ms | 高 |
JUL | ~40万/秒 | - | 15-30ms | 低 |
# 性能优化建议
使用异步日志
- Log4j2: 使用AsyncLogger或AsyncAppender
- Logback: 使用AsyncAppender
避免日志中的字符串拼接
// 错误示例 logger.debug("User " + user.getName() + " logged in"); // 正确示例 logger.debug("User {} logged in", user.getName());
使用条件日志
if (logger.isDebugEnabled()) { logger.debug("Debug info: {}", expensiveOperation()); }
合理设置日志级别
- 生产环境: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提供了以下桥接器:
- jcl-over-slf4j:将Apache Common Logging(JCL)的API转发到SLF4J,通过使用jcl-over-slf4j桥接器,开发人员可以在应用程序中继续使用JCL的API,但实际日志将由SLF4J进行记录。
- jul-to-slf4j:将Java Util Logging(JUL)的API转发到SLF4J。它允许开发人员使用JUL的API进行日志记录,但实际日志将由SLF4J进行输出。
- log4j-over-slf4j:将Log4j的API转发到SLF4J。开发人员可以使用Log4j的API进行日志记录,但实际日志将由SLF4J进行输出。这个桥接器允许在现有的Log4j应用程序中逐步迁移到SLF4J。
此外,还提供了一些适配器:
- slf4j-jcl:提供了JCL的SLF4J适配器,使得通过SLF4J的API使用JCL进行日志记录成为可能。开发人员可以使用SLF4J的API,但实际日志将由JCL进行记录。
- slf4j-jdk14:提供了JDK 1.4 Logger的SLF4J适配器,使得通过SLF4J的API使用JUL进行日志记录成为可能。开发人员可以使用SLF4J的API,但实际日志将由JUL进行记录。
- 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());
# 最佳实践总结
# 选择建议
- 新项目:使用
SLF4J + Logback
或SLF4J + Log4j2
- 高性能要求:使用
Log4j2
的异步模式 - 简单应用:使用
JUL
足够 - Spring Boot项目:默认的
Logback
配置即可
# 配置原则
- 开发环境:DEBUG级别,输出到控制台
- 测试环境:INFO级别,输出到文件和控制台
- 生产环境:WARN级别,异步输出到文件,配置告警
# 编码规范
- 使用参数化日志避免字符串拼接
- 合理使用日志级别
- 避免在循环中记录日志
- 异常日志要包含完整堆栈
- 使用MDC记录请求上下文
# 总结
Java日志系统经历了从简单到复杂、从单一到多样的发展过程。在实际项目中,建议:
- 统一使用SLF4J作为日志门面,便于切换底层实现
- 根据项目特点选择合适的日志实现:
- 一般项目使用Logback
- 高性能需求使用Log4j2
- 简单项目使用JUL
- 重视日志配置,合理设置级别、输出格式和滚动策略
- 关注日志性能,必要时使用异步日志
- 建立日志规范,统一团队的日志使用方式
选择合适的日志系统并正确使用,不仅能提高开发效率,还能在生产环境中快速定位和解决问题,是构建可维护系统的重要基础。
祝你变得更强!