Spring Boot 中统一日志输出

作者:old wang 发布时间: 2022-06-01 阅读量:2 评论数:0

无论是开发调试,还是生产环境排查问题,日志通常都是最直接的线索。

常见用途包括:

  • 记录接口请求;

  • 记录业务关键流程;

  • 记录异常堆栈;

  • 排查线上问题;

  • 分析系统运行状态;

  • 辅助定位性能问题。

在项目早期,很多人可能会直接使用:

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 + Logback

1. 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

其中:

配置

说明

%d{yyyy-MM-dd HH:mm:ss}

日志时间

%5p

日志级别

%t

当前线程名

%logger{36}

Logger 名称

%msg

日志内容

%n

换行

五、生产环境不要随便开 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.log

5. 保留 traceId

如果是分布式系统,日志中最好带上 traceId

这样可以串联一次请求在多个服务中的日志。

结论

项目中不建议使用 System.out.println() 作为正式日志方案。

更推荐使用:

SLF4J + Logback + Lombok @Slf4j

其中:

  • SLF4J 负责提供统一日志 API;

  • Logback 负责具体日志输出;

  • Lombok @Slf4j 用于简化 Logger 声明;

  • logback.xml 用于统一配置日志格式和级别。

实际项目中,日志要注意:

  1. 合理选择日志级别;

  2. 统一日志格式;

  3. 异常日志要打印完整堆栈;

  4. API 请求日志可以统一拦截记录;

  5. 不要输出敏感信息;

  6. 重要错误要接入告警;

  7. 分布式系统中要保留 traceId。

日志不是简单地把内容打印出来,而是要让问题发生时能够快速定位原因。统一日志格式、合理日志级别和完整异常上下文,才是项目中真正有价值的日志实践。

评论