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

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

博文参考

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/bicheng/82900.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

各个链接集合

golang学习&#xff5e;&#xff5e;_从数组中取一个相同大小的slice有成本吗?-CSDN博客 框架 golang学习&#xff5e;&#xff5e;_从数组中取一个相同大小的slice有成本吗?-CSDN博客 golang k8s学习_容器化部署和传统部署区别-CSDN博客 K8S rabbitmq_rabbitmq 广播-CSD…

Cesium 展示——获取鼠标移动、点击位置的几种方法

文章目录 需求分析:这里我们用到了几种常见的鼠标事件1. 获取鼠标移动的位置2. 获取鼠标点击的位置3. 添加面4. 示例代码需求 获取指定断面的 label 分析:这里我们用到了几种常见的鼠标事件 1. 获取鼠标移动的位置 viewer.screenSpaceEventHandler.setInputAction((moveme…

技术分享 | Oracle SQL优化案例一则

本文为墨天轮数据库管理服务团队第70期技术分享&#xff0c;内容原创&#xff0c;作者为技术顾问马奕璇&#xff0c;如需转载请联系小墨&#xff08;VX&#xff1a;modb666&#xff09;并注明来源。 一、问题概述 开发人员反映有条跑批语句在测试环境执行了很久都没结束&…

$3 #12阶段三小结Java se

$3 #12 阶段三小结 Java se 基本没有新学什么知识点 感觉 基础语法 和高级语法 已经学完了 现在就是得学习 一些企业开发的框架 以及项目架构的思维 比如一个产品 从需求分析 到功能模块设计 到接口文档定义 数据库建立 前端接口页面设计 后端接口开发的步骤 然后现在比…

华为云Flexus+DeepSeek征文 | 初探华为云ModelArts Studio:部署DeepSeek-V3/R1商用服务的详细步骤

华为云FlexusDeepSeek征文 | 初探华为云ModelArts Studio&#xff1a;部署DeepSeek-V3/R1商用服务的详细步骤 前言一、华为云ModelArts Studio平台介绍1.1 ModelArts Studio介绍1.2 ModelArts Studio主要特点1.3 ModelArts Studio使用场景1.4 ModelArts Studio产品架构 二、访问…

易经六十四卦象解释数据集分享!智能体知识库收集~

今天给大家分享一个易经六十四卦象解释数据集 &#xff0c;继续来积累AI相关的资料。 六十四卦&#xff0c;记载于《易经》&#xff0c;每一卦的图像均由两个八卦上下组合而成&#xff0c;每一卦各有六个爻。南宋朱熹说&#xff0c;先画八卦于内&#xff0c;后画八卦于外&#…

1 µs = 10⁻⁶ s

1 s 10⁰ s 1 ms 10⁻ s 1 s 10⁻⁶ s 1 ns 10⁻⁹ s 1 ps 10⁻ s 1 fs 10⁻⁵ s ⏱️ 时间单位&#xff08;十进制&#xff09; 符号单位名称10 的幂次s秒&#xff08;second&#xff09;10⁰ms毫秒&#xff08;millisecond&#xff09;10⁻s微秒&#xff08;microseco…

webrtc初了解

1. webrtc的简介 一、WebRTC 是什么&#xff1f; Web Real-Time Communication&#xff08;网页实时通信&#xff09;&#xff0c;是浏览器原生支持的实时音视频通信技术&#xff0c;无需安装插件或客户端&#xff0c;可直接在浏览器之间实现点对点&#xff08;P2P&#xff09…

从数据持久化到网络通信与OpenCV:Qt应用程序开发的深度探索与实战

文章目录 前言一、QSettings&#xff1a;轻量级数据持久化方案1.1 QSettings 主要特点1.2 QSettings 常用函数整理 二、数据库2.1 连接SQLite数据库2.2 建表2.3 增删改 三、网络编程3.1 网络分层3.2 IP地址3.3 端口号3.4 基于TCP的Socket通信3.4 相关接口3.4.1核心类3.4.2 通信…

经典SQL查询问题的练习第一天

首先有三张表&#xff0c;学生表、课程表、成绩表 student:studentId,studentName; course:courseId&#xff0c;courseName,teacher; score:score,studentId,courseId; 接着有以下几道题目&#xff1a; ①查询课程编号为‘0006’的总成绩&#xff1a; 首先总成绩&#x…

企业级网络管理实战:Linux、云与容器的深度融合与优化

在数字化转型浪潮下&#xff0c;企业网络架构日益复杂&#xff0c;Linux系统、云计算与容器技术成为构建高效、灵活网络的核心要素。本文将从技术原理、实践方案、优化策略三个维度&#xff0c;深度解析企业级网络管理中的关键技术&#xff0c;助力企业打造稳定、安全、可扩展的…

信号与系统速成-1.绪论

b站浙大教授虽然讲的比较细&#xff0c;但是太慢了&#xff0c;不适合速成 祖师爷奥本海姆的MIT课程好像和我们教材的版本不太匹配&#xff0c;但是讲的很不错 慕课上也有很多资源&#xff0c;比如信号与系统 - 网易云课堂 同站博主篱笆外的xixi的文章也挺不错 最终我还是选…

缓存架构方案:Caffeine + Redis 双层缓存架构深度解析

在高并发、低延迟的现代互联网系统中&#xff0c;缓存是提升系统性能和稳定性的重要手段。随着业务复杂度的增长&#xff0c;单一缓存方案&#xff08;如仅使用Redis或仅使用本地缓存&#xff09;已难以满足高性能与一致性需求。 本文将围绕 Caffeine Redis 的双层缓存架构展…

【Elasticsearch】track_total_hits

在 Elasticsearch 中&#xff0c;track_total_hits 是一个查询参数&#xff0c;用于控制是否精确计算搜索结果的总命中数&#xff08;total hits&#xff09;。默认情况下&#xff0c;Elasticsearch 在某些情况下可能会对总命中数进行近似计算&#xff0c;以提高性能。track_to…

智能手机上用Termux安装php+Nginx

Termux的官方网站&#xff1a;Termux | The main termux site and help pages. 以下是在 Termux 上安装和配置 PHP Nginx 的完整流程总结&#xff0c;包含关键步骤和命令&#xff1a; 一、安装依赖 pkg update && pkg upgrade # 更新包列表和系统pkg install nginx p…

电脑开机后出现bootmgr is conmpressed原因及解决方法

最近有网友问我为什么我电脑开机后出现BOOTMGR is compressed&#xff0c;这个提示意思是:意思是启动管理器被压缩了&#xff0c;即使重启也无法正常进入系统。原因有很多&#xff0c;大部分是引导出现问题&#xff0c;或选错了启动硬盘所导致的&#xff0c;下面我们来详细分析…

服务发现Nacos

目录 Nacos server 安装 注册服务到Nacos server 接口访问Nacos server中的已注册服务 Nacos控制台介绍 Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 在分布式服务应用中&#xff0c;各类服务需要统一的注册、统一的管理&#xff0c;这个组件工具…

并查集 c++函数的值传递和引用传递 晴神问

目录 学校的班级个数 手推7个班级&#xff0c;答案17&#xff1f;怀疑人生 破案了&#xff0c;应该是6个班。 破案了&#xff0c;原来写的是 unionxy(a, b, father); c if两个数同时为正或为负 简洁写法 可以用位运算&#xff1f; c可以这样赋值吗&#xff1f;ab2 典型…

Qt Creator快捷键合集

前言 QtCreator是一款跨平台的IDE,专为Qt开发设计,支持C/C++/JS/Python编程,支持设备远程调试,支持代码高亮,集成帮助文档,原生支持cmake和git,确实是一款朴实而又强大的集成开发环境,让人有种爱不释手的感觉 编辑 功能快捷键复制Ctrl + C粘贴Ctrl + V剪切Ctrl + X代…

docker网络相关内容详解

一、docker与k8s 一、Docker 核心解析 1. Docker 定义与架构 本质&#xff1a; 容器化平台&#xff08;构建容器化应用&#xff09;、进程管理软件&#xff08;守护进程管理容器生命周期&#xff09;。客户端&#xff08;docker cli&#xff09;与服务端&#xff08;docker ser…