我发现很多程序员都不会打日志

一、为什么要打日志?

日志是系统出现错误时最快速有效的定位工具,同时可记录业务信息(如用户操作),用于系统分析和问题追溯。但很多程序员存在以下问题:

  • 不想打:缺乏意识
  • 意识不到:未理解日志重要性
  • 真不会:未系统学习日志框架

面试案例:询问应届Java开发“如何打日志”,竟有人回答用System.out.println(),暴露日志知识缺失。

二、日志框架选型

1. 为什么不用System.out.println()

  • 同步阻塞:每次调用触发I/O,影响性能(尤其生产环境)。
  • 功能单一:仅输出控制台,无法设置日志级别、格式、存储位置等。

2. 专业日志框架推荐

(1)主流框架对比

框架特点适用场景
Log4j经典框架,已停止维护,存在严重漏洞(如Log4j 2漏洞致命)旧项目兼容
Log4j 2基于LMAX Disruptor实现异步日志,性能高,但需额外绑定SLF4J高并发、高性能需求项目
LogbackSpring 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:可视化分析
  • 适用场景:分布式系统、海量日志分析(搭建成本较高,小团队可暂缓)。

五、总结

  • 日志是留给未来的“救命稻草”:良好的日志习惯能大幅提升故障排查效率。
  • 核心原则
  1. 用专业框架替代System.out
  2. 参数化日志、控制输出量;
  3. 统一格式、管理日志存储;
  4. 敏感信息保护、异步优化性能。

最后提醒:日志不是写给机器的,而是写给未来的你和团队!学会后记得点赞支持~

留下评论

您的邮箱地址不会被公开。 必填项已用 * 标注