告别System.out.println,打造专业日志系统

在真实项目中,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">
&lt;!-- 1. 控制台输出 --&gt;
&lt;appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"&gt;
    &lt;encoder&gt;
        &lt;pattern&gt;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n&lt;/pattern&gt;
        &lt;charset&gt;UTF-8&lt;/charset&gt;
    &lt;/encoder&gt;
&lt;/appender&gt;
&lt;!-- 2. 文件输出 - 按天滚动 --&gt;
&lt;appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"&gt;
&lt;file&gt;logs/application.log&lt;/file&gt;
&lt;rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"&gt;
&lt;fileNamePattern&gt;logs/application.%d{yyyy-MM-dd}.%i.log&lt;/fileNamePattern&gt;
&lt;maxHistory&gt;30&lt;/maxHistory&gt;
&lt;totalSizeCap&gt;3GB&lt;/totalSizeCap&gt;
&lt;timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"&gt;
&lt;maxFileSize&gt;100MB&lt;/maxFileSize&gt;
&lt;/timeBasedFileNamingAndTriggeringPolicy&gt;
&lt;/rollingPolicy&gt;
&lt;encoder&gt;
&lt;pattern&gt;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n&lt;/pattern&gt;
&lt;charset&gt;UTF-8&lt;/charset&gt;
&lt;/encoder&gt;
&lt;/appender&gt;
&lt;!-- 3. 错误日志单独输出 --&gt;
&lt;appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"&gt;
&lt;file&gt;logs/error.log&lt;/file&gt;
&lt;filter class="ch.qos.logback.classic.filter.ThresholdFilter"&gt;
&lt;level&gt;ERROR&lt;/level&gt;
&lt;/filter&gt;
&lt;rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"&gt;
&lt;fileNamePattern&gt;logs/error.%d{yyyy-MM-dd}.log&lt;/fileNamePattern&gt;
&lt;maxHistory&gt;30&lt;/maxHistory&gt;
&lt;/rollingPolicy&gt;
&lt;encoder&gt;
&lt;pattern&gt;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n&lt;/pattern&gt;
&lt;charset&gt;UTF-8&lt;/charset&gt;
&lt;/encoder&gt;
&lt;/appender&gt;
&lt;!-- 4. 各环境差异化配置 --&gt;
&lt;springProfile name="dev"&gt;
&lt;root level="DEBUG"&gt;
&lt;appender-ref ref="CONSOLE" /&gt;
&lt;/root&gt;
&lt;/springProfile&gt;
&lt;springProfile name="prod"&gt;
&lt;root level="INFO"&gt;
&lt;appender-ref ref="FILE" /&gt;
&lt;appender-ref ref="ERROR_FILE" /&gt;
&lt;/root&gt;
&lt;!-- 生产环境强制关闭敏感包的DEBUG日志 --&gt;
&lt;logger name="org.hibernate.SQL" level="INFO" /&gt;
&lt;logger name="org.hibernate.type.descriptor.sql" level="INFO" /&gt;
&lt;/springProfile&gt;
&lt;!-- 5. 默认配置(未指定profile时生效) --&gt;
&lt;springProfile name="default"&gt;
&lt;root level="INFO"&gt;
&lt;appender-ref ref="CONSOLE" /&gt;
&lt;appender-ref ref="FILE" /&gt;
&lt;/root&gt;
&lt;/springProfile&gt;
</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中配置敏感字段过滤 ,自动屏蔽 passwordtokensecret 等关键词,从管道层面兜底防护,不依赖开发人员记住规范。

企业级增强: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 到专业的日志系统,看起来只是一行代码的变化,背后却是从学生项目思维企业级工程思维的跃迁。日志是生产环境的眼睛,没有它,线上出了问题就只能在黑暗中摸索。

相关推荐
无糖可可果1 小时前
从零理解 Docker:为什么我的代码在你的电脑上跑不起来?
javascript·后端
泡海椒1 小时前
轻量级 Java JSON 库推荐:jquick-json 序列化/反序列化 + 变量替换 + 字节码优化全解析
后端
触底反弹1 小时前
🔥 NestJS 从零到实战:一个 Todos CRUD 搞懂企业级后端框架的核心设计
后端·typescript·nestjs
吃饱了得干活1 小时前
从经典的三层架构到DDD:一次对“业务逻辑层”的解剖与重构
java·后端·架构
XuCoder1 小时前
面试官:什么是覆盖索引?
后端
林太白1 小时前
What did Trea work help me with in this optimization process
前端·后端
Asize1 小时前
3 种设计模式 + DI:Nest.js 后端第一课
后端·设计模式·nestjs
程序员cxuan2 小时前
一招教你在 Codex 中开启 1M 上下文
人工智能·后端·程序员
IT_陈寒2 小时前
Vite热更新失效?八成是这个配置在搞鬼
前端·人工智能·后端