Java开发经验——阿里巴巴编码规范实践解析4

摘要

本文主要介绍了阿里巴巴编码规范中关于日志处理的相关实践解析。强调了使用日志框架(如 SLF4J、JCL)而非直接使用日志系统(如 Log4j、Logback)的 API 的重要性,包括解耦日志实现、统一日志调用方式等好处。同时,还涉及了日志文件的保存规范、扩展日志的命名方式、日志输出时字符串拼接的占位符方式、日志级别的开关判断以及避免重复打印日志等多方面的内容,旨在提升日志系统的可维护性、性能和合规性。

1. 【强制】应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架(SLF4J、JCL---Jakarta Commons Logging)中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。

说明:日志框架(SLF4J、JCL--Jakarta Commons Logging)的使用方式(推荐使用 SLF4J)

复制代码
使用 SLF4J:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Test.class);

使用 JCL:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
private static final Log log = LogFactory.getLog(Test.class);

这是面向接口编程思想在日志系统中的体现,使用**门面模式(Facade Pattern)**的日志框架如 SLF4J,可以将日志 API 与具体实现解耦。主要好处包括:

1.1. ✅ 解耦日志实现

  • 直接使用 Log4jLogback,代码就"绑死"在某个实现上。
  • 如果以后想从 Log4j 切换为 Logback,需要大规模修改代码。
  • 使用 SLF4J 接口编程,只需要更换依赖包即可,无需改业务代码。

1.2. ✅ 日志调用方式统一

  • 所有类都用统一的 API,比如 LoggerFactory.getLogger(...)
  • 日志格式、等级统一,便于维护和查错。

1.3. ❌ 错误示例(直接使用 Log4j)

复制代码
import org.apache.log4j.Logger;

public class UserService {
    private static final Logger logger = Logger.getLogger(UserService.class);

    public void createUser() {
        logger.info("创建用户...");
    }
}

如果以后换用 Logback,就得改成用 ch.qos.logback 的类,改动大。

1.4. ✅ 正确示例(使用 SLF4J)

复制代码
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class UserService {
    
    private static final Logger logger = LoggerFactory.getLogger(UserService.class);

    public void createUser() {
        logger.info("创建用户...");
    }
}

🔧 此时你在 pom.xml 中引入:

复制代码
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
</dependency>

将来你想换成 Log4j 也很简单,只需换 Log4j 的绑定依赖即可,无需改业务代码。

2. 【强制】日志文件至少保存 15 天,因为有些异常具备以"周"为频次发生的特点。对于当天日志,以"应用名.log"来保存,保存在/{统一目录}/{应用名}/logs/目录下,过往日志格式为:{logname}.log.{保存日期},日期格式:yyyy-MM-dd

2.1. 日志保留周期要求

"日志文件至少保存 15 天"

  • 有些问题并非每天都发生,而是按周循环出现(如每周一的定时任务、周末批处理等);
  • 如果日志只保留几天,可能无法回溯历史问题;
  • 因此,强制日志保留至少 15 天,以便问题排查。

2.2. 当前日志文件

  • 命名规则:应用名.log
  • 存储路径:/{统一目录}/{应用名}/logs/

例如:

复制代码
/data/apps/user-service/logs/user-service.log

2.3. 历史日志文件

  • 命名规则:{logname}.log.{保存日期}
  • 日期格式:yyyy-MM-dd

例如:

复制代码
/data/apps/user-service/logs/user-service.log.2025-05-27
/data/apps/user-service/logs/user-service.log.2025-05-26

2.4. Logback 配置(以 Spring Boot 工程为例)

以下是一个使用 SLF4J + Logback,满足该规范的配置片段:

2.4.1. logback-spring.xml

复制代码
<configuration>

  <!-- 定义日志目录和应用名 -->
  <property name="LOG_HOME" value="/data/apps/user-service/logs" />
  <property name="APP_NAME" value="user-service" />

  <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!-- 当前日志文件 -->
    <file>${LOG_HOME}/${APP_NAME}.log</file>

    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <!-- 过往日志文件命名 -->
      <fileNamePattern>${LOG_HOME}/${APP_NAME}.log.%d{yyyy-MM-dd}</fileNamePattern>

      <!-- 保留历史日志天数 -->
      <maxHistory>15</maxHistory>
    </rollingPolicy>

    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
    </encoder>
  </appender>

  <root level="INFO">
    <appender-ref ref="FILE"/>
  </root>

</configuration>

3. 【强制】根据国家法律,网络运行状态、网络安全事件、个人敏感信息操作等相关记录,留存的日志不少于六个月,并且进行网络多机备份。

3.1. 合规性要求(《网络安全法》第21条等)

国家法律要求对"网络运行、网络安全事件、用户操作敏感数据"等日志至少留存6个月。

必须记录以下行为,并保留:

  • 网络运行状态(服务启停、接口调用、异常状态)
  • 网络安全事件(攻击、入侵、漏洞、越权访问)
  • 个人敏感信息操作(查看、导出、修改用户敏感数据等)

⚠️ 否则将面临罚款、吊销许可证等法律责任。

3.2. 留存时间:不少于6个月

  • 日志存储不能只保留15天或一个月,而要长期归档保存6个月以上。

3.3. 多机备份要求(防单点失败)

所谓"网络多机备份",是指:

  • 日志不仅保存在本机,还应同步到另一台机器或远程日志服务器;
  • 防止机器损坏或系统故障导致日志丢失。

3.4. 如何实现这项规范?(示例方案)

方案一:本地持久 + 多机远程备份(推荐)

本地日志配置保留 180 天(Logback)

复制代码
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
  <fileNamePattern>${LOG_HOME}/${APP_NAME}.log.%d{yyyy-MM-dd}</fileNamePattern>
  <maxHistory>180</maxHistory> <!-- 保留180天 -->
</rollingPolicy>

使用 rsync / scp / 日志采集工具进行多机备份

  • 定期同步本地日志到远程备份机(如每小时同步一次):

    rsync -az /data/apps/user-service/logs/ logserver:/backup/user-service/

或使用 ELK/EFK 等集中采集日志:

  • Filebeat + Elasticsearch + Kibana
  • Flume + HDFS
  • Kafka + Logstash

方案二:Spring Boot + ELK 日志采集方案

  1. 使用 Filebeat 收集本地日志
  2. 发往 Logstash → Elasticsearch
  3. 设置 Elasticsearch 索引生命周期(ILM)策略,保留180天日志
  4. Kibana 可视化查询、安全审计

|-------------|------------------------|--------------------------------|
| 要求项 | 解释 | 实现方式 |
| 保留日志时间 ≥6个月 | 符合国家《网络安全法》《等级保护2.0》要求 | 日志文件保留180天或存入长期归档系统(如HDFS、ES) |
| 多机备份 | 避免日志因故障丢失 | rsync/rsyslog/Filebeat → 日志服务器 |
| 记录重点内容 | 网络运行、异常事件、敏感信息操作 | 通过埋点/日志拦截记录操作 |

4. 【强制】应用中的扩展日志(如打点、临时监控、访问日志等) 命名方式:appName_logType_logName.log。logType:日志类型,如 stats / monitor / access 等;logName:日志描述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。

说明:推荐对日志进行分类,将错误日志和业务日志分开放,便于开发人员查看,也便于通过日志对系统进行及时监控。

正例:mppserver 应用中单独监控时区转换异常,如:mppserver_monitor_timeZoneConvert.log

扩展日志必须使用统一规范的命名格式,以提高可读性、可分类性与可运维性。

4.1. 如何理解这条规则?

在实际开发中,我们的系统往往输出多种不同目的的日志,比如:

|------------|------------------|
| 类型 | 示例内容 |
| 访问日志 | 用户访问接口的信息 |
| 监控日志 | 系统关键指标、性能监控等 |
| 打点日志 | 埋点数据、用户行为路径 |
| 业务操作日志 | 某个业务流程的处理记录 |
| 错误日志 | 异常堆栈、错误信息 |

这些日志如果都输出到一个文件中,就会:

  • 不便查找
  • 不利于自动监控
  • 日志量爆炸,影响性能

解决办法:分类输出日志,并采用统一命名规范

命名规则:

复制代码
appName_logType_logName.log

|-------------|--------------------------------------|---------------------|
| 部分 | 说明 | 示例 |
| appName | 应用名 | mppserver |
| logType | 日志类型(如 access / stats / monitor) | monitor |
| logName | 日志内容描述(模块或业务名称) | timeZoneConvert |

好处:

  • 文件名一看就知道日志内容,方便开发 & 运维;
  • 日志文件容易归类,便于定向排查、自动告警等;
  • 可以设置不同的日志滚动策略与等级。

正例示例分析

复制代码
mppserver_monitor_timeZoneConvert.log

含义如下:

|---------------------|-----------------|
| 部分 | 含义 |
| mppserver | 应用名 |
| monitor | 日志类型:监控日志 |
| timeZoneConvert | 日志主题:时区转换相关 |

这个日志就可能记录了:

复制代码
[INFO] 2025-05-27 10:00:01 时区转换失败,源=GMT+8,目标=UTC+1,用户ID=123

4.2. 日志分类输出示例(以 Logback 为例)

logback-spring.xml****示例配置:

复制代码
<property name="LOG_PATH" value="/data/apps/mppserver/logs"/>

<!-- 监控日志 -->
<appender name="MONITOR_TIMEZONE" class="ch.qos.logback.core.rolling.RollingFileAppender">
  <file>${LOG_PATH}/mppserver_monitor_timeZoneConvert.log</file>
  <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <fileNamePattern>${LOG_PATH}/mppserver_monitor_timeZoneConvert.log.%d{yyyy-MM-dd}</fileNamePattern>
    <maxHistory>15</maxHistory>
  </rollingPolicy>
  <encoder>
    <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level - %msg%n</pattern>
  </encoder>
</appender>

<!-- 访问日志 -->
<appender name="ACCESS_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
  <file>${LOG_PATH}/mppserver_access_gateway.log</file>
  <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <fileNamePattern>${LOG_PATH}/mppserver_access_gateway.log.%d{yyyy-MM-dd}</fileNamePattern>
    <maxHistory>15</maxHistory>
  </rollingPolicy>
  <encoder>
    <pattern>%msg%n</pattern>
  </encoder>
</appender>

<!-- 日志分类写入 -->
<logger name="com.example.monitor.TimeZoneService" level="INFO" additivity="false">
  <appender-ref ref="MONITOR_TIMEZONE"/>
</logger>

<logger name="com.example.gateway.AccessLogger" level="INFO" additivity="false">
  <appender-ref ref="ACCESS_LOG"/>
</logger>

5. 【强制】在日志输出时,字符串变量之间的拼接使用占位符的方式。

说明:因为 String 字符串的拼接会使用 StringBuilder 的 append() 方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。

正例:logger.debug("Processing trade with id : {} and symbol : {}", id, symbol);

5.1. ✳️ 避免不必要的字符串拼接开销

假设我们使用拼接方式:

复制代码
logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);

即使当前日志级别是 INFO**,不会真正输出这条** DEBUG****日志,但 拼接操作仍会执行

复制代码
String s = "Processing trade with id: " + id + " and symbol: " + symbol;
// 实际生成一个新的 String 对象,性能浪费

这在高并发或大量日志打印场景下性能损耗非常明显。

5.2. ✳️ 占位符方式性能更优

SLF4J / Log4j 等日志门面在内部做了优化,只有当对应日志级别开启时才会替换 {}****:

复制代码
logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
  • 如果 DEBUG****级别关闭, 连字符串拼接都不会做
  • 性能更优,垃圾更少(无多余 StringBuilder 创建);

5.3. ✅正确与错误用法对比

|------------------------------------------------------------------------|---------------------------------------------------------------------|
| ❌错误用法 | ✅正确用法 |
| logger.info("User: " + username + " login success"); | logger.info("User: {} login success", username); |
| logger.debug("Order total: " + total + ", discount: " + discount); | logger.debug("Order total: {}, discount: {}", total, discount); |

5.4. ✅SLF4J 占位符说明

复制代码
logger.info("User {} logged in from IP {}", username, ip);
  • {}是占位符,不需要写成 **{0}**, {1}****;

  • 变量顺序一一对应;

  • 也可以传数组或异常对象:

    logger.error("Request failed: {}", e.getMessage(), e); // 可打印异常栈

5.5. ✅附加示例:错误与业务日志对比

复制代码
String orderId = "ORD123";
String product = "Camera";
BigDecimal price = new BigDecimal("1999.00");

// ❌ 错误方式(始终拼接)
logger.debug("Creating order: " + orderId + ", product=" + product + ", price=" + price);

// ✅ 推荐方式
logger.debug("Creating order: {}, product={}, price={}", orderId, product, price);

6. 【强制】对于 trace / debug / info 级别的日志输出,必须进行日志级别的开关判断:

说明:虽然在 debug(参数) 的方法体内第一行代码 isDisabled(Level.DEBUG_INT) 为真时(Slf4j 的常见实现 Log4j 和Logback) , 就直接 return, 但是参数可能会进行字符串拼接运算。 此外, 如果 debug(getName()) 这种参数内有getName() 方法调用,无谓浪费方法调用的开销。

复制代码
正例:
// 如果判断为真,那么可以输出 trace 和 debug 级别的日志
if (logger.isDebugEnabled()) {
logger.debug("Current ID is: {} and name is: {}", id, getName());
}

6.1. 为什么要加 logger.isDebugEnabled() 判断?

防止不必要的函数调用和拼接操作,即使我们使用了占位符 {},但传参中包含方法调用或对象构造时,这些操作仍然会执行:

6.2. ❌ 示例(不加判断):

复制代码
logger.debug("Current ID is: {} and name is: {}", id, getName());
  • 即使 DEBUG 日志关闭了,
  • getName() 这个函数还是会执行,可能造成性能浪费或副作用!

6.3. ✅ 示例(加判断):

复制代码
if (logger.isDebugEnabled()) {
    logger.debug("Current ID is: {} and name is: {}", id, getName());
}
  • 如果日志级别关闭,整个代码块不会执行
  • 避免无谓函数调用,提高性能

6.4. 有些方法计算成本高或可能抛异常

举个例子:

复制代码
logger.debug("Big JSON result: {}", toJSONString(largeObject));
  • toJSONString() 比较耗时;
  • 如果 DEBUG 没开启,这个方法白执行了;
  • 有可能还抛异常,影响主流程!

这时候最好加判断:if (logger.isDebugEnabled())

6.5. 正确写法示例

复制代码
if (logger.isDebugEnabled()) {
    logger.debug("Current ID is: {} and name is: {}", id, getName());
}

如果 getName() 是一个代价比较高的方法,或者日志中拼接了庞大的对象(如 Map、JSON),建议使用这种写法。

6.6. 其他级别也适用

|---------|---------------------------|-----------|
| 日志级别 | 判断方法 | 适用场景 |
| trace | logger.isTraceEnabled() | 最低级别,性能敏感 |
| debug | logger.isDebugEnabled() | 开发调试时大量使用 |
| info | 一般不加判断(轻量) | 可省略 |
| warn | 通常不加判断 | 可省略 |
| error | 不需要判断 | 永远输出 |

7. 【强制】避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置 additivity=false

正例:<logger name="com.taobao.dubbo.config" additivity="false">

7.1. 如何理解 additivity=false

7.1.1. 📌 additivity 是什么?

在日志系统(如 Logback、Log4j)中,logger 是有层级结构的,例如:

复制代码
com
 └── taobao
     └── dubbo
         └── config
  • 每个层级的 logger 默认 会把日志向上传递 到父 logger(这叫 "additivity")。
  • 如果不禁止传递(即 additivity=true,默认值),那么日志可能被父 logger 重复处理并输出

7.1.2. ❌ 问题示例:重复日志打印

你配置了两个 logger:

复制代码
<logger name="com.taobao.dubbo.config">
  <appender-ref ref="A1"/>
</logger>

<root>
  <appender-ref ref="A2"/>
</root>

如果 additivity=true(默认):

  • com.taobao.dubbo.config 的日志:
    • 会被 A1 打一次
    • 然后"冒泡"到 root,被 A2 再打一次 ❌

7.1.3. 🔁 结果:日志被打印两遍,占用两倍磁盘空间!

7.2. ✅ 正确做法:设置 additivity="false"

复制代码
<logger name="com.taobao.dubbo.config" additivity="false">
  <level value="INFO"/>
  <appender-ref ref="A1"/>
</logger>

这样:

  • 日志只输出一次A1
  • 不会再上传给父 logger(如 root)
  • ✅ 减少重复、避免浪费磁盘。

7.3. 实际示例(Logback)完整配置片段

复制代码
<configuration>
  <appender name="DUBBO_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>/app/logs/dubbo.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>/app/logs/dubbo.log.%d{yyyy-MM-dd}</fileNamePattern>
      <maxHistory>15</maxHistory>
    </rollingPolicy>
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- 👇 防止日志向上传递、重复输出 -->
  <logger name="com.taobao.dubbo.config" level="INFO" additivity="false">
    <appender-ref ref="DUBBO_LOG"/>
  </logger>

  <!-- 根日志,输出系统其他日志 -->
  <root level="INFO">
    <appender-ref ref="CONSOLE"/>
  </root>
</configuration>

|---------|------------------------------------------------------|
| 项目 | 内容 |
| 🔧 设置项 | <logger ... additivity="false"> |
| 📌 功能说明 | 防止日志上传父 logger,重复打印 |
| 🚫 如果不加 | 日志可能被打印多次,占磁盘、扰乱分析 |
| ✅ 推荐写法 | 任何定义了 appender 的子 logger,都应显式设置 additivity="false" |

8. 优秀的Spring项目中日志分类应该是怎么样?Logback配置文件应该是怎么样设计?

在一个良好结构化的 Java Spring项目 中,日志分类和 Logback配置应当遵循可读性、可维护性、按模块分类、可定位问题、环境适配几个核心原则。

8.1. ✅ 日志分类建议(按职责和层级)

通常可以按照以下分类方式命名 logger,并做等级管理:

|--------------|-------------------------------------|--------------|------------------|
| 类别/层 | 包路径示例 | log level 建议 | 说明 |
| Controller 层 | com.example.project.controller.* | INFO/WARN | 记录接口访问、参数、响应耗时等 |
| Service 层 | com.example.project.service.* | INFO/DEBUG | 业务核心逻辑,建议包含调用链信息 |
| DAO 层 | com.example.project.repository.* | DEBUG | 数据库操作,调试使用 |
| 异常处理层 | com.example.project.error.* | ERROR | 异常堆栈、关键异常处理 |
| 第三方调用层 | com.example.project.integration.* | INFO/ERROR | 外部服务接口日志 |
| 定时任务 | com.example.project.job.* | INFO/DEBUG | 定时调度相关日志 |
| 通用工具类 | com.example.project.util.* | WARN/DEBUG | 工具类、通用组件 |
| 框架组件日志 | org.springframework.* | WARN | Spring 框架日志 |
| 数据源、MyBatis | com.zaxxer.hikari, mybatis.* | WARN/INFO | 数据源和持久层日志 |

8.2. ✅ Logback 配置文件标准示例(logback-spring.xml)

这是一个功能齐全、分模块控制、环境切换灵活的样板:

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">
  <property name="LOG_HOME" value="${LOG_HOME:-logs}"/>
  <property name="APP_NAME" value="${spring.application.name:-app}"/>
  <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
  <property name="LOG_LEVEL" value="INFO"/>

  <!-- 控制台输出 -->
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder><pattern>${LOG_PATTERN}</pattern></encoder>
  </appender>

  <!-- 按天滚动的文件输出 -->
  <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${LOG_HOME}/${APP_NAME}.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${LOG_HOME}/${APP_NAME}.%d{yyyy-MM-dd}.log</fileNamePattern>
      <maxHistory>15</maxHistory>
    </rollingPolicy>
    <encoder><pattern>${LOG_PATTERN}</pattern></encoder>
  </appender>

  <!-- 异步日志,提升性能 -->
  <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
    <queueSize>1024</queueSize>
    <discardingThreshold>0</discardingThreshold>
    <neverBlock>true</neverBlock>
    <appender-ref ref="FILE"/>
  </appender>

  <!-- Spring、MyBatis、SQL 等默认组件日志 -->
  <logger name="org.springframework" level="WARN"additivity="false">
      <appender-ref ref="CONSOLE"/>
      <appender-ref ref="ASYNC_FILE"/>
  </logger>
  
  <logger name="org.mybatis" level="WARN" additivity="false">
      <appender-ref ref="CONSOLE"/>
      <appender-ref ref="ASYNC_FILE"/>
  </logger>
  
  <logger name="com.zaxxer.hikari" level="WARN" additivity="false">
      <appender-ref ref="CONSOLE"/>
      <appender-ref ref="ASYNC_FILE"/>
  </logger>

  <!-- 控制层日志,只记录 INFO 及以上,输出到 CONSOLE 和 ASYNC_FILE -->
  <logger name="com.example.project.controller" level="INFO" additivity="false">
      <appender-ref ref="CONSOLE"/>
      <appender-ref ref="ASYNC_FILE"/>
  </logger>
  
  <!-- 服务层日志,记录 DEBUG 及以上,输出到 CONSOLE 和 ASYNC_FILE -->
  <logger name="com.example.project.service" level="DEBUG" additivity="false">
      <appender-ref ref="CONSOLE"/>
      <appender-ref ref="ASYNC_FILE"/>
  </logger>
  
  <!-- 持久层日志,记录 DEBUG 及以上,输出到 ASYNC_FILE -->
  <logger name="com.example.project.repository" level="DEBUG" additivity="false">
      <appender-ref ref="ASYNC_FILE"/>
  </logger>
  
  <!-- 错误处理模块日志,只记录 ERROR,输出到 CONSOLE 和 ASYNC_FILE -->
  <logger name="com.example.project.error" level="ERROR" additivity="false">
      <appender-ref ref="CONSOLE"/>
      <appender-ref ref="ASYNC_FILE"/>
  </logger>
  
  <!-- 定时任务模块日志,记录 INFO 及以上 -->
  <logger name="com.example.project.job" level="INFO" additivity="false">
      <appender-ref ref="ASYNC_FILE"/>
  </logger>
  
  <!-- 系统集成、三方接口模块日志 -->
  <logger name="com.example.project.integration" level="INFO" additivity="false">
      <appender-ref ref="ASYNC_FILE"/>
  </logger>

  <!-- 根日志配置 -->
  <root level="${LOG_LEVEL}">
    <appender-ref ref="CONSOLE"/>
    <appender-ref ref="ASYNC_FILE"/>
  </root>
</configuration>

8.3. ✅ 附加建议

8.3.1. 按环境区分日志配置(Spring Profiles)

复制代码
<springProfile name="dev">
    <logger name="com.example.project" level="DEBUG"/>
</springProfile>

<springProfile name="prod">
    <logger name="com.example.project" level="INFO"/>
</springProfile>

8.3.2. 使用 MDC 实现链路追踪(如 traceId)

在 filter 中设置:

复制代码
MDC.put("traceId", UUID.randomUUID().toString());

在 logback pattern 中使用:

复制代码
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level [%X{traceId}] %logger - %msg%n</pattern>

8.4. ✅ 总结

|----------------------------|------------------|
| 优秀实践 | 说明 |
| 按模块分 logger | 易于查找、屏蔽某一类日志 |
| 使用 AsyncAppender | 避免 I/O 阻塞,性能更好 |
| 使用 MDC + traceId | 日志链路追踪 |
| 环境敏感日志级别 | 开发 debug,生产 info |
| 保留最近 N 天日志 | 利于问题追溯 |
| 不要用 System.out.println() | 统一日志管理 |

9. 【强制】生产环境禁止使用 System.out 或 System.err 输出或使用 e.printStackTrace() 打印异常堆栈。

说明:标准日志输出与标准错误输出文件每次 Jboss 重启时才滚动,如果大量输出送往这两个文件,容易造成文件大小超过操作系统大小限制。

复制代码
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ExampleService {
    private static final Logger logger = LoggerFactory.getLogger(ExampleService.class);

    public void doSomething() {
        try {
            // 业务逻辑...
        } catch (Exception e) {
            // ❌ 错误做法:e.printStackTrace();
            // System.out.println("出现异常:" + e.getMessage());

            // ✅ 推荐做法:
            logger.error("业务处理失败", e);
        }
    }
}

使用 logger.error("xxx", e) 输出异常,有以下优势:

  • 自动打印完整堆栈;
  • 日志等级明确(ERROR);
  • 包含上下文信息;
  • 可配置输出到不同文件或集中式日志系统(如 ELK、Loki);
  • 避免信息泄露(通过脱敏配置);
  • 支持异步写入提高性能

10. 【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字throws 往上抛出。

正例:logger.error("inputParams: {} and errorMessage: {}", 各类参数或者对象 toString(), e.getMessage(), e);

这是一个非常重要的日志输出规范要求 ,旨在保证出现异常时,日志中不仅有错误堆栈信息(异常是什么) ,还包括上下文信息(发生异常时系统在做什么) ,以便于问题排查和复现。**理解说明:"案发现场" + "异常堆栈" = 有价值的异常日志,**两类信息:

|------------|-----------------------|-------------------|
| 信息类型 | 说明 | 目的 |
| 案发现场信息 | 方法入参、操作用户、请求来源、处理上下文等 | 定位是哪个请求或数据导致的 |
| 异常堆栈信息 | Exception 对象的堆栈 | 定位代码具体出错位置 |

10.1. 正例解读

复制代码
logger.error("inputParams: {} and errorMessage: {}", request.toString(), e.getMessage(), e);

这行日志做到了:

  • {} 第一个参数 :打印 request 的内容(案发现场)。
  • {} 第二个参数:打印异常提示信息(便于快速识别异常类型)。
  • 最后的 e:打印完整异常堆栈。

日志最终可能打印成:

复制代码
ERROR com.example.UserService - inputParams: UserRequest{id=1, name='张三'} and errorMessage: java.lang.NullPointerException: xx
java.lang.NullPointerException
at com.example.UserService.getUser(UserService.java:45)
at ...

10.2. ✅ 示例:推荐做法

复制代码
public void handleRequest(UserRequest request) {
    try {
        // 业务处理
    } catch (Exception e) {
        logger.error("处理请求失败,请求参数: {}, 异常原因: {}", request, e.getMessage(), e);
        throw new BusinessException("用户处理失败", e); // 或者继续往上抛
    }
}

10.3. ❌ 反例:不包含上下文

复制代码
catch (Exception e) {
    logger.error("出错了", e); // 缺少关键参数信息
}

无法知道是哪一个请求、哪个参数导致错误,排查困难。

10.4. ✅ 再进阶:统一异常处理(推荐)

如果你用 Spring Boot,可以统一用 @ControllerAdvice 把这些信息收集起来打日志:

复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleException(HttpServletRequest request, Exception e) {
        logger.error("请求地址: {}, 请求参数: {}, 异常信息: {}", request.getRequestURI(),request.getQueryString(), e.getMessage(), e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("系统异常");
    }
}

11. 【强制】日志打印时禁止直接用 JSON 工具将对象转换成 String。

说明: 如果对象里某些 get 方法被覆写, 存在抛出异常的情况,则可能会因为打印日志而影响正常业务流程的执行。正例:打印日志时仅打印出业务相关属性值或者调用其对象的 toString() 方法。这是一个非常重要的日志打印规范,防止因为"日志打印本身"而引发系统异常。

规范理解:不要直接使用 JSON 工具(如 ObjectMapper**,** Gson**,** FastJson**)将对象序列化为字符串用于日志打印。**

11.1. 原因:

  • 有些对象的 getXxx() 方法被重写,里面可能抛异常(例如懒加载未初始化、连接已关闭等);
  • JSON 工具在序列化时会自动调用所有 getter,如果其中某个抛异常,会打断日志打印,甚至影响主业务流程。

11.2. ❌ 反例(违背规范)

复制代码
// 错误做法:直接用 JSON 工具打印整个对象
logger.info("用户信息: {}", objectMapper.writeValueAsString(user));

潜在风险:

  • 如果 user.getAccountBalance() 内部操作数据库连接,而连接已关闭,日志打印时会报错;
  • 程序可能在日志阶段抛异常,导致主流程中断。

11.3. ✅ 正例(推荐做法)

  1. 调用对象的 toString()****(前提是实现良好)

  2. 只打印业务关键字段

    // 推荐方式 1:对象已有良好的 toString 实现
    logger.info("用户信息: {}", user.toString());

    // 推荐方式 2:只打印关键属性
    logger.info("用户信息: id={}, name={}", user.getId(), user.getName());

12. 不建议使用 JSON.toJSONString(obj) 等类似方法直接打印日志

12.1. 为什么 JSON.toJSONString() 不推荐用于日志打印?

会调用对象的所有 getter 方法

复制代码
JSON.toJSONString(user)

这会自动遍历对象属性并执行 **getXxx()**方法,而这些方法中:

  • 可能含有业务逻辑;
  • 可能访问数据库(如懒加载字段);
  • 有些重写的 getter 甚至会抛异常;

结果就是: 🔥 日志打印行为影响业务执行流程,甚至导致程序异常。

12.2. 推荐做法

12.2.1. ✅ 方案 1:只打印关键字段

复制代码
logger.info("userId={}, userName={}", user.getId(), user.getName());

12.2.2. ✅ 方案 2:使用toString(),前提是你确认安全

复制代码
logger.info("user info: {}", user.toString());

⚠️ 注意:不要在 toString() 里调用会抛异常的方法。

12.3. ❓举个反例

复制代码
logger.info("用户信息:{}", JSON.toJSONString(user)); // ❌ 可能触发懒加载/空指针异常

如果 user.getBalance() 是懒加载字段,没初始化,打印时就会抛 LazyInitializationException,程序可能因此挂掉。

13. 【推荐】为了保护用户隐私,日志文件中的用户敏感信息需要进行脱敏处理。

不要在日志中输出敏感信息:姓名、身份证号、手机号、银行卡号、地址、登录密码、验证码等。这些数据如果未脱敏就出现在日志中,一旦日志泄露就会导致用户隐私泄露、触发法律风险。

13.1. 推荐做法:敏感信息日志中要脱敏处理

|----------|-----------------------------------|
| 信息类型 | 脱敏规则示例 |
| 手机号 | 136****1234 |
| 身份证号 | 110***********1234 |
| 姓名 | 王** |
| 银行卡号 | 6227********3456 |

13.2. 正例代码示例

复制代码
User user = getUser();

// 脱敏处理
String maskedPhone = DesensitizationUtil.maskPhone(user.getPhone());
String maskedIdCard = DesensitizationUtil.maskIdCard(user.getIdCard());

logger.info("用户信息 - userId: {}, phone: {}, idCard: {}", user.getId(), maskedPhone, maskedIdCard);

推荐在日志中使用 userId**、** orderId**、** uuid****等非敏感的唯一标识进行问题定位。

13.3. ❌ 反例代码(绝对禁止)

复制代码
logger.info("用户信息 - 姓名: {}, 身份证: {}, 手机号: {}", user.getName(), user.getIdCard(), user.getPhone());
// 泄露完整敏感信息,严重违规

13.4. ✅ 推荐脱敏工具类 DesensitizationUtil

复制代码
public class DesensitizationUtil {

    public static String maskPhone(String phone) {
        if (phone == null || phone.length() != 11) return phone;
        return phone.substring(0, 3) + "****" + phone.substring(7);
    }

    public static String maskIdCard(String idCard) {
        if (idCard == null || idCard.length() < 8) return idCard;
        return idCard.substring(0, 3) + "***********" + idCard.substring(idCard.length() - 4);
    }

    public static String maskName(String name) {
        if (name == null || name.length() < 2) return "*";
        return name.charAt(0) + "*".repeat(name.length() - 1);
    }
}

13.5. ✅ 日志中推荐使用哪些字段定位问题?

  • userId / accountId
  • orderId
  • uuid
  • transactionId
  • requestId(可作为链路跟踪标识)

这些字段 既不包含用户隐私,又能唯一定位问题。

博文参考

相关推荐
doiito11 天前
【Agent Harness】Gliding Horse v0.1.4.preview 发布:时间感知、闭环审计与智能增强
ai·rust·架构设计·系统设计·ai agent
doiito14 天前
Gliding Horse 时间感知系统:让 AI Agent 真正“感知“时间流逝
ai·rust·架构设计·系统设计·ai agent
万亿少女的梦16814 天前
基于Node.js和Express的旅游信息管理平台设计与开发
mysql·node.js·express·系统设计·旅游信息管理
万亿少女的梦16814 天前
基于SpringBoot与Vue的历史博物馆展品展示与游客管理系统设计
spring boot·mysql·vue·系统设计·博物馆管理系统
万亿少女的梦16815 天前
基于Spring Boot与Vue的繁星技术论坛系统设计与实现
java·spring boot·mysql·vue·系统设计
万亿少女的梦16815 天前
基于SpringBoot的考研学习交流系统设计与开发
spring boot·mysql·vue·系统设计·论坛系统
万亿少女的梦16817 天前
基于Spring Boot的天空影院电影网站系统设计与实现
java·spring boot·mysql·vue·系统设计
万亿少女的梦16817 天前
基于Spring Boot的社区管理系统设计与实现
java·spring boot·mysql·vue·系统设计
万亿少女的梦16817 天前
基于Spring Boot的楚雄旅游景区门票售卖系统设计与实现
java·spring boot·mysql·vue·系统设计