当前位置: 首页 > news >正文

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);

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

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(可作为链路跟踪标识)

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

博文参考

相关文章:

  • 封装一个小程序选择器(可多选、单选、搜索)
  • windows安装启动elasticsearch
  • 数据拟合实验
  • TechCrunch 最新文章 (2025-05-28)
  • 【Halcon】 affine_trans_image 算子详解
  • 构建安全高效的邮件网关ngx_mail_ssl_module
  • 【iOS】源码阅读(五)——类类的结构分析
  • 数字孪生赋能智能制造:某汽车发动机产线优化实践
  • SQL中各个子句的执行顺序
  • 亚远景-ISO 21434标准:汽车网络安全实践的落地指南
  • DBus总线详解
  • c++ 拷贝构造函数
  • vue 中的ref属性
  • Grafana-Gauge仪表盘
  • git配置(1): 根据remote自动选择账号执行commit
  • 【掌握文件操作】(下):文件的顺序读写、文件的随机读写、文件读取结束的判定、文件缓冲区
  • C++异常处理机制
  • :inline=“true“会发生什么
  • 酒店用品源头厂家推荐
  • SQL中的锁机制
  • 网站编辑如何做/苏州吴中区seo关键词优化排名
  • 网站建设工作情况总结/线上网络平台推广
  • 国外做问卷网站/制作网站公司
  • 网站404怎么做视频教程/seo核心技术排名
  • 网站开发建设付款方式/企业网站排名优化方案
  • 成都模板网站建设服务/提高基层治理效能