无论是开发调试,还是生产环境排查问题,日志通常都是最直接的线索。
常见用途包括:
记录接口请求;
记录业务关键流程;
记录异常堆栈;
排查线上问题;
分析系统运行状态;
辅助定位性能问题。
在项目早期,很多人可能会直接使用:
System.out.println("debug message");这种写法虽然简单,但并不适合正式项目。
更推荐的方式是使用日志框架,例如:
SLF4J + Logback本文记录一下在 Spring Boot 项目中如何统一管理日志输出。
一、为什么不要使用 System.out.println
System.out.println() 最大的问题不是不能用,而是不适合用来管理项目日志。
1. 没有日志级别
正常日志框架通常会区分:
DEBUG
INFO
WARN
ERROR不同级别适合不同场景。
例如:
DEBUG:开发调试;INFO:正常业务流程;WARN:可能存在风险,但不影响主流程;ERROR:异常或错误,需要关注。
而 System.out.println() 没有级别概念。
所有内容都只是普通输出,无法按环境、按级别灵活控制。
2. 不方便统一管理
日志框架可以统一配置输出格式。
例如:
时间 日志级别 线程名 类名 日志内容也可以统一配置日志输出位置:
控制台;
文件;
按日期滚动文件;
按大小切分文件;
输出到日志平台。
而 System.out.println() 很难做到这些。
3. 不适合生产环境排查问题
生产环境中,排查问题通常需要完整上下文。
例如:
请求路径
请求参数
用户 ID
traceId
异常堆栈
线程名
执行耗时如果只是零散地输出几行 System.out.println(),很难定位问题。
4. 性能和可控性较差
在高并发场景下,大量控制台输出可能影响系统性能。
日志框架可以通过异步日志、日志级别控制、文件滚动等方式降低影响。
而 System.out.println() 不适合作为项目中的正式日志方案。
二、SLF4J 和 Logback 的关系
在 Java 项目中,常见组合是:
SLF4J + Logback1. SLF4J
SLF4J 是日志门面。
它本身不负责真正输出日志,而是提供统一的日志 API。
代码中通常这样写:
log.info("用户登录成功,userId={}", userId);业务代码只依赖 SLF4J 接口,不直接依赖具体日志实现。
2. Logback
Logback 是具体日志实现。
它负责:
日志级别控制;
日志格式化;
日志输出到控制台;
日志输出到文件;
日志滚动切分;
异常堆栈打印。
Spring Boot 默认使用的日志实现就是 Logback。
所以在 Spring Boot 项目中,一般不需要额外引入复杂配置,就可以直接使用日志能力。
三、引入依赖
如果是普通 Maven 项目,可以手动引入 SLF4J 和 Logback。
<dependencies>
<!-- SLF4J 日志接口 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.32</version>
</dependency>
<!-- Logback 日志实现 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.6</version>
</dependency>
<!-- Logback 核心依赖 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.6</version>
</dependency>
</dependencies>如果是 Spring Boot 项目,通常只需要引入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>Spring Boot 默认已经包含日志相关依赖。
如果项目中使用 Lombok,还需要引入:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>四、配置 logback.xml
可以在:
src/main/resources目录下创建:
logback.xml基础配置如下:
<configuration>
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- 时间 - 级别 [线程名] 类名 - 日志内容 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss} - %5p [%t] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 根日志级别 -->
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>输出格式示例:
2026-05-23 14:00:00 - INFO [http-nio-8080-exec-1] c.e.demo.UserController - 查询用户成功,userId=1001其中:
五、生产环境不要随便开 DEBUG
示例中配置的是:
<root level="debug">这适合本地开发环境。
生产环境一般不建议全局开启 DEBUG。
更常见的配置是:
<root level="info">
<appender-ref ref="STDOUT" />
</root>如果线上排查某个包的问题,可以只对指定包打开 DEBUG。
例如:
<logger name="com.example.demo.service" level="debug" />这样可以避免生产环境输出过多日志。
六、使用 Lombok @Slf4j
传统写法需要手动声明 Logger。
private static final Logger log = LoggerFactory.getLogger(UserService.class);使用 Lombok 的 @Slf4j 后,可以简化为:
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LogExample {
public void test() {
log.info("This is an info message");
log.debug("This is a debug message");
log.warn("This is a warning message");
log.error("This is an error message");
}
}@Slf4j 会在编译期自动生成 log 对象。
这样代码更简洁。
在项目中,Controller、Service、定时任务、监听器等类都可以使用 @Slf4j。
七、日志级别如何选择
日志级别不要随意使用。
可以按下面的习惯区分。
1. DEBUG
用于开发调试。
例如:
log.debug("查询参数:{}", queryParam);适合本地开发或临时排查问题。
生产环境一般默认关闭 DEBUG。
2. INFO
用于记录正常业务流程。
例如:
log.info("用户登录成功,userId={}", userId);适合记录系统中的关键操作。
3. WARN
用于记录异常但可恢复的情况。
例如:
log.warn("用户积分不足,userId={}, currentPoint={}", userId, point);这类日志表示需要关注,但不一定是系统错误。
4. ERROR
用于记录系统异常。
例如:
log.error("创建订单失败,userId={}, skuId={}", userId, skuId, e);ERROR 日志通常需要关注,必要时应该接入告警。
八、API 请求日志记录
在 Web 项目中,经常需要记录 API 请求日志。
简单示例:
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class ApiController {
@GetMapping("/api/test")
public String testApi() {
log.info("API request received, path={}", "/api/test");
String response = "Hello, world!";
log.info("API response, path={}, response={}", "/api/test", response);
return response;
}
}
这种方式适合简单接口。
但如果每个接口都手写请求日志,会比较重复。
实际项目中更推荐用过滤器、拦截器或 AOP 统一记录。
九、使用拦截器统一记录 API 日志
可以使用 Spring MVC 拦截器统一记录请求信息。
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
@Slf4j
public class ApiLogInterceptor implements HandlerInterceptor {
private static final String START_TIME = "startTime";
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
request.setAttribute(START_TIME, System.currentTimeMillis());
log.info("接口请求开始,method={}, uri={}, query={}",
request.getMethod(),
request.getRequestURI(),
request.getQueryString());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
Long startTime = (Long) request.getAttribute(START_TIME);
long cost = startTime == null ? 0 : System.currentTimeMillis() - startTime;
log.info("接口请求结束,method={}, uri={}, status={}, cost={}ms",
request.getMethod(),
request.getRequestURI(),
response.getStatus(),
cost);
if (ex != null) {
log.error("接口请求异常,method={}, uri={}",
request.getMethod(),
request.getRequestURI(),
ex);
}
}
}注册拦截器:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ApiLogInterceptor())
.addPathPatterns("/**");
}
}这样可以统一记录:
请求方法;
请求路径;
查询参数;
响应状态码;
接口耗时;
异常信息。
十、异常日志记录方式
记录异常时,不要只写:
log.error("系统异常:" + e.getMessage());这种写法只会输出异常信息,不一定能完整打印堆栈。
更推荐这样写:
log.error("系统异常", e);如果需要带业务上下文,可以写成:
log.error("创建订单失败,userId={}, orderId={}", userId, orderId, e);这样日志中既有业务参数,也有完整异常堆栈。
示例:
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ExceptionExample {
public void handleException() {
try {
int result = 10 / 0;
} catch (Exception e) {
log.error("计算失败", e);
}
}
}生产环境排查问题时,异常堆栈非常关键。
十一、日志中不要直接输出敏感信息
日志虽然方便排查问题,但也可能造成敏感数据泄露。
不要直接打印:
密码
Token
身份证号
银行卡号
手机号完整明文
邮箱完整明文
详细地址例如下面这种写法就不推荐:
log.info("用户登录,username={}, password={}", username, password);更好的方式是:
log.info("用户登录,username={}", username);如果确实需要输出手机号,可以脱敏:
log.info("用户手机号:{}", maskPhone(phone));简单脱敏方法:
public String maskPhone(String phone) {
if (phone == null || phone.length() != 11) {
return phone;
}
return phone.substring(0, 3) + "****" + phone.substring(7);
}日志中的敏感信息泄露,在生产环境中同样是安全问题。
十二、日志内容要有上下文
一条好的日志应该能帮助定位问题。
不推荐这样写:
log.error("处理失败");因为看不出哪个用户、哪个订单、哪个请求失败。
更推荐这样写:
log.error("订单支付失败,userId={}, orderId={}, payChannel={}",
userId,
orderId,
payChannel,
e);日志中建议保留:
业务 ID;
用户 ID;
请求路径;
traceId;
关键参数;
执行结果;
异常堆栈。
这样线上排查会更高效。
十三、实际项目中的建议
1. 本地开发可以使用 DEBUG
本地调试阶段可以开启 DEBUG。
便于观察 SQL、参数、接口流程。
2. 生产环境默认使用 INFO
生产环境建议默认 INFO。
避免日志量过大。
3. ERROR 日志接入告警
关键业务的 ERROR 日志最好接入告警。
例如:
企业微信;
钉钉;
邮件;
日志平台告警。
4. 日志按天滚动
生产环境不建议所有日志一直写一个文件。
应该按日期或大小滚动。
例如:
app.2026-05-23.log
app.2026-05-24.log5. 保留 traceId
如果是分布式系统,日志中最好带上 traceId。
这样可以串联一次请求在多个服务中的日志。
结论
项目中不建议使用 System.out.println() 作为正式日志方案。
更推荐使用:
SLF4J + Logback + Lombok @Slf4j其中:
SLF4J 负责提供统一日志 API;
Logback 负责具体日志输出;
Lombok
@Slf4j用于简化 Logger 声明;logback.xml用于统一配置日志格式和级别。
实际项目中,日志要注意:
合理选择日志级别;
统一日志格式;
异常日志要打印完整堆栈;
API 请求日志可以统一拦截记录;
不要输出敏感信息;
重要错误要接入告警;
分布式系统中要保留 traceId。
日志不是简单地把内容打印出来,而是要让问题发生时能够快速定位原因。统一日志格式、合理日志级别和完整异常上下文,才是项目中真正有价值的日志实践。