diff --git a/module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java b/module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java
index 5207b1af4..cc1242780 100644
--- a/module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java
+++ b/module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java
@@ -1,7 +1,10 @@
package io.izzel.incision.bridge;
import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
/**
* Incision 字节码桥 — 所有被 incision 织入的 INVOKESTATIC 调用都指向这里。
@@ -17,8 +20,8 @@
*
* 解析顺序:
*
- * - 按被织入类的 defining ClassLoader 精确查找本地 TheatreDispatcher;
- * - 目标是服务端类且当前只有一个本地 lease 时,允许唯一回退;
+ * - 按目标签名查找声明该切术的 TheatreDispatcher;
+ * - 旧调用方未登记目标时,才按 defining ClassLoader 或唯一 lease 回退;
* - 本地没有可用路由时再交给系统 ClassLoader 上的 Gate。
*
*
@@ -39,6 +42,19 @@ private IncisionBridge() {}
/** ClassLoader → 本地 TheatreDispatcher.dispatch Method 缓存(单插件 fallback 路径) */
private static final ConcurrentHashMap localCache = new ConcurrentHashMap();
+ /**
+ * 运行时目标签名 → 声明该目标的 dispatcher。
+ *
+ * 被织入类可能属于 Leaf 的服务端 URLClassLoader,也可能属于 AuraSkills 等第三方插件;
+ * 它的 defining loader 与切术声明方没有必然关系,因此 loader 绝不能作为正常路由依据。
+ */
+ private static final ConcurrentHashMap> targetRoutes =
+ new ConcurrentHashMap>();
+
+ /** 多插件声明同一目标的诊断去重;真正的跨插件优先级聚合必须由 Gate 完成。 */
+ private static final ConcurrentHashMap routeConflictWarnings =
+ new ConcurrentHashMap();
+
/**
* 供 weaver 注入的 INVOKESTATIC 目标。
*
@@ -54,16 +70,26 @@ private IncisionBridge() {}
*/
public static Object dispatch(Class> ownerClass, String targetSignature, Object self, Object[] args) {
ClassLoader definingLoader = ownerClass == null ? null : ownerClass.getClassLoader();
- Method local = resolveLocalDispatch(definingLoader);
- if (local != null) {
- try {
- if (local.getParameterCount() == 4) {
- return local.invoke(null, targetSignature, self, args, null);
+ List locals = resolveLocalDispatches(definingLoader, targetSignature);
+ if (!locals.isEmpty()) {
+ Object result = null;
+ boolean invoked = false;
+ for (Method local : locals) {
+ try {
+ Object localResult;
+ if (local.getParameterCount() == 4) {
+ localResult = local.invoke(null, targetSignature, self, args, null);
+ } else {
+ localResult = local.invoke(null, targetSignature, self, args);
+ }
+ // 未持有该 target 的 dispatcher 以 null 表示未命中,不能覆盖前一个插件的有效结果。
+ if (localResult != null) result = localResult;
+ invoked = true;
+ } catch (Throwable t) {
+ System.err.println("[Incision][Bridge] local dispatch failed: " + t);
}
- return local.invoke(null, targetSignature, self, args);
- } catch (Throwable t) {
- System.err.println("[Incision][Bridge] local dispatch failed: " + t);
}
+ if (invoked) return result;
}
Method m = systemDispatch;
Object host = systemHost;
@@ -77,15 +103,21 @@ public static Object dispatch(Class> ownerClass, String targetSignature, Objec
// 精确 loader 路由失败属于生命周期错误,不能静默伪装成 advice 未命中。
System.err.println("[Incision][Bridge] dispatch unavailable: owner=" +
(ownerClass == null ? "null" : ownerClass.getName()) + " loader=" + definingLoader +
- " localLeases=" + localCache.size() + " target=" + targetSignature);
+ " localLeases=" + localCache.size() + " targetRoutes=" + targetRoutes.size() +
+ " target=" + targetSignature);
return null;
}
public static Object dispatchBypass(Class> ownerClass, String targetSignature, Object self, Object[] args) {
- Method local = resolveLocalSibling(ownerClass == null ? null : ownerClass.getClassLoader(), "dispatchBypass");
- if (local != null) {
+ List dispatches = resolveLocalDispatches(
+ ownerClass == null ? null : ownerClass.getClassLoader(), targetSignature
+ );
+ for (Method dispatch : dispatches) {
+ Method local = resolveLocalSibling(dispatch, "dispatchBypass");
+ if (local == null) continue;
try {
- return local.invoke(null, targetSignature, self, args);
+ Object result = local.invoke(null, targetSignature, self, args);
+ if (!isBypassMiss(result)) return result;
} catch (Throwable t) {
System.err.println("[Incision][Bridge] local bypass dispatch failed: " + t);
}
@@ -116,6 +148,11 @@ public static boolean hasSystemHost() {
return systemHost != null && systemDispatch != null;
}
+ /** JVM 级 lease 数量;Gate holder 只能在该值归零后释放共享 delegate。 */
+ public static int localLeaseCount() {
+ return localCache.size();
+ }
+
// -----------------------------------------------------------------
private static Object findSystemHost() {
@@ -141,14 +178,63 @@ private static Method resolveDispatch(Object host) {
}
private static Method resolveLocalDispatch(ClassLoader definingLoader) {
- Method cached = localCache.get(definingLoader);
+ Method cached = definingLoader == null ? null : localCache.get(definingLoader);
if (cached != null) return cached;
- // 服务端类通常由 bootstrap/system loader 定义。只有一个插件持有 lease 时路由没有歧义;
- // 多插件场景必须交给 Gate,禁止退化为“最后注册 dispatcher”。
+ // 兼容未登记 target 的旧调用方:只有一个插件持有 lease 时路由没有歧义。
+ // 多 lease 的广播由 resolveLocalDispatches 生成快照,禁止退化为“最后注册 dispatcher”。
if (localCache.size() == 1) return localCache.values().iterator().next();
return null;
}
+ private static List resolveLocalDispatches(ClassLoader definingLoader, String targetSignature) {
+ CopyOnWriteArrayList routed = targetRoutes.get(baseSignature(targetSignature));
+ if (routed != null && !routed.isEmpty()) return new ArrayList(routed);
+ Method legacy = resolveLocalDispatch(definingLoader);
+ if (legacy != null) return java.util.Collections.singletonList(legacy);
+ // 旧版调用方不会登记 target。owner loader 又可能属于 Leaf 或第三方插件,
+ // 此时广播给快照中的 dispatcher,由各自的 chain 表自行判定是否命中,禁止再因多 lease 直接断链。
+ return new ArrayList(localCache.values());
+ }
+
+ /**
+ * 登记目标的真实声明方。相位与 Site advice id 属于调用后缀,不参与路由键。
+ */
+ public static void registerLocalTarget(Class> dispatcherClass, String targetSignature) {
+ if (dispatcherClass == null || targetSignature == null) return;
+ Method dispatch = pickDispatchMethod(dispatcherClass);
+ if (dispatch == null) return;
+ String base = baseSignature(targetSignature);
+ CopyOnWriteArrayList routes = targetRoutes.computeIfAbsent(
+ base, ignored -> new CopyOnWriteArrayList()
+ );
+ for (Method route : routes) {
+ if (route.getDeclaringClass() == dispatcherClass) return;
+ }
+ routes.add(dispatch);
+ if (routes.size() > 1 && routeConflictWarnings.putIfAbsent(base, Boolean.TRUE) == null) {
+ System.err.println("[Incision][Bridge] multiple dispatchers registered for target=" + base +
+ " routes=" + routes.size() + " (cross-plugin priority requires Gate aggregation)");
+ }
+ }
+
+ /** 仅移除当前插件对指定目标的路由,不影响其他同时安装 Incision 的插件。 */
+ public static void unregisterLocalTarget(ClassLoader classLoader, String targetSignature) {
+ if (classLoader == null || targetSignature == null) return;
+ String base = baseSignature(targetSignature);
+ CopyOnWriteArrayList routes = targetRoutes.get(base);
+ if (routes == null) return;
+ routes.removeIf(method -> method.getDeclaringClass().getClassLoader() == classLoader);
+ if (routes.isEmpty()) targetRoutes.remove(base, routes);
+ if (routes.size() <= 1) routeConflictWarnings.remove(base);
+ }
+
+ private static String baseSignature(String targetSignature) {
+ int hash = targetSignature.indexOf('#');
+ String withoutAdvice = hash < 0 ? targetSignature : targetSignature.substring(0, hash);
+ int phase = withoutAdvice.lastIndexOf('@');
+ return phase < 0 ? withoutAdvice : withoutAdvice.substring(0, phase);
+ }
+
/** 由 IncisionBootstrap 在 CONST 阶段调用,显式注册经过重定向后的 dispatcher 类 */
public static void registerLocalDispatcher(Class> dispatcherClass) {
if (dispatcherClass == null) return;
@@ -177,8 +263,7 @@ private static Method pickDispatchMethod(Class> cls) {
return anyShape4 != null ? anyShape4 : anyShape3;
}
- private static Method resolveLocalSibling(ClassLoader cl, String methodName) {
- Method local = resolveLocalDispatch(cl);
+ private static Method resolveLocalSibling(Method local, String methodName) {
if (local == null) return null;
try {
return local.getDeclaringClass().getMethod(methodName, String.class, Object.class, Object[].class);
@@ -191,5 +276,10 @@ private static Method resolveLocalSibling(ClassLoader cl, String methodName) {
public static void unregisterLocalDispatcher(ClassLoader cl) {
if (cl == null) return;
localCache.remove(cl);
+ for (String target : new ArrayList(targetRoutes.keySet())) {
+ unregisterLocalTarget(cl, target);
+ }
+ // 最后一个插件退出后必须断开 Gate 对首个插件 ClassLoader 的强引用;更早解绑会破坏其他 lease。
+ if (localCache.isEmpty()) unbindSystemHost();
}
}
diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt b/module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt
index 17edf8ec2..5b4fc9226 100644
--- a/module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt
+++ b/module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt
@@ -1,6 +1,5 @@
package taboolib.module.incision
-import io.izzel.incision.bridge.IncisionBridge
import taboolib.common.Inject
import taboolib.common.LifeCycle
import taboolib.common.env.RuntimeDependencies
@@ -19,6 +18,7 @@ import taboolib.module.incision.loader.PipelineBackend
import taboolib.module.incision.reflex.IncisionReflex
import taboolib.module.incision.remap.RemapRouter
import taboolib.module.incision.remap.TabooLibNmsResolver
+import taboolib.module.incision.runtime.CanonicalBridge
import taboolib.module.incision.runtime.SurgeryRegistry
import taboolib.module.incision.runtime.TheatreDispatcher
@@ -50,13 +50,7 @@ object IncisionBootstrap {
TabooLibNmsResolver.installIfAvailable()
// 2. 安装反射穿透适配器,让 TabooLib reflex 在 invoke 时自动 withoutIncision
IncisionReflex.installReflexAdapter()
- // 3. 把经 gradle 重定位后真实的 TheatreDispatcher 类名注册进桥(插件 CL 版本)
- try {
- IncisionBridge.registerLocalDispatcher(TheatreDispatcher::class.java)
- } catch (t: Throwable) {
- Forensics.warn("Incision bridge registerLocalDispatcher failed: ${t.message}")
- }
- // 4. 注入 IncisionBridge 到 bootstrap ClassLoader,使跨 CL 目标(NMS、Bukkit API)能解析桥类
+ // 3. 注入 IncisionBridge 到 bootstrap ClassLoader,使跨 CL 目标(NMS、Bukkit API)能解析桥类
// 必须在任何 installWeaver/retransform 之前完成
injectBridgeIntoSystemClassLoader()
Forensics.info("Incision CONST: api=$API_VERSION resolver=${RemapRouter.name()}")
@@ -70,7 +64,7 @@ object IncisionBootstrap {
// 1. 接入 / 创建 IncisionGate
try {
val gate = IncisionGateLocator.locateOrCreate(API_VERSION)
- IncisionBridge.bindSystemHost(gate)
+ CanonicalBridge.bindSystemHost(gate)
Forensics.info("Incision Gate online: api=${gate.apiVersion()}")
} catch (t: Throwable) {
Forensics.warn("Incision Gate 接入失败(将使用本地 dispatcher 兜底):${t.javaClass.name}: ${t.message}")
@@ -104,10 +98,7 @@ object IncisionBootstrap {
PipelineBackend.clear()
InstrumentationBackend.clearTransformers()
// 解绑本插件在桥上的本地 dispatcher 缓存
- runCatching {
- IncisionBridge.unregisterLocalDispatcher(IncisionBootstrap::class.java.classLoader)
- }
- IncisionBridge.unbindSystemHost()
+ CanonicalBridge.unregisterDispatcher(IncisionBootstrap::class.java.classLoader)
GateBootstrapper.release(IncisionBootstrap::class.java.classLoader)
JvmtiBackend.dispose()
}
@@ -165,8 +156,8 @@ object IncisionBootstrap {
} catch (_: Throwable) {
null
}
- if (existing != null && existing.classLoader == null) {
- Forensics.info("IncisionBridge 已存在于 bootstrap ClassLoader")
+ if (existing != null && (existing.classLoader == null || existing.classLoader === sysCL)) {
+ Forensics.info("IncisionBridge 已存在于 ${existing.classLoader ?: "bootstrap"} ClassLoader")
registerDispatcherOn(existing)
return
}
@@ -208,13 +199,17 @@ object IncisionBootstrap {
}
}
// 路径 3: fallback — 仅在插件 CL 中,跨 CL 目标无法使用
+ runCatching {
+ Class.forName(bridgeClassName, true, IncisionBootstrap::class.java.classLoader)
+ }.getOrNull()?.let(::registerDispatcherOn)
Forensics.warn("IncisionBridge 未能注入 bootstrap/system CL — 跨 ClassLoader 目标(NMS、Bukkit API)将不可用")
}
private fun registerDispatcherOn(bridgeClass: Class<*>) {
try {
- val regMethod = bridgeClass.getMethod("registerLocalDispatcher", Class::class.java)
- regMethod.invoke(null, TheatreDispatcher::class.java)
+ // 后续 target 注册、host 绑定与卸载必须复用同一个类句柄,不能再直接链接插件内同名 Bridge。
+ CanonicalBridge.bind(bridgeClass)
+ CanonicalBridge.registerDispatcher(TheatreDispatcher::class.java)
Forensics.info("IncisionBridge dispatcher 已注册 (CL=${bridgeClass.classLoader ?: "bootstrap"})")
} catch (t: Throwable) {
Forensics.warn("IncisionBridge registerLocalDispatcher 失败: ${t.message}")
diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/dsl/Scalpel.kt b/module/incision/src/main/kotlin/taboolib/module/incision/dsl/Scalpel.kt
index 9802869ae..be0fbd78f 100644
--- a/module/incision/src/main/kotlin/taboolib/module/incision/dsl/Scalpel.kt
+++ b/module/incision/src/main/kotlin/taboolib/module/incision/dsl/Scalpel.kt
@@ -211,6 +211,7 @@ object Scalpel {
continue
}
activeTokens[resolvedOwner] = token
+ syncRuntimeAliases(resolvedOwner, ownerEntries.values.toList())
Forensics.debug("installWeaver status=${installation.status} owner=$owner resolved=$resolvedOwner advices=${group.size} total=${ownerEntries.size} backend=${backend.name}")
}
}
@@ -256,14 +257,14 @@ object Scalpel {
val token = installation.token ?: return false
if (installation.status !in setOf(Backend.InstallStatus.INSTALLED, Backend.InstallStatus.PENDING_LOAD)) return false
activeTokens[owner] = token
+ syncRuntimeAliases(owner, entries)
return true
}
/** 把逻辑声明统一翻译为运行时坐标;宿主与 Site 必须经过同一条映射链。 */
private fun buildRuntimeTargets(resolvedOwner: String, entries: List): List {
return entries.groupBy { it.target }.map { (target, targetEntries) ->
- val (resolvedName, resolvedDescriptor) = RemapRouter.resolveMethod(target.owner, target.name, target.descriptor)
- val runtimeTarget = target.copy(owner = resolvedOwner, name = resolvedName, descriptor = resolvedDescriptor)
+ val runtimeTarget = resolveRuntimeTarget(resolvedOwner, target)
ScalpelWeaver.AdviceTargetSpec(
target = runtimeTarget,
kinds = targetEntries.map { it.kind }.toSet(),
@@ -294,6 +295,20 @@ object Scalpel {
}
}
+ /** 后端确认接受计划后才发布别名,失败安装不得留下一个永远不会被字节码调用的幽灵 chain。 */
+ private fun syncRuntimeAliases(resolvedOwner: String, entries: List) {
+ entries.groupBy { it.target }.forEach { (logicalTarget, targetEntries) ->
+ val runtimeTarget = resolveRuntimeTarget(resolvedOwner, logicalTarget)
+ TheatreDispatcher.registerRuntimeAlias(runtimeTarget, targetEntries)
+ }
+ }
+
+ /** 宿主坐标只允许经过这一条解析链,保证 weave key、Bridge route 与 dispatcher alias 完全一致。 */
+ private fun resolveRuntimeTarget(resolvedOwner: String, target: taboolib.module.incision.api.MethodCoordinate): taboolib.module.incision.api.MethodCoordinate {
+ val (resolvedName, resolvedDescriptor) = RemapRouter.resolveMethod(target.owner, target.name, target.descriptor)
+ return target.copy(owner = resolvedOwner, name = resolvedName, descriptor = resolvedDescriptor)
+ }
+
/**
* 插件卸载边界:移除全部 transformer 与累计计划,避免下一 ClassLoader 再次叠加旧织入。
*/
diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/gate/GateBootstrapper.kt b/module/incision/src/main/kotlin/taboolib/module/incision/gate/GateBootstrapper.kt
index 02548298e..cc3f08110 100644
--- a/module/incision/src/main/kotlin/taboolib/module/incision/gate/GateBootstrapper.kt
+++ b/module/incision/src/main/kotlin/taboolib/module/incision/gate/GateBootstrapper.kt
@@ -3,6 +3,7 @@ package taboolib.module.incision.gate
import taboolib.module.incision.diagnostic.Forensics
import taboolib.module.incision.diagnostic.Trauma
import taboolib.module.incision.loader.InstrumentationBackend
+import taboolib.module.incision.runtime.CanonicalBridge
import taboolib.platform.bukkit.Exchanges
import java.io.File
import java.io.FileOutputStream
@@ -41,10 +42,14 @@ object GateBootstrapper {
val gate = bound ?: return
runCatching { gate.healByClassLoader(classLoader) }
bound = null
- val holder = runCatching {
- Class.forName("taboolib.incision.gate.IncisionGate\$V${gate.apiVersion()}", false, ClassLoader.getSystemClassLoader())
- }.getOrNull()
- runCatching { holder?.getMethod("setDelegate", Object::class.java)?.invoke(null, null) }
+ // holder 是 JVM 共享对象。任一插件单独 disable 都不能清掉其他 lease 正在使用的 delegate;
+ // canonical Bridge 在最后一个 dispatcher 注销时同步断开 systemHost,二者必须一起归零。
+ if (CanonicalBridge.localLeaseCount() == 0) {
+ val holder = runCatching {
+ Class.forName("taboolib.incision.gate.IncisionGate\$V${gate.apiVersion()}", false, ClassLoader.getSystemClassLoader())
+ }.getOrNull()
+ runCatching { holder?.getMethod("setDelegate", Object::class.java)?.invoke(null, null) }
+ }
}
fun bootstrap(apiVersion: Int): IncisionGateApi {
diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/runtime/AdviceChain.kt b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/AdviceChain.kt
index 12599c64b..ffc422f96 100644
--- a/module/incision/src/main/kotlin/taboolib/module/incision/runtime/AdviceChain.kt
+++ b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/AdviceChain.kt
@@ -47,6 +47,8 @@ class AdviceChain(val target: MethodCoordinate) {
private val entries = java.util.concurrent.CopyOnWriteArrayList()
fun add(entry: AdviceEntry) {
+ // 聚合计划重装会再次同步逻辑/运行时别名;同一 id 必须替换而不是重复执行。
+ entries.removeIf { it.id == entry.id }
entries.add(entry)
entries.sortByDescending { it.priority }
}
diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/runtime/CanonicalBridge.kt b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/CanonicalBridge.kt
new file mode 100644
index 000000000..91f7cbc8e
--- /dev/null
+++ b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/CanonicalBridge.kt
@@ -0,0 +1,80 @@
+package taboolib.module.incision.runtime
+
+import taboolib.module.incision.diagnostic.Forensics
+
+/**
+ * 当前插件对 JVM 唯一 IncisionBridge 的反射句柄。
+ *
+ * 同名 Bridge 可能同时存在于插件与 bootstrap ClassLoader;业务代码若直接调用静态方法,
+ * JVM 会在链接时永久绑定到其中一个版本。这里强制所有 lease 与目标路由操作都落到启动期
+ * 选定的 canonical 类,避免出现日志显示两个 lease、实际却写入另一份静态表的分裂状态。
+ */
+internal object CanonicalBridge {
+
+ /** 仅在 Bridge 注入彻底失败时使用;该场景本来就不支持跨 ClassLoader 织入。 */
+ private val fallbackBypassMiss = Any()
+
+ @Volatile
+ private var bridgeClass: Class<*>? = null
+
+ /** 只能在 Bridge 注入完成后绑定;重复绑定同一个类是幂等的。 */
+ fun bind(type: Class<*>) {
+ bridgeClass = type
+ }
+
+ fun registerDispatcher(dispatcherClass: Class<*>) {
+ invoke("registerLocalDispatcher", arrayOf>(Class::class.java), dispatcherClass)
+ }
+
+ fun unregisterDispatcher(classLoader: ClassLoader) {
+ invoke("unregisterLocalDispatcher", arrayOf>(ClassLoader::class.java), classLoader)
+ }
+
+ fun registerTarget(dispatcherClass: Class<*>, targetSignature: String) {
+ invoke(
+ "registerLocalTarget",
+ arrayOf>(Class::class.java, String::class.java),
+ dispatcherClass,
+ targetSignature,
+ )
+ }
+
+ fun unregisterTarget(classLoader: ClassLoader, targetSignature: String) {
+ invoke(
+ "unregisterLocalTarget",
+ arrayOf>(ClassLoader::class.java, String::class.java),
+ classLoader,
+ targetSignature,
+ )
+ }
+
+ fun bindSystemHost(host: Any) {
+ invoke("bindSystemHost", arrayOf>(Any::class.java), host)
+ }
+
+ /** 返回 JVM canonical Bridge 当前持有的插件 lease 数,而不是当前重定位副本的本地状态。 */
+ fun localLeaseCount(): Int =
+ (invokeResult("localLeaseCount", emptyArray>()) as? Number)?.toInt() ?: 0
+
+ /** Bypass sentinel 必须来自 canonical Bridge,引用身份才能跨插件 ClassLoader 保持一致。 */
+ fun bypassMiss(): Any =
+ invokeResult("bypassMiss", emptyArray>()) ?: fallbackBypassMiss
+
+ private fun invoke(name: String, parameterTypes: Array>, vararg args: Any?) {
+ invokeResult(name, parameterTypes, *args)
+ }
+
+ private fun invokeResult(name: String, parameterTypes: Array>, vararg args: Any?): Any? {
+ val type = bridgeClass
+ if (type == null) {
+ Forensics.warn("Incision canonical Bridge 尚未绑定,忽略操作: $name")
+ return null
+ }
+ return try {
+ type.getMethod(name, *parameterTypes).invoke(null, *args)
+ } catch (t: Throwable) {
+ Forensics.warn("Incision canonical Bridge 调用失败: method=$name reason=${t.message}")
+ null
+ }
+ }
+}
diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/runtime/TheatreDispatcher.kt b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/TheatreDispatcher.kt
index dfb1c1485..1c96f4c59 100644
--- a/module/incision/src/main/kotlin/taboolib/module/incision/runtime/TheatreDispatcher.kt
+++ b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/TheatreDispatcher.kt
@@ -1,6 +1,5 @@
package taboolib.module.incision.runtime
-import io.izzel.incision.bridge.IncisionBridge
import taboolib.module.incision.api.MethodCoordinate
import taboolib.module.incision.api.Resume
import taboolib.module.incision.api.Theatre
@@ -20,7 +19,7 @@ import java.util.concurrent.ConcurrentHashMap
object TheatreDispatcher {
@JvmField
- val BYPASS_MISS: Any = IncisionBridge.bypassMiss()
+ val BYPASS_MISS: Any = CanonicalBridge.bypassMiss()
private val chains = ConcurrentHashMap()
private val wildcardEntries = ConcurrentHashMap>()
@@ -108,6 +107,8 @@ object TheatreDispatcher {
chains.computeIfAbsent(target.signature) { AdviceChain(target) }
fun register(entry: AdviceEntry) {
+ // owner 的 defining loader 可能属于 Leaf、AuraSkills 或其他第三方插件;按签名登记声明方才能精确回调。
+ CanonicalBridge.registerTarget(TheatreDispatcher::class.java, entry.target.signature)
val desc = entry.target.descriptor
if (desc.contains('*')) {
val ownerName = "${entry.target.owner}.${entry.target.name}"
@@ -128,17 +129,34 @@ object TheatreDispatcher {
}
}
+ /**
+ * 为 remap 后的运行时坐标建立同一 advice 链的别名。
+ *
+ * Weaver 写入字节码的是运行时 owner/name/descriptor;dispatcher 若只保存 Mojang/Spigot
+ * 声明坐标,会在旧版 NMS 或成员改名时路由成功却找不到 chain。
+ */
+ internal fun registerRuntimeAlias(runtimeTarget: MethodCoordinate, entries: List) {
+ if (entries.isEmpty()) return
+ CanonicalBridge.registerTarget(TheatreDispatcher::class.java, runtimeTarget.signature)
+ val chain = chainOf(runtimeTarget)
+ entries.forEach(chain::add)
+ }
+
fun unregister(target: MethodCoordinate, id: String): Boolean {
- var removed = chains[target.signature]?.remove(id) ?: false
+ var removed = false
+ // 一个 entry 可能同时存在于声明坐标和 remap 后运行时坐标;按 id 清理全部投影。
+ for ((signature, chain) in chains) {
+ removed = chain.remove(id) || removed
+ if (chain.isEmpty() && chains.remove(signature, chain)) {
+ CanonicalBridge.unregisterTarget(TheatreDispatcher::class.java.classLoader, signature)
+ bodyInvokerCache.keys.removeIf { it.baseSig == signature }
+ }
+ }
val ownerName = "${target.owner}.${target.name}"
wildcardEntries[ownerName]?.let { entries ->
removed = entries.removeIf { it.id == id } || removed
if (entries.isEmpty()) wildcardEntries.remove(ownerName, entries)
}
- if (target.descriptor.contains('*')) {
- // 通配 advice 注册时会被复制进已存在的具体 chain;卸载必须按 id 全量清除这些投影。
- chains.values.forEach { chain -> removed = chain.remove(id) || removed }
- }
predicateWarnedIds.remove(id)
bodyInvokerCache.keys.removeIf { it.baseSig == target.signature }
return removed
diff --git a/module/incision/src/test/java/taboolib/module/incision/bridge/BridgeFixtureDispatcher.java b/module/incision/src/test/java/taboolib/module/incision/bridge/BridgeFixtureDispatcher.java
new file mode 100644
index 000000000..ef914a057
--- /dev/null
+++ b/module/incision/src/test/java/taboolib/module/incision/bridge/BridgeFixtureDispatcher.java
@@ -0,0 +1,23 @@
+package taboolib.module.incision.bridge;
+
+/**
+ * Bridge 路由测试夹具;测试会把同一份字节码定义到两个隔离 ClassLoader,模拟两个插件 lease。
+ */
+public final class BridgeFixtureDispatcher {
+
+ private static String acceptedTarget;
+
+ private BridgeFixtureDispatcher() {}
+
+ public static Object dispatch(String targetSignature, Object self, Object[] args) {
+ return targetSignature.startsWith(acceptedTarget) ? BridgeFixtureDispatcher.class.getClassLoader() : null;
+ }
+
+ public static Object dispatchBypass(String targetSignature, Object self, Object[] args) {
+ return dispatch(targetSignature, self, args);
+ }
+
+ public static void configure(String targetSignature) {
+ acceptedTarget = targetSignature;
+ }
+}
diff --git a/module/incision/src/test/kotlin/taboolib/module/incision/bridge/IncisionBridgeTargetRoutingTest.kt b/module/incision/src/test/kotlin/taboolib/module/incision/bridge/IncisionBridgeTargetRoutingTest.kt
new file mode 100644
index 000000000..38128e952
--- /dev/null
+++ b/module/incision/src/test/kotlin/taboolib/module/incision/bridge/IncisionBridgeTargetRoutingTest.kt
@@ -0,0 +1,87 @@
+package taboolib.module.incision.bridge
+
+import io.izzel.incision.bridge.IncisionBridge
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertFalse
+import org.junit.jupiter.api.Assertions.assertSame
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+
+/**
+ * 覆盖 Leaf 服务端类和第三方插件类均无法用 defining loader 找到切术声明方的场景。
+ */
+@DisplayName("IncisionBridge 多 lease 目标签名路由")
+class IncisionBridgeTargetRoutingTest {
+
+ @Test
+ @DisplayName("两个隔离 lease 存在时按目标签名选择 dispatcher")
+ fun routesByTargetInsteadOfOwnerLoader() {
+ val fixtureName = BridgeFixtureDispatcher::class.java.name
+ val fixturePath = fixtureName.replace('.', '/') + ".class"
+ val bytes = BridgeFixtureDispatcher::class.java.classLoader
+ .getResourceAsStream(fixturePath)!!.use { it.readBytes() }
+ val loaderA = FixtureLoader(fixtureName, bytes)
+ val loaderB = FixtureLoader(fixtureName, bytes)
+ val dispatcherA = Class.forName(fixtureName, true, loaderA)
+ val dispatcherB = Class.forName(fixtureName, true, loaderB)
+ val targetA = "example/Target.method()Ljava/lang/Object;"
+ val targetB = "example/Other.method()Ljava/lang/Object;"
+
+ try {
+ dispatcherA.getMethod("configure", String::class.java).invoke(null, targetA)
+ dispatcherB.getMethod("configure", String::class.java).invoke(null, targetB)
+ IncisionBridge.registerLocalDispatcher(dispatcherA)
+ IncisionBridge.registerLocalDispatcher(dispatcherB)
+ IncisionBridge.registerLocalTarget(dispatcherA, targetA)
+ IncisionBridge.registerLocalTarget(dispatcherB, targetB)
+
+ val result = IncisionBridge.dispatch(dispatcherB, "$targetA@SPLICE", null, emptyArray())
+
+ assertSame(loaderA, result)
+
+ // 兼容尚未调用 registerLocalTarget 的旧插件:owner loader 不属于任一 lease 时广播快照。
+ val legacyTarget = "legacy/Target.method()Ljava/lang/Object;"
+ dispatcherA.getMethod("configure", String::class.java).invoke(null, legacyTarget)
+ val legacyResult = IncisionBridge.dispatch(
+ IncisionBridgeTargetRoutingTest::class.java,
+ "$legacyTarget@SPLICE",
+ null,
+ emptyArray(),
+ )
+ assertSame(loaderA, legacyResult)
+
+ IncisionBridge.bindSystemHost(FakeHost())
+ IncisionBridge.unregisterLocalDispatcher(loaderA)
+ assertEquals(1, IncisionBridge.localLeaseCount())
+ assertTrue(IncisionBridge.hasSystemHost())
+ IncisionBridge.unregisterLocalDispatcher(loaderB)
+ assertEquals(0, IncisionBridge.localLeaseCount())
+ assertFalse(IncisionBridge.hasSystemHost())
+ } finally {
+ IncisionBridge.unregisterLocalDispatcher(loaderA)
+ IncisionBridge.unregisterLocalDispatcher(loaderB)
+ }
+ }
+
+ /** Gate 生命周期夹具;最后一个 lease 释放前 host 必须保持可用。 */
+ class FakeHost {
+ fun dispatch(targetSignature: String, self: Any?, args: Array): Any? = null
+ }
+
+ /** child-first 仅作用于夹具类,确保测试本身仍共享 JUnit 与 Bridge 类型。 */
+ private class FixtureLoader(
+ private val fixtureName: String,
+ private val fixtureBytes: ByteArray,
+ ) : ClassLoader(BridgeFixtureDispatcher::class.java.classLoader) {
+
+ override fun loadClass(name: String, resolve: Boolean): Class<*> {
+ if (name != fixtureName) return super.loadClass(name, resolve)
+ synchronized(getClassLoadingLock(name)) {
+ val loaded = findLoadedClass(name) ?: defineClass(name, fixtureBytes, 0, fixtureBytes.size)
+ if (resolve) resolveClass(loaded)
+ return loaded
+ }
+ }
+ }
+}
diff --git a/module/incision/src/test/kotlin/taboolib/module/incision/runtime/TheatreDispatcherRuntimeAliasTest.kt b/module/incision/src/test/kotlin/taboolib/module/incision/runtime/TheatreDispatcherRuntimeAliasTest.kt
new file mode 100644
index 000000000..71a1c4b18
--- /dev/null
+++ b/module/incision/src/test/kotlin/taboolib/module/incision/runtime/TheatreDispatcherRuntimeAliasTest.kt
@@ -0,0 +1,42 @@
+package taboolib.module.incision.runtime
+
+import io.izzel.incision.bridge.IncisionBridge
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+import taboolib.module.incision.api.MethodCoordinate
+import java.util.concurrent.atomic.AtomicInteger
+
+/** 验证 NMS remap 后字节码使用运行时签名时仍能命中逻辑声明的 advice。 */
+@DisplayName("TheatreDispatcher 运行时坐标别名")
+class TheatreDispatcherRuntimeAliasTest {
+
+ @Test
+ @DisplayName("运行时签名与逻辑签名不同时只执行一次")
+ fun dispatchesRemappedRuntimeAlias() {
+ CanonicalBridge.bind(IncisionBridge::class.java)
+ val calls = AtomicInteger()
+ val logical = MethodCoordinate("net/minecraft/Logical", "logicalName", "()V")
+ val runtime = MethodCoordinate("net/minecraft/Runtime", "runtimeName", "()V")
+ val entry = AdviceEntry(
+ id = "runtime-alias-test",
+ kind = AdviceKind.LEAD,
+ target = logical,
+ priority = 0,
+ handler = { calls.incrementAndGet(); null },
+ )
+
+ try {
+ TheatreDispatcher.register(entry)
+ TheatreDispatcher.registerRuntimeAlias(runtime, listOf(entry))
+
+ TheatreDispatcher.dispatch("${runtime.signature}@LEAD", null, emptyArray())
+
+ assertEquals(1, calls.get())
+ } finally {
+ TheatreDispatcher.unregister(logical, entry.id)
+ TheatreDispatcher.clear()
+ IncisionBridge.unregisterLocalDispatcher(TheatreDispatcher::class.java.classLoader)
+ }
+ }
+}