package jnpf.audit.sdk; import jnpf.audit.AuditApiConsts; import jnpf.audit.model.AuditEventDTO; import jnpf.util.RandomUtil; import org.slf4j.MDC; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * 事务级审计缓冲 holder(spec §3.3/§5.5 契约的最小实现)。 * *

职责:把同一事务内提交的审计事件缓冲到 afterCommit 才异步发出(回滚不发);operationId 的 * 生成与 MDC 写入统一归本类单源管理(终验#6:collector 先写 MDC 会破坏 previousMdc 语义)。 * *

operationId 语义(M2 spec §2):归属最外层的「操作作用域」({@link #beginOperation} * 返回的 OperationScope),作用域内的全部事务——含 REQUIRES_NEW 子事务——共享同一个 id; * 没有作用域时退化为每个事务自己一个 id。缓冲仍按事务划分(回滚不发的语义不变), * 只是「缓冲身份」与「操作身份」自此解耦。 * *

全类只有一条 id 规则:决定 id 时先读 MDC,有值则沿用,无值才生成—— * {@link #submit} 无事务分支、{@link #beginOperation} 无事务分支、{@link #createBuffer} * 三处一致,无例外。跨事务(REQUIRES_NEW)的沿用在挂起点由两条途径承接: * {@code previousMdc} 非 null 时靠它本身,为 null 时靠 {@code TxAuditBuffer.scopeOwned}—— * 两者的等价性依赖下面 {@link #createBuffer} 处标明的不变量。 * *

本类的时序注释对应事务级缓冲的并发时序契约,均经过验证——重构时不要随手删改, * 每一处都对应一个真实存在过的竞态场景。 * 回归守卫:{@code bash tasks/audit-opid-checks.sh}(十一项并发时序矩阵)。 * 平台雪花工具经现场核实为 {@link RandomUtil#uuId()}(= {@code String.valueOf(YitIdHelper.nextId())})。 */ public class AuditTxHolder { private static final Object RESOURCE_KEY = new Object(); /** 事务级缓冲:{operationId + 不可变事件列表};MDC 保存/恢复防线程池串号(评审#6) */ static class TxAuditBuffer { final String operationId; final String previousMdc; // 进入前的 MDC 旧值,afterCompletion 恢复 final List events = new ArrayList<>(); /** * 作用域是否**经由 {@link #beginOperation} 的事务分支**挂在本 buffer 上。 * *

注意它不等于「本 buffer 的 id 归属某个作用域」:形态甲(作用域在事务外开启、 * buffer 由 {@link #submit} 在事务内创建)下 id 同样归属该作用域,但本标记为 false—— * 那种形态的沿用由 {@code previousMdc} 承接,不走本标记。本标记只负责补上 * {@code previousMdc == null} 那一格。 * *

为什么需要它:作用域由事务分支自己创建时 previousMdc 恒为 null,此时若挂起点按 * previousMdc 恢复会把 MDC 清空、内层子事务另生成 id → 同一次操作分叉 * (spec §1.3;这正是今日 lims 四处的事务形态)。 * *

非 final:作用域可能**晚于** buffer 出现(层 1 先建 buffer,随后才进入标注方法, * spec §4 形态丁),故 beginOperation 的创建与复用两条路径都要置位。 * buffer 绑在 TSM 事务资源上、线程内独占,无并发写风险。 */ boolean scopeOwned; TxAuditBuffer(String operationId, String previousMdc) { this.operationId = operationId; this.previousMdc = previousMdc; } } /** operationId 的生成与 MDC 写入统一归本类管理(终验#6:collector 先写 MDC 会破坏 previousMdc 语义) */ public static void submit(AuditEventDTO event, AuditEventPublisher publisher) { if (!TransactionSynchronizationManager.isSynchronizationActive()) { String outerMdc = MDC.get(AuditApiConsts.MDC_OPERATION_ID); String opId = event.getOperationId() != null ? event.getOperationId() : (outerMdc != null ? outerMdc : RandomUtil.uuId()); // 平台雪花工具,类名对照现场 publisher.publishAsync(Collections.singletonList(event.toBuilder().operationId(opId).build())); if (outerMdc == null) { MDC.remove(AuditApiConsts.MDC_OPERATION_ID); // 自己没写过就保持干净 } return; } TxAuditBuffer buffer = (TxAuditBuffer) TransactionSynchronizationManager.getResource(RESOURCE_KEY); if (buffer == null) { buffer = createBuffer(publisher); } buffer.events.add(event.toBuilder().operationId(buffer.operationId).build()); } /** 操作作用域:close() 只在**无事务**分支需要恢复 MDC;有事务时由 afterCompletion 统一恢复。 */ public interface OperationScope extends AutoCloseable { String operationId(); @Override void close(); // 覆写掉 AutoCloseable 的 throws Exception,调用方无需 catch } /** * getOrCreate 语义(M2 契约兑现,M1 已在 currentOperationId 的 javadoc 预告): * 进入业务方法时即确保「本事务有 operationId 且已写入 MDC」,供内层旧 BizLogAspect 读取。 * *

为什么不能靠 {@link #submit}:submit 发生在 proceed() 之后,而内层旧切面写日志更早, * 那时 MDC 还没值 → A/B 配不上对。 * *

MDC 生命周期仍锁在本类一处(单源契约):有事务走 afterCompletion 恢复, * 无事务由返回的 scope.close() 恢复——两条路径都保存进入前旧值,防线程池复用串号。 */ public static OperationScope beginOperation(AuditEventPublisher publisher) { return beginOperation(publisher, null); } /** * 使用可信业务凭证派生的显式关联键开启作用域;已有外层作用域时仍以外层为准。 */ public static OperationScope beginOperation(AuditEventPublisher publisher, String preferredOperationId) { if (!TransactionSynchronizationManager.isSynchronizationActive()) { final String previousMdc = MDC.get(AuditApiConsts.MDC_OPERATION_ID); final String opId = previousMdc != null ? previousMdc : (preferredOperationId != null ? preferredOperationId : RandomUtil.uuId()); MDC.put(AuditApiConsts.MDC_OPERATION_ID, opId); return new OperationScope() { @Override public String operationId() { return opId; } @Override public void close() { restoreMdc(previousMdc); } }; } TxAuditBuffer buffer = (TxAuditBuffer) TransactionSynchronizationManager.getResource(RESOURCE_KEY); if (buffer == null) { buffer = createBuffer(publisher, preferredOperationId); } // 作用域标记:创建与复用两条路径都要置(spec §4 形态丁——层 1 可能先建了 buffer, // 作用域晚于它出现;只在创建路径置位会让该形态的 REQUIRES_NEW 内层重新分叉) buffer.scopeOwned = true; final String opId = buffer.operationId; return new OperationScope() { @Override public String operationId() { return opId; } @Override public void close() { /* 有事务:MDC 由 afterCompletion 恢复,此处刻意 no-op */ } }; } private static TxAuditBuffer createBuffer(final AuditEventPublisher publisher) { return createBuffer(publisher, null); } private static TxAuditBuffer createBuffer(final AuditEventPublisher publisher, String preferredOperationId) { // 关键时序:先保存外层旧值,再决定本事务 operationId 并写 MDC(终验#6) String previousMdc = MDC.get(AuditApiConsts.MDC_OPERATION_ID); // M2 语义(spec §2):operationId 归属最外层操作作用域——MDC 已有值即代表外层已有 // 作用域,一律沿用;无值才生成。与 submit(:42)/beginOperation(:77) 两条无事务分支的 // 写法在此拉齐,全类只有一条规则:"先看 MDC,有则沿用"。 // // ⚠️ 本行确立一条**全类依赖的不变量**:凡本方法产出的 buffer, // previousMdc != null ⟹ operationId == previousMdc // suspend 的三目(scopeOwned ? operationId : previousMdc)正是靠它让两支殊途同归—— // previousMdc 非 null 时两支取值必然相等,故三目只在 previousMdc == null 时才真正分岔 // (即行为变更半径 = spec §4 形态丙/丁/戊)。**若将来给 id 决定逻辑加旁路(例如允许调用方 // 显式传入 id)而打破该不变量,suspend 的第二支会静默失去沿用能力——改这里先去看 suspend。** final TxAuditBuffer created = new TxAuditBuffer( previousMdc != null ? previousMdc : (preferredOperationId != null ? preferredOperationId : RandomUtil.uuId()), previousMdc); MDC.put(AuditApiConsts.MDC_OPERATION_ID, created.operationId); TransactionSynchronizationManager.bindResource(RESOURCE_KEY, created); TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { private TxAuditBuffer suspended; @Override public void suspend() { // REQUIRES_NEW 内层开始:TSM 与 MDC 同步挂起 suspended = (TxAuditBuffer) TransactionSynchronizationManager.unbindResource(RESOURCE_KEY); // spec §4,按 (scopeOwned, previousMdc) 四组合读: // (true , null) 形态丙/丁/戊 → 恢复 operationId,内层沿用 ←【本次唯一被改变的组合】 // (true , X ) → 恢复 operationId,但由 createBuffer 的不变量 X == operationId, // 与走第二支等价,无行为变化 // (false, X ) 形态甲 → 恢复 previousMdc = X,内层照样沿用(作用域存在但不置标记) // (false, null) 形态乙 → 恢复 null(MDC 清空),内层另生成 id // 后两格逐字保留 M1「终验#6:只解绑资源不动 MDC 的话,内层会看到外层 opId」的原行为 // (S5 断言守之);第一格是本次**有意覆盖**该结论——终验#6 是在「一个事务一个 id」 // 语义下作出的,M2 引入 beginOperation 后语义改为「一次用户操作一个 id」。 // 改动前先读 spec §2 与 createBuffer 处标明的不变量。 restoreMdc(suspended.scopeOwned ? suspended.operationId : suspended.previousMdc); } @Override public void resume() { TransactionSynchronizationManager.bindResource(RESOURCE_KEY, suspended); MDC.put(AuditApiConsts.MDC_OPERATION_ID, suspended.operationId); } @Override public void afterCommit() { if (created.events.isEmpty()) { return; // beginOperation 建了缓冲但本次没有事件(如注解方法内无审计动作),不空发 } publisher.publishAsync(new ArrayList<>(created.events)); // 回滚不发 } @Override public void afterCompletion(int status) { if (TransactionSynchronizationManager.hasResource(RESOURCE_KEY)) { TransactionSynchronizationManager.unbindResource(RESOURCE_KEY); // 防泄漏 } restoreMdc(created.previousMdc); // 恢复外层值 / 清理防线程池串号 } }); return created; } private static void restoreMdc(String previous) { if (previous != null) { MDC.put(AuditApiConsts.MDC_OPERATION_ID, previous); } else { MDC.remove(AuditApiConsts.MDC_OPERATION_ID); } } /** * 读接口(终验四轮①,M1 只声明不消费):返回当前事务已有的 operationId; * 无活跃缓冲返回 null,**本方法不触发创建**(终验五轮①:javadoc 不承诺超出代码能力的语义)。 * M2 设计契约:@AuditLog aspect 不得自行生成 operationId——若需要 getOrCreate 语义, * M2 时在本类新增对应方法,由 holder 单源生成后写 MDC 供旧 BizLogAspect 读取; * aspect 与 holder 双源生成会破坏 A/B 精确配对。 */ public static String currentOperationId() { TxAuditBuffer buffer = (TxAuditBuffer) TransactionSynchronizationManager.getResource(RESOURCE_KEY); return buffer != null ? buffer.operationId : null; } }