刚入职的程序员小阿巴,开发的批量导入数据程序一上线就崩了。用户反馈报错中断,可服务器上的报错信息少得可怜,他硬生生花了 8 小时,才通过逐行测试用户数据定位到问题,原本的摸鱼时光全泡汤了。
Taimili 艾米莉 ( 一款专业的 GitHub star 管理和github 加星涨星工具taimili.com )
艾米莉 是一款优雅便捷的 GitHub star 管理和github 加星涨星工具,基于 PHP & javascript 构建, 能对github 得 star fork follow watch 管理和提升,最适合github 的深度用户
导师鱼皮看到他愁眉苦脸的样子,一语道破关键:“你没打日志吧?” 小阿巴满脸疑惑:“谁是日志?为什么要打它?” 这一幕,恐怕很多刚入行的程序员都似曾相识。日志作为程序的 “运行日记”,能精准记录执行状态,可太多人不懂怎么正确使用。今天就把鱼皮沉淀多年的 8 个日志实战技巧分享给大家,让你少走弯路!
一、先搞懂:日志到底是什么?
日志是程序运行时的 “状态记录仪”,能详细记录代码执行的关键节点、参数信息和异常情况,当系统出现 Bug 时,无需反复测试,通过日志就能快速定位问题根源。
比如批量导入用户的核心代码,合理的日志能让执行过程一目了然:
java
运行
@Slf4j
public class UserService {
public void batchImport(List<UserDTO> userList) {
log.info("开始批量导入用户,总数:{}", userList.size());
int successCount = 0;
int failCount = 0;
for (UserDTO userDTO : userList) {
try {
log.info("正在导入用户:{}", userDTO.getUsername());
validateUser(userDTO);
saveUser(userDTO);
successCount++;
log.info("用户 {} 导入成功", userDTO.getUsername());
} catch (Exception e) {
failCount++;
log.error("用户 {} 导入失败,原因:{}", userDTO.getUsername(), e.getMessage(), e);
}
}
log.info("批量导入完成,成功:{},失败:{}", successCount, failCount);
}
}
代码中的log.info(正常流程)和log.error(异常情况),就是最基础的日志记录方式。
二、入门:日志框架怎么用?
每种编程语言都有成熟的日志框架,Java 生态中常用 Log4j 2、Logback 等。如果使用 Spring Boot,无需额外引入依赖,它默认集成了 Logback,直接使用即可。
1. 获取日志对象(3 种方式)
- 方式 1:手动通过 LoggerFactory 获取
java
运行
public class MyService {
private static final Logger logger = LoggerFactory.getLogger(MyService.class);
}
- 方式 2:通过
this.getClass()获取当前类类型
java
运行
public class MyService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
}
- 方式 3:用 Lombok 的
@Slf4j注解(推荐)无需手动定义日志对象,注解会自动生成:
java
运行
@Slf4j
public class MyService {
public void doSomething() {
log.info("执行了一些操作");
}
}
等同于自动生成:
java
运行
private static final org.slf4j.Logger log =
org.slf4j.LoggerFactory.getLogger(MyService.class);
2. 千万别用 System.out.println 替代日志!
很多新手会图方便用System.out.println输出信息,但这存在两大致命问题:
- 性能差:同步方法,频繁调用会触发耗时 I/O 操作,拖慢程序;
- 不灵活:只能输出到控制台,无法保存到文件,也不能控制格式和级别,历史日志无法追溯。
而日志框架能完美解决这些问题,还支持灵活配置,这也是打日志的核心价值。
三、进阶:8 个日志最佳实践
1. 合理选择日志级别(避免信息泛滥)
日志按重要程度分为多个级别,核心常用的有 4 种,不同场景必须严格区分:
- DEBUG:调试用的详细信息(如对象完整属性),仅开发 / 测试环境使用;
- INFO:正常业务流程记录(如 “开始导入”“执行完成”);
- WARN:潜在问题但不影响主流程(如邮箱格式异常但仍可导入);
- ERROR:异常或错误(如导入失败、接口调用超时),需包含完整异常信息。
示例代码:
java
运行
log.debug("用户对象的详细信息:{}", userDTO); // 调试信息
log.info("用户 {} 开始导入", username); // 正常流程
log.warn("用户 {} 的邮箱格式可疑,但仍然导入", username); // 警告信息
log.error("用户 {} 导入失败", username, e); // 错误信息(e为异常对象)
关键原则:生产环境需调高日志级别(如 INFO 或 WARN),避免 DEBUG 日志淹没重要信息,同时减少磁盘占用。
2. 用参数化日志,拒绝字符串拼接
很多人习惯用字符串拼接记录日志:
java
运行
// 不推荐!
log.info("用户 " + username + " 开始导入");
这种方式的问题是:无论日志是否最终输出,拼接操作都会执行,浪费性能。
正确做法是使用参数化日志({}作为占位符):
java
运行
// 推荐!
log.info("用户 {} 开始导入", username);
日志框架会在运行时动态替换参数,未输出的日志不会执行额外操作,性能更优。
记录异常时更要注意,需传入完整异常对象e,才能保留堆栈信息:
java
运行
try {
// 业务逻辑
} catch (Exception e) {
log.error("用户 {} 导入失败", username, e); // 必须传入e
}
3. 把握日志时机,不冗余不遗漏
日志不是越多越好,关键是 “关键时刻有记录”:
- 必记场景:方法入口(参数)、方法出口(返回值)、核心业务步骤、异常捕获处;
- 减少冗余:循环中避免每条数据都打日志(如 10 万条数据无需逐条记录导入中状态);
- 高效技巧:用 AOP 切面编程,自动为所有业务方法添加入口 / 出口日志,减少重复代码;
- 安全提醒:日志中严禁记录敏感信息(如密码、手机号、身份证号),防止信息泄露。
4. 控制输出量,避免日志刷屏
当处理大量数据时,需通过以下方式控制日志数量:
- 条件输出:每处理 N 条数据记录一次进度(如每 100 条);
java
运行
if ((i + 1) % 100 == 0) {
log.info("批量导入进度:{}/{}", i + 1, userList.size());
}
- 批量拼接:循环中用 StringBuilder 积累信息,结束后统一输出;
java
运行
StringBuilder logBuilder = new StringBuilder("处理结果:");
for (UserDTO userDTO : userList) {
processUser(userDTO);
logBuilder.append(String.format("成功[ID=%s], ", userDTO.getId()));
}
log.info(logBuilder.toString());
- 级别过滤:通过配置文件只输出 INFO 及以上级别日志,屏蔽 DEBUG 冗余信息。
5. 统一日志格式,方便排查
混乱的日志格式会增加排查难度,需在配置文件中定义统一格式,包含关键信息:
xml
<!-- 控制台日志格式配置 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
格式说明:
%d:时间戳(精确到毫秒);[%thread]:线程名称;%-5level:日志级别(左对齐,占 5 位);%logger{36}:类名(最长 36 字符);%msg%n:日志内容 + 换行。
进阶技巧:用 MDC 添加上下文信息(如请求 ID、用户 ID),方便追踪分布式系统中的调用链路:
java
运行
@PostMapping("/user/import")
public Result importUsers(@RequestBody UserImportRequest request) {
// 设置MDC上下文
MDC.put("requestId", generateRequestId());
MDC.put("userId", String.valueOf(request.getUserId()));
try {
log.info("用户请求处理开始");
userService.batchImport(request.getUserList());
return Result.success();
} finally {
MDC.clear(); // 必须清理,避免内存泄漏
}
}
配置文件中引用 MDC 变量:
xml
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{requestId}] [%X{userId}] %msg%n</pattern>
6. 开启异步日志,提升程序性能
默认情况下,日志输出是同步操作(写入文件时阻塞主线程),高并发场景会影响性能。开启异步日志后,写日志操作会交给独立线程执行,不阻塞业务流程:
xml
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>512</queueSize> <!-- 队列大小 -->
<discardingThreshold>0</discardingThreshold> <!-- 不丢弃日志 -->
<neverBlock>false</neverBlock> <!-- 队列满时阻塞(避免日志丢失) -->
<appender-ref ref="FILE" /> <!-- 关联文件输出目标 -->
</appender>
<root level="INFO">
<appender-ref ref="ASYNC" />
</root>
注意:异步日志可能因程序崩溃丢失缓冲区日志,需根据业务场景权衡(性能优先可开启,日志完整性优先则关闭)。
7. 日志管理:自动切分 + 清理,避免磁盘爆满
日志文件长期不处理会占用大量磁盘空间,甚至导致服务器崩溃。通过配置滚动策略,让框架自动管理日志:
xml
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/app-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>10MB</maxFileSize> <!-- 单个文件最大10MB -->
<maxHistory>30</maxHistory> <!-- 保留30天日志 -->
<totalSizeCap>1GB</totalSizeCap> <!-- 日志总大小不超过1GB -->
</rollingPolicy>
配置说明:
- 按日期 + 大小切分:每天生成新文件,单个文件超 10MB 自动创建新文件;
- 自动压缩:.gz 后缀自动压缩日志文件,节省磁盘空间;
- 自动清理:只保留最近 30 天日志,总大小超 1GB 时自动删除旧日志。
8. 分布式系统:集成日志收集系统
当项目发展为多服务分布式架构时,登录多台服务器查看日志效率极低,需搭建集中式日志系统,常用组合 ELK(Elasticsearch+Logstash+Kibana):
- Logstash:收集各服务日志,统一格式化;
- Elasticsearch:存储并索引日志,支持快速搜索;
- Kibana:可视化界面,可按关键词、时间范围、服务名等筛选日志。
注意:ELK 搭建和运维成本较高,小团队或小型项目可选用轻量级方案(如 Graylog、Loki),按需选择即可。
四、最后提醒:日志是写给 “人” 看的
日志的核心价值是 “便于排查问题”,无论是未来的自己,还是团队同事,都能通过日志快速理解程序运行状态。记住:
- 不打日志的程序员,不是好程序员;
- 乱打日志的程序员,会给队友挖坑;
- 规范打日志,才能让 Bug 无所遁形。