在真实项目中,System.out.println 是比Bug更隐蔽的生产事故隐患。本文带你从零搭建企业级日志系统。
为什么你还在用 System.out.println?
很多Java新手习惯用 System.out.println() 打印调试信息。看起来简单直观,但在真实项目中,这种方式存在严重问题:
| 问题 | 说明 |
|---|---|
| 无法控制输出级别 | 只能全量输出,不能按ERROR/WARN/INFO/DEBUG分级查看 |
| 无法持久化 | 日志只打在控制台,应用重启后全部丢失 |
| 性能极差 | System.out.println() 是同步阻塞的,大量输出会严重拖慢应用 |
| 无格式/无归档 | 无法统一格式,无法按天/按大小滚动切割 |
注意:专业的日志系统不仅是为了调试,更是生产环境排障的命脉。没有日志,线上出问题就像在黑暗中摸索。
Spring Boot 日志体系:SLF4J + Logback
Spring Boot 默认集成了 SLF4J(日志门面)+ Logback(日志实现),无需额外引入依赖即可开箱即用。
SLF4J 是什么?
它就像JDBC一样,是一套统一的API接口,背后可以对接Logback、Log4j等不同实现。你写代码时只用SLF4J的API,以后想换日志实现,只需改依赖配置,代码一行都不用改**《阿里巴巴Java开发手册》强制规定:应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用SLF4J中的API**。这一条是强制级别。
在代码中正确使用 Logger
1 .基础用法
bash
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/user")
public class UserController {
// 1. 声明为 private static final,类名作为logger名
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@GetMapping("/{id}")
public UserVO getUser(@PathVariable Long id) {
// 2. 按级别打印日志
logger.debug("调试信息:正在查询用户,id={}", id);
logger.info("普通信息:接收到查询用户请求,id={}", id);
logger.warn("警告信息:用户id={}的某些数据缺失", id);
logger.error("错误信息:查询用户失败,id={}", id);
// 3. ✅ 使用 {} 占位符,不要用 + 拼接字符串!
String name = "张三";
String action = "登录";
logger.info("用户 {} 执行了 {} 操作", name, action);
// 4. ❌ 错误示范:字符串拼接(即使日志级别不输出,拼接操作也会执行)
// logger.info("用户 " + name + " 执行了 " + action + " 操作");
return userService.getUser(id);
}
}
2.关键规范
-
Logger声明为
private static final,避免重复创建 -
使用
{}占位符,利用SLF4J的延迟评估特性,避免不必要的字符串拼接开销 -
不要用
+拼接日志信息,即使日志级别被关闭,拼接操作仍会执行
3.日志级别选择指南
| 级别 | 使用场景 | 生产环境 |
|---|---|---|
| ERROR | 系统错误、异常,需要立即人工介入 | 开启 |
| WARN | 潜在问题、降级处理、配置异常 | 开启 |
| INFO | 关键业务流程节点(登录、下单、支付) | 开启 |
| DEBUG | 开发调试、方法入参出参 | 关闭 |
| TRACE | 更细粒度的调试信息 | 关闭 |
安全红线:DEBUG和TRACE级别绝对不能在生产环境全局开启,它们会打印SQL参数、请求体等敏感数据,可能泄露密码、Token、个人隐私信息。
日志配置实战
1 .application.yml 基础配置
bash
logging:
# 指定自定义logback配置文件(可选)
config: classpath:logback-spring.xml
# 设置特定包的日志级别
level:
com.example.demo.dao: DEBUG # 开发时打印SQL
org.springframework.web: INFO
com.example.demo: INFO
# 日志文件配置(简单场景可用)
file:
name: logs/application.log
max-size: 100MB
max-history: 30
2 .logback-spring.xml 专业配置
使用 logback-spring.xml 而非 logback.xml,可以享受Spring的profile支持和属性替换功能。建议在 src/main/resources 下创建:
bash
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds">
<!-- 1. 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 2. 文件输出 - 按天滚动 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 3. 错误日志单独输出 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/error.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<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] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 4. 各环境差异化配置 -->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE" />
</root>
</springProfile>
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
<!-- 生产环境强制关闭敏感包的DEBUG日志 -->
<logger name="org.hibernate.SQL" level="INFO" />
<logger name="org.hibernate.type.descriptor.sql" level="INFO" />
</springProfile>
<!-- 5. 默认配置(未指定profile时生效) -->
<springProfile name="default">
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</springProfile>
</configuration>
进阶:结构化日志(Spring Boot 3.4+)
传统的文本日志适合人读,但不适合机器解析。当你的应用需要接入ELK等日志平台时,JSON格式的结构化日志是更好的选择。
Spring Boot 3.4+ 开箱即用支持结构化日志,只需在配置中开启:
bash
# application.yml
logging:
structured:
format:
console: ecs # Elastic Common Schema格式
# console: logstash # 或Logstash格式
# console: gelf # 或GELF格式
file:
name: logs/app.json
开启后控制台输出的日志会变为JSON格式
javascript
{"@timestamp":"2026-08-18T10:30:00.123Z","log.level":"INFO","service.name":"my-service","message":"用户 张三 登录成功","userId":"12345"}
1.添加业务字段(MDC)
使用 MDC(Mapped Diagnostic Context) 可以为每条日志附加业务上下文,极大方便链路追踪:
javascript
import org.slf4j.MDC;
@RestController
public class OrderController {
@GetMapping("/order/{orderId}")
public OrderVO getOrder(@PathVariable String orderId) {
// 设置MDC字段,该请求后续所有日志都会带上
MDC.put("orderId", orderId);
MDC.put("userId", getCurrentUserId());
try {
logger.info("开始查询订单");
// ... 业务逻辑
return orderService.getOrder(orderId);
} finally {
// 请求结束后必须清除,避免线程复用污染
MDC.clear();
}
}
}
然后在 logback-spring.xml 的pattern中添加 %X{orderId} 即可输出:
javascript
%X{userId} - %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n
2.使用流式API添加字段(Spring Boot 3.4+)
不依赖MDC,直接在日志语句中添加键值对:
java
logger.atInfo()
.setMessage("订单创建成功")
.addKeyValue("orderId", order.getId())
.addKeyValue("amount", order.getAmount())
.log();
日志安全:永远不要记录敏感信息
日志是安全攻击面,必须遵守以下铁律:
| 敏感信息类型 | 处理方案 |
|---|---|
| 密码、Token、API Key | 禁止记录,或记录为 **** 掩码 |
| 身份证号、手机号、邮箱 | 脱敏后记录(如 138****1234) |
| 完整的请求/响应对象 | 只记录特定安全字段,避免全量打印 |
| 异常堆栈中的用户输入 | 记录异常类型 + 关联ID,过滤具体输入内容 |
| 数据库连接信息、密钥 | 绝对禁止记录到日志文件 |
危险代码!:
java
logger.info("登录请求:{}", request); // request里可能包含password字段
安全代码:
java
logger.info("用户 {} 登录{}", username, success ? "成功" : "失败");
注意: 可以在Logback中配置敏感字段过滤 ,自动屏蔽 password、token、secret 等关键词,从管道层面兜底防护,不依赖开发人员记住规范。
企业级增强:AOP统一日志切面
如果每个Controller方法都手动写日志,代码会非常冗余。可以用 Spring AOP 实现统一日志拦截,做到业务代码零侵入。
java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class WebLogAspect {
private static final Logger logger = LoggerFactory.getLogger(WebLogAspect.class);
// 切点:controller包下的所有方法
@Around("execution(* com.example.demo.controller.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().toShortString();
logger.info("【请求开始】方法:{},参数:{}", methodName, joinPoint.getArgs());
try {
Object result = joinPoint.proceed();
long cost = System.currentTimeMillis() - startTime;
logger.info("【请求结束】方法:{},耗时:{}ms,返回:{}", methodName, cost, result);
return result;
} catch (Exception e) {
logger.error("【请求异常】方法:{},耗时:{}ms,异常:{}",
methodName, System.currentTimeMillis() - startTime, e.getMessage(), e);
throw e;
}
}
}
注意:@Around 中打印入参和出参时,注意脱敏,避免记录密码等敏感信息。
检查清单
写完代码后逐条核对:
| 检查项 | 要求 |
|---|---|
| 代码中没有任何 System.out.println() 或 e.printStackTrace() | 所有日志输出必须使用 SLF4J Logger |
| 所有日志都通过 org.slf4j.Logger 输出 | 使用 SLF4J API,而不是 Log4j/Logback 的直接 API |
| Logger 声明为 private static final | 避免重复创建 Logger 实例 |
| 使用 {} 占位符,不用 + 拼接字符串 | 利用 SLF4J 的延迟评估特性,避免不必要的字符串拼接开销 |
| 日志级别使用正确 | ERROR/WARN/INFO/DEBUG/TRACE 按场景合理使用 |
| 生产环境配置了 logback-spring.xml | 且 DEBUG 级别已关闭 |
| 日志文件配置了按天滚动和保留策略 | 避免日志文件无限增长 |
| 敏感信息已被脱敏或排除 | 密码、Token、手机号等敏感信息不记录或脱敏记录 |
| (进阶)已经用 AOP 实现了统一的接口日志切面 | 业务代码零侵入,统一记录请求/响应日志 |
| (进阶,Spring Boot 3.4+)考虑开启结构化日志 | 方便 ELK 等日志平台接入和分析 |
写在最后的话
从 System.out.println 到专业的日志系统,看起来只是一行代码的变化,背后却是从学生项目思维 到企业级工程思维的跃迁。日志是生产环境的眼睛,没有它,线上出了问题就只能在黑暗中摸索。