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 注意它不等于「本 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;
}
}