一、为什么要打日志?
日志是系统出现错误时最快速有效的定位工具,同时可记录业务信息(如用户操作),用于系统分析和问题追溯。但很多程序员存在以下问题:
- 不想打:缺乏意识
- 意识不到:未理解日志重要性
- 真不会:未系统学习日志框架
面试案例:询问应届Java开发“如何打日志”,竟有人回答用System.out.println()
,暴露日志知识缺失。
二、日志框架选型
1. 为什么不用System.out.println()
?
- 同步阻塞:每次调用触发I/O,影响性能(尤其生产环境)。
- 功能单一:仅输出控制台,无法设置日志级别、格式、存储位置等。
2. 专业日志框架推荐
(1)主流框架对比
框架 | 特点 | 适用场景 |
---|---|---|
Log4j | 经典框架,已停止维护,存在严重漏洞(如Log4j 2漏洞致命) | 旧项目兼容 |
Log4j 2 | 基于LMAX Disruptor实现异步日志,性能高,但需额外绑定SLF4J | 高并发、高性能需求项目 |
Logback | Spring Boot默认集成,SLF4J原生实现,稳定性和易用性更优 | 新项目首选 |
(2)SLF4J:日志门面模式
- 作用:提供统一接口,解耦应用代码与具体日志实现(如Logback/Log4j)。
- 优势:切换日志框架时无需修改应用代码,只需更换底层实现。
推荐组合:
- 新手/普通项目:SLF4J + Logback(Spring Boot默认集成,无需额外配置)。
- 高性能需求:SLF4J + Log4j 2(需引入绑定器)。
三、日志框架使用方法
1. 获取Logger对象的三种方式
(1)手动获取(传统方式)
import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MyService { private static final Logger logger = LoggerFactory.getLogger(MyService.class); public void doSomething() { logger.info("执行了一些操作"); } }
(2)动态获取当前类(简化类名修改)
public class MyService { private final Logger logger = LoggerFactory.getLogger(this.getClass()); public void doSomething() { logger.info("执行了一些操作"); } }
(3)Lombok注解(极简方式)
import lombok.extern.slf4j.Slf4j; @Slf4j public class MyService { public void doSomething() { log.info("执行了一些操作"); // 自动生成log对象 } }
2. 配置文件示例(Logback)
<?xml version="1.0" encoding="UTF-8"?> <configuration scan="true" scanPeriod="10 seconds"> <!-- 日志存储路径 --> <property name="log.path" value="logs"/> <!-- 日志最大保留30天 --> <property name="maxHistory" value="30"/> <!-- 控制台输出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%thread] %highlight(%-5level) %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender> </configuration>
四、日志记录最佳实践
1. 合理选择日志级别
级别 | 用途描述 | 环境建议 |
---|---|---|
TRACE | 最细粒度调试信息(开发阶段使用) | 开发环境 |
DEBUG | 调试变量值、执行路径(开发/测试环境) | 开发/测试环境 |
INFO | 关键业务流程记录(如用户登录、订单创建) | 生产环境 |
WARN | 潜在问题(如配置异常,不影响功能但需关注) | 生产环境 |
ERROR | 功能异常(如数据库连接失败,需立即处理) | 全环境 |
FATAL | 致命错误(如JVM崩溃,系统无法运行) | 全环境 |
建议:
- 开发环境:启用DEBUG级别,获取详细信息。
- 生产环境:使用INFO或WARN级别,减少性能开销。
2. 正确记录日志信息
(1)参数化日志(避免字符串拼接)
// 反例:字符串拼接(性能差、可能空指针) logger.debug("用户ID:" + userId + " 登录成功。"); // 正例:占位符参数化 logger.debug("用户ID:{} 登录成功。", userId);
(2)记录完整异常信息
try { // 业务逻辑 } catch (Exception e) { // 记录参数+完整堆栈 logger.error("处理用户ID:{} 时发生异常", userId, e); }
3. 控制日志输出量
(1)避免循环内日志
// 反例:循环内频繁输出 for (int i = 0; i < 10000; i++) { logger.info("处理第{}条数据", i); // 1万次I/O操作 } // 正例:条件过滤(每1000条记录输出一次) for (int i = 0; i < 10000; i++) { if (i % 1000 == 0) { logger.info("已处理{}条记录", i); // 仅输出10次 } }
(2)开销较大的参数检查
if (logger.isDebugEnabled()) { // 先检查日志级别,避免无效计算 logger.debug("复杂参数:{}", expensiveOperation()); }
4. 把控日志记录时机
(1)关键业务节点
- 用户登录、支付、订单状态变更等核心流程。
- 方法入口/出口:记录参数和返回值(便于问题复现)。
(2)AOP统一日志(推荐)
@Aspect @Component public class LoggingAspect { @Before("execution(* com.example.service..*(..))") public void logBeforeMethod(JoinPoint joinPoint) { Logger logger = LoggerFactory.getLogger(joinPoint.getTarget().getClass()); logger.info("方法{}开始执行,参数:{}", joinPoint.getSignature().getName(), Arrays.toString(joinPoint.getArgs())); } }
(3)禁止记录敏感信息
- 避免日志中出现用户密码、银行卡号等敏感数据。
5. 日志管理策略
(1)日志滚动策略
- 按文件大小滚动:
<rollingPolicy class="ch.qos.logback.core.rolling.SizeBasedRollingPolicy"> <maxFileSize>10MB</maxFileSize> <!-- 单个文件最大10MB --> </rollingPolicy>
- 按时间滚动:
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/app-%d{yyyy-MM-dd}.log</fileNamePattern> <!-- 按天生成日志 --> </rollingPolicy>
(2)日志压缩与清理
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/app-%d{yyyy-MM-dd}.log.gz</fileNamePattern> <!-- 压缩存储 --> <maxHistory>30</maxHistory> <!-- 保留30天 --> </rollingPolicy>
(3)自动化清理脚本
# 清理90天前的日志 find /var/log/myapp/ -type f -mtime +90 -exec rm {} \;
6. 统一日志格式
(1)标准格式示例
2024-11-21 14:30:15.123 [main] INFO com.example.service.UserService - 用户ID:12345 登录成功
- 包含:时间戳、线程名、日志级别、类名、消息。
(2)JSON格式(便于分析)
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder"> <!-- 自动生成JSON结构日志 --> </encoder>
(3)MDC上下文信息
MDC.put("requestId", "666"); // 添加请求ID MDC.put("userId", "yupi"); // 添加用户ID logger.info("用户请求处理完成"); MDC.clear(); // 清理上下文(避免线程安全问题)
7. 异步日志提升性能
<!-- 异步Appender配置 --> <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender"> <queueSize>500</queueSize> <!-- 队列大小(建议500-1000) --> <discardingThreshold>0</discardingThreshold> <!-- 不丢弃日志 --> <neverBlock>true</neverBlock> <!-- 队列满时不阻塞主线程 --> <appender-ref ref="CONSOLE"/> <!-- 绑定目标Appender(控制台) --> <appender-ref ref="FILE"/> <!-- 绑定目标Appender(文件) --> </appender>
8. 集成日志收集系统(ELK)
- 组件说明:
- Elasticsearch:存储和搜索日志
- Logstash:收集和处理日志
- Kibana:可视化分析
- 适用场景:分布式系统、海量日志分析(搭建成本较高,小团队可暂缓)。
五、总结
- 日志是留给未来的“救命稻草”:良好的日志习惯能大幅提升故障排查效率。
- 核心原则:
- 用专业框架替代
System.out
; - 参数化日志、控制输出量;
- 统一格式、管理日志存储;
- 敏感信息保护、异步优化性能。
最后提醒:日志不是写给机器的,而是写给未来的你和团队!学会后记得点赞支持~