为什么不要 System.out.println
初学阶段用 System.out.println 打印结果没问题,但业务代码里继续这么写,迟早会吃苦头:它是同步输出,高并发时拖慢接口;没有级别,没法按 ERROR/INFO 过滤;不会滚动,日志文件能涨到几十 GB;也没法按包名单独调级别。
Spring Boot 默认的日志组合是 SLF4J + Logback:代码里面向 Logger 接口编程,底层实现可以换,配置通过 logback-spring.xml 完成。这一套是 2022 年最主流的方案,先掌握它就够用了。
日志级别与写法
级别从高到低是 ERROR > WARN > INFO > DEBUG > TRACE。生产环境一般用 INFO,排查问题时临时把某个包调到 DEBUG,别一上来就全局 DEBUG,磁盘和性能都扛不住。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
public void pay(Long orderId, BigDecimal amount) {
log.debug("开始处理支付, orderId={}", orderId);
log.info("订单支付成功, orderId={}, amount={}", orderId, amount);
}
}
两个细节:第一,用 {} 占位符,不要用字符串拼接,否则日志级别没开时拼接也照样执行;第二,异常对象放在最后一个参数,不要只传 e.getMessage():
log.error("订单支付失败, orderId={}", orderId, e);
这样堆栈才会完整输出。
logback-spring.xml
在 src/main/resources 下新建 logback-spring.xml,一个能直接用的版本:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_HOME" value="./logs"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>15</maxHistory>
<totalSizeCap>2GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
要点:SizeAndTimeBasedRollingPolicy 按天加大小滚动,maxHistory 控制保留天数,totalSizeCap 兜住总磁盘占用。日志量大时可以再加一个 AsyncAppender,把写文件放到独立线程,但队列要设上限,否则堆积起来反而容易 OOM。
按环境切换配置
不同环境的日志策略应该不一样:开发时控制台打 DEBUG,生产只打 INFO 并写文件。把上面 root 那一段换成下面的写法,用 <springProfile> 区分环境:
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</springProfile>
这个标签只有 logback-spring.xml 认识,logback.xml 会把它当未知标签忽略,日志级别就一直是默认值。
生产环境建议把文件 appender 包一层 AsyncAppender,队列大小和丢弃策略要显式配置。默认队列满了会丢日志,排查问题时最怕关键那条正好被丢掉,所以要么加大队列,要么在丢弃时打一条警告。
日志格式里几个占位符值得记住:%d 时间、%level 级别、%thread 线程名、%logger 类名、%msg 消息、%n 换行。线程名对排查并发问题特别有用,别把它省掉。
如果用了日志收集系统,建议输出 JSON 格式,字段固定,采集端不用写复杂的正则。本地开发仍然用可读的文本格式,两边通过 profile 切换。
常见坑
1. 文件名写成了 logback.xml
logback.xml 会在 Spring 环境准备好之前被加载,<springProfile>、<springProperty> 这些扩展标签全部不生效。Spring Boot 项目统一用 logback-spring.xml。如果两个文件同时存在,还会出现“配置改了半天没反应”的情况。
2. 日志重复打印
自己定义了一个 logger 并挂了 appender,又忘了设 additivity="false",日志会沿着父 logger 一路向上,最后 root 再打一遍。自定义 logger 时加上 <logger name="com.example" level="DEBUG" additivity="false">。
3. 日志目录没有写权限
./logs 是相对路径,相对于 JVM 启动目录,不是项目目录。服务器上应用装在 /opt/app,启动脚本却 cd /,日志就会写到根目录,然后因为权限不足静默失败。生产环境用绝对路径,或者通过启动参数 -DLOG_HOME=/data/logs/app 指定。
4. 日志里出现密码和身份证号
把整个 DTO 的 toString() 打进日志,请求参数里的手机号、密码、token 全暴露了。打印时只取必要字段,敏感信息做脱敏,别图省事直接打对象。
5. 大集合一次性打进日志
log.info("用户列表={}", list) 在数据量大时会产生几 MB 的单行日志,既拖慢应用又占磁盘。日志里只打数量和关键 ID,明细留给排查工具。
排查步骤
- 日志没输出:先确认依赖里没有排除
spring-boot-starter-logging,再确认配置文件在 classpath 根目录; - 配置不生效:在
<configuration>上临时加debug="true",启动时会打印 Logback 内部状态; - 文件没生成:
ls -l看目录是否存在、当前用户有没有写权限; - 级别不对:检查启动参数
--logging.level.com.example=DEBUG是否被配置文件覆盖; - 日志重复:检查自定义 logger 的
additivity和 root 上的 appender 配置。
评论