Java 日志规范与实践
作者: shura | 日期: 2025-02-23
说明
本文讲 Java 日志的正确用法,重点解决三个问题:
- 日志框架怎么选?
- 日志该怎么打?
- 部署时日志该输出到哪?(标准输出 vs 文件)
最后一个问题很多人搞不清楚,本文会详细讲解。
一、日志框架选择
1.1 框架关系图
应用代码
↓
SLF4J (门面) ← 你应该使用的 API
↓
实现层:
├── Logback ← Spring Boot 默认
├── Log4j2 ← 高性能选择
└── JUL/Log4j/... ← 其他实现
核心原则: 代码只依赖 SLF4J,运行时选择具体实现。
1.2 Logback vs Log4j2
| 对比项 | Logback | Log4j2 | 说明 |
|---|---|---|---|
| 性能 | 普通 | 优秀 | Log4j2 异步性能高 10 倍+ |
| 集成 | Spring Boot 默认 | 需替换 | Logback 开箱即用 |
| 配置 | 简单 | 较复杂 | Logback 上手快 |
| 新特性 | 少 | 多 | Log4j2 支持更多特性 |
| 推荐场景 | 中小项目 | 高并发项目 | - |
1.3 如何选择
选 Logback:
- Spring Boot 项目,无特殊性能要求
- 团队不想折腾,开箱即用
选 Log4j2:
- 高并发场景(日志量大)
- 追求极致性能
- 新项目,愿意花时间配置
依赖配置:
<!-- Logback (Spring Boot 默认,无需额外配置) -->
<!-- Log4j2 (需替换) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
二、日志规范
2.1 日志级别
| 级别 | 用途 | 示例 |
|---|---|---|
| ERROR | 系统错误,影响功能 | 数据库连接失败、外部接口超时 |
| WARN | 警告,不影响主流程 | 配置缺失用默认值、余额不足 |
| INFO | 关键业务节点 | 用户登录、订单创建、支付完成 |
| DEBUG | 调试信息 | 方法入参、查询结果 |
生产环境:ERROR/WARN/INFO 开启,DEBUG 关闭。
2.2 什么该打日志
必须记录:
// 1. 关键业务操作
log.info("创建订单, userId: {}, amount: {}", userId, amount);
// 2. 外部调用
log.info("调用支付接口, orderId: {}", orderId);
// 3. 异常(必须包含堆栈)
try {
paymentService.pay(orderId);
} catch (Exception e) {
log.error("支付失败, orderId: {}", orderId, e); // 注意:e 放最后,不用 {}
throw e;
}
// ❌ 错误:不会打印堆栈
log.error("支付失败, orderId: {}, error: {}", orderId, e.getMessage());
// ✅ 正确:异常对象放最后,自动打印堆栈
log.error("支付失败, orderId: {}", orderId, e);
// 4. 状态变更
log.info("订单状态变更, orderId: {}, {} -> {}", orderId, oldStatus, newStatus);
不要记录:
// ❌ 敏感信息
log.info("用户登录, password: {}", password); // 密码
log.info("银行卡号: {}", cardNo); // 卡号
// ❌ 大对象
log.info("查询结果: {}", largeList); // 可能几千条数据
// ❌ 循环内
for (User user : users) {
log.info("处理用户: {}", user); // 性能问题
}
2.3 性能优化
1. 使用占位符避免字符串拼接
// ✅ 正确:占位符延迟计算
log.debug("用户信息: {}", user);
// ❌ 错误:即使 DEBUG 关闭,字符串拼接也会执行
log.debug("用户信息: " + user.toString());
2. 复杂对象构建需要判断
// ❌ 问题:buildComplexObject() 方法会执行,即使日志不打印
log.debug("详细信息: {}", buildComplexObject());
// ✅ 正确:先判断日志级别
if (log.isDebugEnabled()) {
log.debug("详细信息: {}", buildComplexObject());
}
为什么要判断?
占位符只能避免字符串拼接,但不能避免方法调用:
// 场景:构建复杂对象的方法
private String buildComplexObject() {
// 耗时操作:查询数据库、计算、遍历集合等
List<User> users = userRepository.findAll(); // 查数据库
return users.stream()
.map(User::toString)
.collect(Collectors.joining(",")); // 遍历拼接
}
// 即使生产环境 DEBUG 关闭,这个方法依然会执行!
log.debug("用户列表: {}", buildComplexObject()); // ❌ 性能浪费
// 先判断,DEBUG 关闭时方法不会执行
if (log.isDebugEnabled()) {
log.debug("用户列表: {}", buildComplexObject()); // ✅ 性能优化
}
总结:
- 简单对象(已存在的变量):直接用占位符
- 复杂对象(需要方法构建):先判断日志级别
三、日志输出:标准输出 vs 文件
3.1 两种输出方式
标准输出(stdout/stderr):
日志 → 控制台/终端
文件输出:
日志 → 文件(/var/log/app.log)
3.2 传统部署方式(物理机/虚拟机)
推荐:文件输出
<!-- logback-spring.xml -->
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/myapp/application.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/var/log/myapp/application.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
</appender>
<root level="INFO">
<appender-ref ref="FILE"/>
</root>
</configuration>
为什么用文件输出:
- 持久化,重启不丢失
- 可按日期滚动,方便查找
- 支持日志收集工具(Filebeat)
3.3 容器化部署(Docker/K8s)
推荐:标准输出
<!-- logback-spring.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} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
为什么用标准输出:
1. 容器是短暂的
- 容器销毁,文件就没了
- 写文件需要挂载 Volume,麻烦
2. 日志收集更简单
- Docker:
docker logs <container> - K8s:
kubectl logs <pod> - 日志收集工具直接读取 stdout
3. 符合 12-Factor 原则
- 应用不关心日志存储
- 日志当作事件流处理
3.4 实际场景决策
场景 1:容器 + ELK 日志集成
推荐:标准输出 + JSON 格式
应用 stdout → Docker → Fluentd/Filebeat → Elasticsearch → Kibana
配置:
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<!-- JSON 格式,直接适配 ELK -->
<includeMdcKeyName>traceId</includeMdcKeyName>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
优点:
- 日志收集工具直接读取 stdout
- JSON 格式方便 Elasticsearch 解析
- 不需要挂载 Volume
场景 2:容器 + NFS 共享存储
推荐:文件输出 + 挂载 NFS
应用日志 → 文件 → NFS 共享目录 → 多个容器可访问
配置:
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/app/logs/application.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/app/logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<root level="INFO">
<appender-ref ref="FILE"/>
</root>
</configuration>
Docker 运行:
docker run -v /nfs/logs:/app/logs myapp:latest
优点:
- 多个容器共享日志
- 日志持久化,容器重启不丢失
- 可以直接在 NFS 服务器上查看
场景 3:容器 + 传统运维工具(Filebeat 读文件)
推荐:文件输出 + 挂载宿主机
应用日志 → 文件 → 宿主机目录 → Filebeat 读取 → Elasticsearch
这种场景适用于:
- 公司已有 Filebeat 基础设施
- 运维习惯于读取文件
- 需要日志文件分级管理
场景 4:本地开发/调试
推荐:双输出(控制台 + 文件)
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
</appender>
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
优点:
- 控制台实时查看
- 文件保留历史日志
3.5 常见误区
误区 1:容器里写文件不挂载 Volume
# ❌ 错误:日志在容器内,容器删除就没了
docker run myapp:latest
# ✅ 正确:挂载出来或用标准输出
docker run -v /var/log:/app/logs myapp:latest
误区 2:生产环境标准输出和文件同时开
<!-- ❌ 不推荐:生产环境双写浪费性能 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
生产环境应根据部署方式选一个,本地开发可以双开。
误区 3:标准输出没配置日志驱动
Docker 默认用 json-file 驱动,日志会堆积:
# 查看日志文件大小
du -sh /var/lib/docker/containers/*/
解决方法:
// /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
四、多环境配置
4.1 Spring Boot Profile
# application.yml
spring:
profiles:
active: ${ENV:dev}
# 开发环境
---
spring:
config:
activate:
on-profile: dev
logging:
level:
root: INFO
com.example: DEBUG
# 生产环境
---
spring:
config:
activate:
on-profile: prod
logging:
level:
root: INFO
com.example: INFO
4.2 Logback 按环境切换
<configuration>
<!-- 开发环境:控制台 + 彩色 -->
<springProfile name="dev">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%-5level) %clr(${PID:- }){magenta} %clr([%thread]){faint} %clr(%logger{36}){cyan} %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<!-- 生产环境:标准输出(容器)或文件(物理机) -->
<springProfile name="prod">
<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>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
</configuration>
五、实践建议
5.1 日志分级输出
<!-- ERROR 日志单独文件(方便告警) -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/myapp/error.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/var/log/myapp/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
5.2 异步日志
<!-- 异步 Appender(提升性能) -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE"/>
<queueSize>512</queueSize>
<discardingThreshold>0</discardingThreshold> <!-- 0 = 不丢弃任何级别日志 -->
<neverBlock>false</neverBlock> <!-- false = 队列满时阻塞,保证不丢失 -->
</appender>
<root level="INFO">
<appender-ref ref="ASYNC"/>
</root>
注意:
discardingThreshold=0保证不丢弃日志neverBlock=false队列满时阻塞,防止丢失- 高吞吐场景可设置
neverBlock=true允许丢失非 ERROR 日志
5.3 日志 JSON 格式
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>traceId</includeMdcKeyName>
</encoder>
</appender>
输出:
{
"timestamp": "2025-02-23T10:30:45.123+08:00",
"level": "INFO",
"thread": "http-nio-8080-exec-1",
"logger": "com.example.UserService",
"message": "创建用户, userId: 123",
"traceId": "abc123"
}
六、检查清单
代码层面:
- 使用 SLF4J,不直接使用 Logback/Log4j2
- 日志级别正确(ERROR/WARN/INFO/DEBUG)
- 异常日志包含堆栈(第三个参数传异常对象)
- 使用占位符
{},不用字符串拼接 - 没有记录敏感信息(密码、卡号)
配置层面:
- 生产环境关闭 DEBUG 日志
- 配置日志滚动策略(防止磁盘爆满)
- 容器化部署用标准输出
- 物理机部署用文件输出
- ERROR 日志有监控告警
部署层面:
- 容器配置日志驱动限制大小
- 日志收集工具正常运行(ELK/EFK)
- 定期清理过期日志
七、总结
| 场景 | 推荐方案 |
|---|---|
| 传统部署 | 文件输出 + 日志滚动 |
| 容器部署 | 标准输出 + 日志收集 |
| 开发环境 | 控制台输出 + DEBUG 级别 |
| 生产环境 | 看部署方式 + INFO 级别 |
核心原则:
- 代码只依赖 SLF4J
- 日志框架根据场景选(Logback 够用,Log4j2 更快)
- 容器化优先用标准输出
- 传统部署用文件输出
本文基于 Spring Boot 3.x

被折叠的 条评论
为什么被折叠?



