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. ✅ 解耦日志实现
- 直接使用
Log4j
或Logback
,代码就“绑死”在某个实现上。 - 如果以后想从 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 日志采集方案
- 使用 Filebeat 收集本地日志
- 发往 Logstash → Elasticsearch
- 设置 Elasticsearch 索引生命周期(ILM)策略,保留180天日志
- 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 | 应用名 |
|
logType | 日志类型(如 access / stats / monitor) |
|
logName | 日志内容描述(模块或业务名称) |
|
好处:
- 文件名一看就知道日志内容,方便开发 & 运维;
- 日志文件容易归类,便于定向排查、自动告警等;
- 可以设置不同的日志滚动策略与等级。
正例示例分析
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. ✅ 正确与错误用法对比
❌ 错误用法 | ✅ 正确用法 |
|
|
|
|
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. 其他级别也适用
日志级别 | 判断方法 | 适用场景 |
|
| 最低级别,性能敏感 |
|
| 开发调试时大量使用 |
| 一般不加判断(轻量) | 可省略 |
| 通常不加判断 | 可省略 |
| 不需要判断 | 永远输出 |
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,重复打印 |
🚫 如果不加 | 日志可能被打印多次,占磁盘、扰乱分析 |
✅ 推荐写法 | 任何定义了 appender 的子 logger,都应显式设置 |
8. 优秀的Spring项目中日志分类应该是怎么样?Logback配置文件应该是怎么样设计?
在一个良好结构化的 Java Spring项目 中,日志分类和 Logback配置应当遵循可读性、可维护性、按模块分类、可定位问题、环境适配几个核心原则。
8.1. ✅ 日志分类建议(按职责和层级)
通常可以按照以下分类方式命名 logger,并做等级管理:
类别/层 | 包路径示例 | log level 建议 | 说明 |
Controller 层 |
| INFO/WARN | 记录接口访问、参数、响应耗时等 |
Service 层 |
| INFO/DEBUG | 业务核心逻辑,建议包含调用链信息 |
DAO 层 |
| DEBUG | 数据库操作,调试使用 |
异常处理层 |
| ERROR | 异常堆栈、关键异常处理 |
第三方调用层 |
| INFO/ERROR | 外部服务接口日志 |
定时任务 |
| INFO/DEBUG | 定时调度相关日志 |
通用工具类 |
| WARN/DEBUG | 工具类、通用组件 |
框架组件日志 |
| WARN | Spring 框架日志 |
数据源、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 天日志 | 利于问题追溯 |
不要用 | 统一日志管理 |
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);
这是一个非常重要的日志输出规范要求,旨在保证出现异常时,日志中不仅有错误堆栈信息(异常是什么),还包括上下文信息(发生异常时系统在做什么),以便于问题排查和复现。理解说明:“案发现场” + “异常堆栈” = 有价值的异常日志,两类信息:
信息类型 | 说明 | 目的 |
案发现场信息 | 方法入参、操作用户、请求来源、处理上下文等 | 定位是哪个请求或数据导致的 |
异常堆栈信息 |
| 定位代码具体出错位置 |
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. ✅ 正例(推荐做法)
- 调用对象的
toString()
(前提是实现良好) - 只打印业务关键字段
// 推荐方式 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
(可作为链路跟踪标识)
这些字段 既不包含用户隐私,又能唯一定位问题。