楼主: lp_bigdataapp
108 0

[其他] 超越 AOP:搜集业务操作日志的 3 个新探索 [推广有奖]

  • 0关注
  • 0粉丝

等待验证会员

学前班

40%

还不是VIP/贵宾

-

威望
0
论坛币
0 个
通用积分
0
学术水平
0 点
热心指数
0 点
信用等级
0 点
经验
20 点
帖子
1
精华
0
在线时间
0 小时
注册时间
2018-12-9
最后登录
2018-12-9

楼主
lp_bigdataapp 发表于 2025-11-24 14:58:25 |AI写论文

+2 论坛币
k人 参与回答

经管之家送您一份

应届毕业生专属福利!

求职就业群
赵安豆老师微信:zhaoandou666

经管之家联合CDA

送您一个全额奖学金名额~ !

感谢您参与论坛问题回答

经管之家送您两个论坛币!

+2 论坛币

0. 背景

在当前项目推进过程中,系统需实现对核心业务操作的全生命周期日志追踪功能。由于本系统处于整体业务链的核心位置,承担着接收上游数据并向下游传递用户操作结果的关键职责,面对复杂的业务流程和多样化的操作场景,我们决定引入精细化的操作日志机制,以提升问题排查效率,并为后续的数据分析与用户支持提供有力支撑。

0.1 收益分析

相较于常规的系统运行日志,业务操作日志专注于记录用户对关键业务实体所执行的具体行为。这类日志不仅具备审计价值,还能用于安全监控、故障定位、用户体验优化等多个方面,具体收益如下:

  • 审计与合规性:通过操作日志可追溯数据变更历史,明确“谁在何时做了什么”,满足企业内审及外部监管要求。
  • 安全保障
    • 入侵检测:识别异常访问模式或非法操作尝试。
    • 事件回溯:安全事故发生后,可通过日志还原攻击路径,评估影响范围,辅助制定防御策略。
  • 故障诊断与性能监控
    • 问题排查:当服务出现异常时,操作日志能提供上下文线索,加速根因定位。
    • 性能分析:结合执行耗时、调用频率等信息,评估系统响应能力与资源使用情况。
  • 用户行为洞察
    • 业务优化:通过对操作频次、路径偏好等数据进行挖掘,获取产品改进建议。
    • 客户服务支持:帮助客服团队还原用户操作过程,精准解决使用难题。
  • 数据恢复能力增强:在发生误删或数据损坏的情况下,操作日志可作为重要依据,辅助完成数据重建。
  • 流程改进依据:基于日志中的操作序列,识别冗余环节,推动业务流程自动化与效率提升。

0.2 核心目标

构建一个高透明度、强可追踪的日志体系,确保所有关键业务动作均可被完整记录。最终期望达成的技术效果如下:

核心要解决的问题是清晰回答:“”在“什么时间”对“哪些对象”执行了“何种操作”。

1. 初始方案:AOP 切面 + 注解(版本 1.0)

作为 Spring 框架的深度使用者,面对上述需求,自然优先考虑采用 AOP(面向切面编程)结合自定义注解的方式来实现日志捕获。该方式具有侵入性低、复用性强的优点,能够自动拦截指定方法并在其执行前后插入日志逻辑。

通过为需要记录日志的方法打上特定注解,我们可以灵活控制日志采集范围,而无需修改原有业务代码。接下来进入具体实施步骤:

1.1 自定义日志注解定义

首先创建一个注解类,用于标识需要纳入日志记录范围的目标方法。此注解将作为触发切面逻辑的开关。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Loggable {
? ? String value() default?"";
? ? // 可以添加更多的配置属性,如操作类型、级别等
}

1.2 构建 AOP 切面类

编写切面组件,用于拦截带有前述注解的方法调用。利用环绕通知(@Around)机制,在方法执行前后捕获执行上下文。

@Aspect
@Component
public class LoggingAspect {

? ? @Around("@annotation(loggable)")
? ? public Object logExecutionTime(ProceedingJoinPoint joinPoint, Loggable loggable) throws Throwable {
? ? ? ? long start = System.currentTimeMillis();
? ? ? ??
? ? ? ? Object proceed = joinPoint.proceed(); // 执行目标方法

? ? ? ? long executionTime = System.currentTimeMillis() - start;
? ? ? ??
? ? ? ? // 记录日志的逻辑
? ? ? ? logger.info(joinPoint.getSignature() +?" executed in "?+ executionTime +?"ms");
? ? ? ??
? ? ? ??return?proceed;
? ? }
}

1.3 配置 Spring AOP 并应用注解

启用 Spring 的 AOP 功能,并在目标服务方法上添加自定义注解,完成切面绑定。

@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
? ? // 可能还需要其他的配置或Bean定义
}

public class SomeService {
? ??
? ? @Loggable
? ? public void someBusinessMethod(Object someParam) {
? ? ? ? // 业务逻辑
? ? }
}

1.4 实现日志内容生成逻辑

在切面内部实现详细的日志构造逻辑,包括但不限于:方法参数、返回结果、执行时长、抛出异常等信息的提取与持久化。

@Autowired
private Logger logger; // 例如,通过SLF4J获取的Logger

@Around("@annotation(loggable)")
public Object logBusinessOperation(ProceedingJoinPoint joinPoint, Loggable loggable) throws Throwable {
? ? // 方法执行前的逻辑,例如记录开始时间、方法参数等
? ? long start = System.currentTimeMillis();
? ? try {
? ? ? ? Object result = joinPoint.proceed(); // 执行目标方法
? ? ? ? // 方法执行后的逻辑,例如记录结束时间、返回值等
? ? ? ??return?result;
? ? } catch (Exception e) {
? ? ? ? // 异常处理逻辑,如记录异常信息
? ? ? ? throw e;
? ? } finally {
? ? ? ? long executionTime = System.currentTimeMillis() - start;
? ? ? ? // 构建日志信息并记录
? ? ? ? logger.info("{} executed in {} ms", joinPoint.getSignature(), executionTime);
? ? }
}

1.5 方案小结

至此,基础框架已搭建完毕,只需部署上线即可开始收集用户操作日志。

然而在实际运行中发现,该方案存在一定局限性:

  • 日志信息颗粒度不足:切面所能获取的上下文较为有限,难以拼接出完整且具语义的操作描述。
  • 缺乏业务语境感知:注解本身不具备业务含义,无法区分不同操作类型(如“创建订单” vs “关闭工单”)。
  • 跨服务/多表操作断连:对于涉及多个服务或数据库表的复合操作,各环节日志彼此孤立,无法形成统一的操作链条。

针对以上痛点,有必要对现有方案进行迭代升级。

2. 升级方案:AOP + SpEL 表达式(版本 2.0)

在初版实践中我们观察到,生成的日志内容多为固定模板,例如“XXX 修改了项目”、“XXX 删除了风险项”。由于无法动态提取参数中的业务实体信息(如ID、名称),导致日志缺乏细节。

为此,我们引入 Spring Expression Language(SpEL)表达式机制,允许在注解中嵌入动态变量引用,从而实现更丰富的日志内容渲染,例如:“XXX 修改了项目 ID=001 的配置”。

2.1 SpEL 表达式简介

SpEL 是 Spring 提供的强大表达式语言,支持在运行时解析对象属性、方法调用、条件判断等。将其集成至日志注解中,可在切面执行期间动态计算日志文本中的占位符值。

2.2 表达式的定义方式

在自定义注解中增加 String 类型字段,用于接收 SpEL 表达式字符串,如:value = "#project.id", targetName = "#user.name" 等,分别表示从方法参数中提取项目ID和用户名。

2.3 表达式的实际运用

在切面逻辑中,借助 Spring 的 ExpressionParser 对表达式进行求值,结合 JoinPoint 获取运行时参数映射,最终将真实值填充到日志模板中,生成更具可读性的操作描述。

2.4 方案总结

通过融合 SpEL,显著提升了日志的信息密度与业务相关性,使得每条记录都能准确反映具体操作对象和行为内容。但与此同时,也带来了更高的维护成本——需手动编写表达式,且易出错。此外,仍未能彻底解决跨方法调用的日志串联问题。

3. 进阶方案:基于 Binlog 与时间窗口的日志重建(版本 3.0)

为了突破传统切面方案在分布式环境下的局限,我们探索了一种全新的思路:不再依赖代码层面的日志埋点,而是从数据库变更流入手,结合时间窗口聚合机制,重构完整的业务操作轨迹。

3.1 Binlog 数据捕获

MySQL 的 Binlog 记录了所有数据变更操作(INSERT/UPDATE/DELETE)。通过监听这些日志流(如使用 Canal 或 Debezium),可以实时感知每一笔数据变动的发生。

3.2 存在问题与设计思路

单纯依赖 Binlog 存在两个主要挑战:

  • 无法直接获取操作者身份(即“谁”做的);
  • 难以判断多个变更是否属于同一业务动作。

解决方案是:在应用层将用户会话信息与事务操作关联,并通过唯一操作ID注入上下文。随后在消费端根据该标识和时间邻近性,对多个 Binlog 事件进行归集,还原成一条完整的业务操作日志。

3.3 整体架构设计

系统由以下模块组成:

  • 业务服务层:在关键操作中生成操作上下文,包含用户ID、操作类型、事务ID等;
  • 数据库层:产生包含数据变更的 Binlog 日志;
  • 日志采集器:订阅 Binlog 流,提取变更数据;
  • 上下文匹配引擎:将变更事件与前置记录的操作上下文进行关联;
  • 聚合存储层:按时间窗口合并事件,生成最终的操作日志。

3.4 方案总结

该方案摆脱了对代码侵入的依赖,实现了跨服务、跨模块的操作日志统一采集,尤其适用于微服务架构下复杂事务的追踪。虽然实现复杂度较高,但其完整性与可靠性远超传统方案,是未来演进的重要方向。

4. 结语

从业务操作日志的设计出发,我们经历了从简单切面到动态表达式,再到基于数据流的日志重建之路。每一次升级都是对系统可观测性边界的拓展。选择何种方案应结合团队技术栈、系统复杂度与运维成本综合权衡。理想的状态是在保证准确性的同时,尽可能降低开发与维护负担。

首先,我们来简单了解一下 SpEL。它并不是一种特别复杂的新技术,实际上在日常开发中,很多开发者都在无形中使用过 SpEL。

根据官方定义,SpEL(Spring Expression Language)是 Spring 框架中的表达式语言,主要用于在运行时对表达式进行求值和处理。它提供了一种灵活的方式,用于访问和操作对象的属性、调用方法以及执行各类表达式运算。SpEL 可广泛应用于配置文件、注解、XML 配置等多种场景中。

一个典型的例子就是我们在项目中频繁使用的 @Value 注解。该注解支持常见的表达式操作,如算术运算、逻辑判断、条件表达式、集合处理等,并且能够与 Spring 的其他功能无缝集成。

@Value("#{mq.topic}")
public String mqTopic;

@Value("#{T(java.lang.Math).random() * 100.0}")
private double randomNumber;

核心概念解析

在深入应用之前,我们需要先掌握 SpEL 的一些关键概念:

// 解析器
ExpressionParser parser = new SpelExpressionParser();
// 使用解析器,解析 SpEL 表达式
// 该表达式中定义了字符串?'Hello '?常量,并且调用它的 .concat 方法,参数是一个引用变量
Expression expression = parser.parseExpression("'Hello '.concat(#param)"); // 在表达式中,使用?#param?访问上下文中的参数

// 在上下文中执行表达式,获取 String 类型的结果
String result = expression.getValue(context, String.class);

基于注解的扩展设计

通过上述对 SpEL 的理解,我们可以考虑在方案 1 的基础上对自定义注解进行增强。通过对注解内容的拓展,使得被标记的方法可以获取到完整的上下文信息。这种方式有效弥补了原方案中信息捕获有限、无法自定义的缺陷。那么具体应如何实现呢?

2.2 表达式的定义

第一步是对原有注解结构进行升级,将业务场景中所需的操作类型、数据字段等内容纳入定义范围:

@Repeatable(LogRecords.class)
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface LogRecord {
? ? String success();

? ? String fail() default?"";

? ? String operator() default?""; //业务操作场景人

? ? String?type(); // 业务场景 模块范围

? ? String subType() default?""; //业务子场景,主要是模块下的功能范围

? ? String bizNo(); //业务场景的业务编号,

? ? String extra() default?"";//一些操作的扩展操作

? ? String actionType(); //业务操作类型,比如编辑、新增、删除
}

接下来,基于该注解构建对应的 SpEL 解析器,用于解析注解中包含的表达式字段并加以利用:

import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.TypeResolutionContext;

import java.lang.reflect.Annotation;
import java.util.Map;

public class LogRecordParser {

? ? public static Map<String, Object> parseLogRecord(Annotation logRecordAnnotation) {
? ? ? ? Map<String, Object> result = new HashMap<>();

? ? ? ? ExpressionParser parser = new ExpressionParser(new SpelFunction("parseLogRecord", LogRecordParser.class,?"parseLogRecord"));

? ? ? ??for?(String attribute : logRecordAnnotation.getAttributeNames()) {
? ? ? ? ? ? Object value = logRecordAnnotation.getAttribute(attribute);

? ? ? ? ? ? Expression expression = parser.parseExpression(attribute);
? ? ? ? ? ? TypeResolutionContext typeResolutionContext = new TypeResolutionContext();
? ? ? ? ? ? typeResolutionContext.setMethod(new Method(null, null, null));
? ? ? ? ? ? Object parsedValue = expression.getValue(typeResolutionContext);

? ? ? ? ? ? result.put(attribute, parsedValue);
? ? ? ? }

? ? ? ??return?result;
? ? }
}

上述展示的是核心的数据解析流程,具体的后续业务逻辑可根据实际需求自行扩展。结合 AOP 与 SpEL 技术,我们便可以在需要监控的业务方法上直接使用自定义注解,实现自动化日志采集。

2.3 实际应用场景

下图展示了系统中某项具体业务操作的实际使用情况。可以看到,在注解中我们填充了大量与业务相关的操作数据。如果还需要记录操作前后数据的变化,还可以进一步扩展 SpEL 字段及相应的解析逻辑。有了这一机制,我们的能力边界得到了显著提升!当然,随之而来的也是注解长度不断增加的问题。

2.4 方案总结与分析

优点:

  • 优点1: 相较于方案1,降低了代码集成的复杂度,开发者只需定义注解即可完成接入;
  • 优点2: 大幅拓宽了可采集的业务数据范围,不仅覆盖更广,还支持根据业务需要无限扩展。

缺点:

  • 缺点1: 虽然减少了重复代码的侵入,但会引入大量注解定义,仍存在一定的框架侵入性;
  • 缺点2: 日志内容仍需由系统自身按上报规则进行封装,要求产品与研发在业务定义和编码层面达成一致。

从落地效果来看,本方案确实有效解决了业务操作日志的收集难题,能够清晰记录各类操作行为、动作类型以及数据变更前后的内容。然而整体实现仍显繁琐,是否还有更优雅的方式来克服当前方案的不足呢?

思考方向:能否从底层突破?

一直以来,我们的思路都集中在应用层面对操作场景进行拦截、处理和存储,导致复杂度高度集中于应用代码中。既然如此,是否可以尝试换一个角度——借助数据库本身的机制来完成这项任务?毕竟,MySQL 对数据变化的感知远比应用层更为敏锐。

3. 方案升级 3.0:Binlog + 时间窗口机制

3.1 Binlog 简介

Binlog 是 MySQL 中以二进制形式存在的日志文件,用于记录所有对数据库进行修改的 SQL 操作,例如表结构变更、数据增删改等语句都会被写入 Binlog。那么,它是否也能用来追踪业务层面的数据变动呢?

3.2 面临的问题与解决思路

问题一: 对于涉及多表级联保存或更新的场景,Binlog 很难做到良好兼容。因为 Binlog 本身不具备严格的顺序性,若上游操作未包裹在事务中,数据关联将更加困难。

问题二: 更新人信息难以识别。若系统未显式记录操作人,则无法准确归因,需推动上游系统统一改造。但从系统职责来看,操作人本就应随业务动作联动传递。

解决方案设计

针对问题一的应对策略:
由于 Binlog 数据天然无序,难以直接组织成完整事务流。对于非事务性操作,无法依赖事务 ID 进行聚合。此时可借鉴 Flink 的时间窗口思想:采用滚动时间窗口机制,将每个事件分配至固定大小的时间片段中,窗口之间不重叠。

举例来说,设定一个 1 分钟的滚动窗口,每过一分钟开启一个新的窗口,所有在此期间产生的数据都将归属到对应的时间段内,如下图所示:

通过这种时间窗口机制,我们可以先对数据进行粗粒度划分。再结合滑动窗口与补偿机制,对窗口内的数据进行关联处理。但仅靠时间维度仍不足以精准关联跨表变更。

因此,我们进一步从数据关联本身入手:在定义 Binlog 解析规则时,明确指定前后镜像数据以及表间的引用字段。这样,在窗口滑动过程中,即可依据外键或业务主键实现子表之间的关联匹配。最终,通过“关联字段 + 时间窗口”的双重机制,实现对复杂级联更新的有效补偿与还原。

以下是部分 binlog 数据变动结构中 RowChange 的定义:

@Data
public static class RowChange {
? ? private int tableId;
? ? private List<RowDatas> rowDatas;
? ? private String eventType;
? ? private boolean isDdl;
}

@Data
public static class RowDatas {
? ? private List<DataColumn> afterColumns;
? ? private List<DataColumn> beforeColumns;
}

@Data
public static class DataColumn {
? ? private int sqlType;
? ? private boolean isNull;
? ? private String mysqlType;
? ? private String name;
? ? private boolean isKey;
? ? private int index;
? ? private boolean updated;
? ? private String value;
}

3.3 方案架构

针对问题 2 的更新人信息记录,本质上属于各系统需自行处理的范畴。在业务进行数据操作时,通常都需要保留操作痕迹并记录更新人,较为通用的做法是通过底层 ORM 框架实现统一拦截与处理,具体实现可参考主流技术方案。

在明确了核心思路和关键问题后,我们梳理出整体的方案架构图如下:

3.4 方案总结

完成整体设计后,整个架构显得更加简洁高效,应用层所需承担的职责大幅减少,同时该结构具备良好的可复用性,能够推广至其他系统,支持日志收集的集中化管理。

然而,这一方案是否完全无缺?答案是否定的。虽然基于 binlog 能更敏锐地捕捉到底层数据的变化,但必须注意到,binlog 的数据来源不仅限于应用层,还包括数据库工单执行、定时批处理任务、手动刷数等场景。这些非应用层的数据变更往往影响范围较大,且难以精准识别。

此外,方案 3 相较于方案 2 弱化了对具体业务动作(如 actiontype、subtype 等)的描述能力,导致在细粒度行为感知方面存在不足。

欢迎各位补充更多可能存在的问题或优化建议。

4. 写在最后

回顾本次从业务背景分析到方案落地的全过程,我深刻体会到,在日常需求对接与技术实现中,总会遇到各种预料之外的问题。有时为了应对某一挑战而放弃某个技术路径,却可能引发一系列新的连锁问题。

系统架构的演进本就是一个持续迭代的过程。面对变化,我们应保持开放心态,避免过度追求“极致技术”,转而注重实际适配性和稳定性。完美的解决方案或许罕见,真正重要的是不断调整与优化,使技术架构始终贴合业务发展需要。

二维码

扫码加我 拉你入群

请注明:姓名-公司-职位

以便审核进群资格,未注明则拒绝

关键词:Expression resolution attribute EXECUTION Retention

您需要登录后才可以回帖 登录 | 我要注册

本版微信群
jg-xs1
拉您进交流群
GMT+8, 2025-12-5 16:56