日志规范最佳实践
2025/8/15非最佳实践#Java#日志
日志规范最佳实践
一、日志规范问题及改进方案
1. 异常处理规范
❌ 错误示例
// 1. 生吞异常,只打印toString
catch (Exception e) {
log.error("查询错误:" + e); // 丢失堆栈信息
}
// 2. 空catch块
catch (Exception e) {
// 什么都不做 - 严重问题!
}
// 3. 使用printStackTrace
catch (Exception e) {
e.printStackTrace(); // 输出到System.out,不受日志框架管理
}
// 4. 字符串拼接
catch (Exception e) {
log.error("查询模板错误: " + templateName + ", 异常: " + e);
}
✅ 正确示例
// 1. 完整记录异常堆栈
catch (Exception e) {
log.error("查询数据库失败,module: {}, template: {}", module, templateName, e);
}
// 2. 明确说明忽略原因
catch (Exception e) {
// 此异常为预期行为,用户取消操作
log.debug("用户取消操作,忽略异常", e);
}
// 3. 使用日志框架
catch (Exception e) {
log.error("ResultSet映射失败,表名: {}", tableName, e);
}
// 4. 使用占位符
catch (Exception e) {
log.error("查询模板错误,templateName: {}, userId: {}", templateName, userId, e);
}
2. 日志级别使用规范
| 级别 | 使用场景 | 示例 |
|---|---|---|
| ERROR | 系统异常、需要人工介入 | 数据库连接失败、外部服务不可用 |
| WARN | 业务异常、性能问题 | 参数校验失败、慢查询、降级处理 |
| INFO | 关键业务流程 | 用户登录、订单创建、支付成功 |
| DEBUG | 调试信息 | 方法入参出参、SQL 语句、中间状态 |
| TRACE | 详细追踪 | 循环内部状态、详细计算过程 |
❌ 错误示例
// 异常用INFO级别
try {
// ...
} catch (Exception e) {
log.info("系统错误:" + e); // 应该用ERROR
}
// 调试信息用ERROR级别
log.error("进入方法:{}", methodName); // 应该用DEBUG
✅ 正确示例
// 系统异常用ERROR
catch (SQLException e) {
log.error("数据库查询失败,SQL: {}", sql, e);
}
// 业务异常用WARN
if (user == null) {
log.warn("用户不存在,userId: {}", userId);
}
// 关键流程用INFO
log.info("用户登录成功,userId: {}, loginTime: {}", userId, loginTime);
// 调试信息用DEBUG
log.debug("方法入参,method: {}, params: {}", methodName, params);
3. 日志内容规范
必须包含的信息
- What - 发生了什么
- Where - 在哪里发生(类、方法、行号)
- When - 什么时候发生
- Who - 谁触发的(用户 ID、请求 ID)
- Why - 为什么发生(异常原因)
- How - 如何处理(降级、重试、忽略)
❌ 错误示例
log.error("失败"); // 信息太少
log.error("查询失败" + e.getMessage()); // 缺少上下文
log.info("result=" + result.toString()); // 可能NPE
✅ 正确示例
// 包含完整上下文
log.error("查询用户订单失败,userId: {}, orderId: {}, 耗时: {}ms",
userId, orderId, costTime, e);
// 防御性编程
log.info("查询结果,count: {}, data: {}",
result != null ? result.size() : 0,
result);
// 使用工具类
LoggerUtil.logSystemError(log,
"数据库连接失败,尝试重连", e,
"database", dbName, "retry", retryCount);
4. 性能优化
❌ 错误示例
// 1. 不必要的字符串拼接
log.debug("Heavy object: " + expensiveToString()); // 即使DEBUG关闭也会执行
// 2. 重复日志
for (User user : users) {
log.info("Processing user: {}", user); // 可能打印上千条
}
✅ 正确示例
// 1. 使用占位符或判断
if (log.isDebugEnabled()) {
log.debug("Heavy object: {}", expensiveToString());
}
// 2. 批量记录
log.info("开始处理用户,总数: {}", users.size());
// 处理逻辑...
log.info("用户处理完成,成功: {}, 失败: {}", successCount, failCount);
二、实战示例改进
改进前后对比
示例 1:DmService.queryDb
// ❌ 原代码
public List<Map<String, Object>> queryDb(parameter, queryDetail, isQueryDb) {
try {
// ...
} catch (Exception e) {
log.error("[getSqlAndQuery]查询报错"); // 没有异常信息!
}
return new ArrayList<>();
}
// ✅ 改进后
public List<Map<String, Object>> queryDb(parameter, queryDetail, isQueryDb) {
String module = (String) parameter.get(MODULE);
String requestId = LoggerUtil.getRequestId();
try {
log.debug("开始查询,module: {}, requestId: {}", module, requestId);
List<Map<String, Object>> result = service.getSqlAndQuery(parameter, queryDetail, isQueryDb);
log.debug("查询成功,module: {}, size: {}", module, result.size());
return result;
} catch (Exception e) {
log.error("查询失败,module: {}, requestId: {}, params: {}",
module, requestId, parameter, e);
throw new BizException("查询失败", e);
}
}
示例 2:文件上传异常处理
// ❌ 原代码
catch (IOException ex) {
log.error("合并文件失败," + ex);
throw new RuntimeException("上传失败");
}
// ✅ 改进后
catch (IOException ex) {
log.error("文件合并失败,fileName: {}, size: {}KB, userId: {}",
fileName, fileSize/1024, userId, ex);
// 保留原始异常信息
throw new BizException("文件上传失败:" + ex.getMessage(), ex);
}
示例 3:慢查询监控
// ✅ 新增慢查询监控
long startTime = System.currentTimeMillis();
try {
List<Map<String, Object>> result = jdbcTemplate.queryForList(sql);
long costTime = System.currentTimeMillis() - startTime;
// 记录慢查询
if (costTime > 3000) {
log.warn("慢查询警告,sql: {}, cost: {}ms, rows: {}",
sql.substring(0, Math.min(sql.length(), 100)),
costTime, result.size());
}
return result;
} catch (Exception e) {
long costTime = System.currentTimeMillis() - startTime;
log.error("SQL执行失败,sql: {}, cost: {}ms", sql, costTime, e);
throw e;
}
三、日志配置建议
logback.xml 配置示例
<configuration>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{requestId}] [%X{userId}] - %msg%n</pattern>
</encoder>
</appender>
<!-- 错误日志单独文件 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<file>logs/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %logger{36} [%X{requestId}] - %msg%n%ex</pattern>
</encoder>
</appender>
<!-- 慢查询日志 -->
<appender name="SLOW_SQL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/slow-sql.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{requestId}] - %msg%n</pattern>
</encoder>
</appender>
<!-- 日志级别配置 -->
<logger name="com.mgtv.mofang" level="INFO"/>
<logger name="com.mgtv.mofang.service.dw" level="DEBUG"/>
<logger name="SLOW_SQL" level="INFO" additivity="false">
<appender-ref ref="SLOW_SQL"/>
</logger>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</configuration>
四、检查清单
代码审查检查项
- 所有 catch 块都有日志记录
- 异常日志包含完整堆栈(最后一个参数是 Throwable)
- 使用占位符而非字符串拼接
- 日志级别使用正确
- 包含足够的上下文信息
- 没有敏感信息(密码、密钥等)
- 循环中避免重复日志
- DEBUG 日志有 isDebugEnabled 判断
常见错误速查
log.error(msg + e)→log.error(msg, e)e.printStackTrace()→log.error(msg, e)catch(Exception e) {}→catch(Exception e) { log.error(msg, e); }log.info("error: " + e)→log.error(msg, e)"param=" + param→"param: {}"
五、总结
良好的日志是系统可观测性的基础。遵循这些最佳实践可以:
- 快速定位问题 - 完整的异常堆栈和上下文
- 监控系统健康 - 合理的日志级别和告警
- 优化性能 - 发现慢查询和性能瓶颈
- 保障安全 - 避免敏感信息泄露
记住:日志是写给未来的自己和同事看的,请善待他们!