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 契约的最小实现)。
|
*
|
* <p>职责:把同一事务内提交的审计事件缓冲到 afterCommit 才异步发出(回滚不发);operationId 的
|
* 生成与 MDC 写入统一归本类单源管理(终验#6:collector 先写 MDC 会破坏 previousMdc 语义)。
|
*
|
* <p><b>operationId 语义(M2 spec §2)</b>:归属最外层的「操作作用域」({@link #beginOperation}
|
* 返回的 OperationScope),作用域内的全部事务——含 REQUIRES_NEW 子事务——共享同一个 id;
|
* 没有作用域时退化为每个事务自己一个 id。缓冲仍按事务划分(回滚不发的语义不变),
|
* 只是「缓冲身份」与「操作身份」自此解耦。
|
*
|
* <p>全类只有一条 id 规则:<b>决定 id 时先读 MDC,有值则沿用,无值才生成</b>——
|
* {@link #submit} 无事务分支、{@link #beginOperation} 无事务分支、{@link #createBuffer}
|
* 三处一致,无例外。跨事务(REQUIRES_NEW)的沿用在挂起点由两条途径承接:
|
* {@code previousMdc} 非 null 时靠它本身,为 null 时靠 {@code TxAuditBuffer.scopeOwned}——
|
* 两者的等价性依赖下面 {@link #createBuffer} 处标明的不变量。
|
*
|
* <p>本类的时序注释对应事务级缓冲的并发时序契约,均经过验证——重构时不要随手删改,
|
* 每一处都对应一个真实存在过的竞态场景。
|
* 回归守卫:{@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<AuditEventDTO> events = new ArrayList<>();
|
/**
|
* 作用域是否**经由 {@link #beginOperation} 的事务分支**挂在本 buffer 上。
|
*
|
* <p><b>注意它不等于「本 buffer 的 id 归属某个作用域」</b>:形态甲(作用域在事务外开启、
|
* buffer 由 {@link #submit} 在事务内创建)下 id 同样归属该作用域,但本标记为 false——
|
* 那种形态的沿用由 {@code previousMdc} 承接,不走本标记。本标记只负责补上
|
* {@code previousMdc == null} 那一格。
|
*
|
* <p>为什么需要它:作用域由事务分支自己创建时 previousMdc 恒为 null,此时若挂起点按
|
* previousMdc 恢复会把 MDC 清空、内层子事务另生成 id → 同一次操作分叉
|
* (spec §1.3;这正是今日 lims 四处的事务形态)。
|
*
|
* <p>非 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 读取。
|
*
|
* <p>为什么不能靠 {@link #submit}:submit 发生在 proceed() 之后,而内层旧切面写日志更早,
|
* 那时 MDC 还没值 → A/B 配不上对。
|
*
|
* <p>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;
|
}
|
}
|