Incision 字节码织入
Incision 是 TabooLib 的运行时织入模块,为 Bukkit/Paper/NMS 场景提供一套可控、可诊断、可回滚的手术式织入能力。
它不是通用 AOP 框架,也不是编译期 Mixin 系统。它更像一套给 Minecraft 服务端插件准备的手术工具:下刀点要准,生命周期要能控,出了问题要能查,必要时还得能撤。
先把它理解对
最容易把 Incision 想歪的地方有三个:
- 它不是动态代理。不会新建
$Proxy类,不需要接口,连目标类自己内部的调用、final方法、NMS 那种根本不走代理链的路径,它也能碰到。 - 它不是编译期 Mixin。Mixin 在程序启动前改好目标类;Incision 在类已经加载到 JVM 后做运行时织入。
- 它不是拿来把业务逻辑全都写成 patch 的。能正常写接口、事件、服务层的地方,还是正常写。
适用场景
适合:
- 在 Bukkit/Paper 插件里做方法入口、出口、调用点级别的轻量织入
- 对 NMS/Bukkit 方法做版本门控、remap 后再织入
- 对 Kotlin
object、companion、@JvmStatic目标做双路径覆盖 - 做临时 patch、范围 patch、线程局部 patch、批量 patch
- 对运行时问题提供可回滚的诊断性织入
- 读写目标类的 private/final/static 字段,或调用 private 方法
不适合:
- 把整套业务逻辑长期建立在大规模字节码改写之上
- 期望它替代完整字节码框架或编译期 Mixin 系统
- 在完全不了解目标字节码形态时直接依赖复杂
InsnPattern
两套入口
Incision 同时支持两套入口:
| 入口 | 写法 | 推荐场景 |
|---|---|---|
| 注解模式 | @Surgeon + @Lead/@Trail/@Splice/... | 长期、稳定、声明式的 patch(默认首选) |
| DSL 模式 | Scalpel { ... } / Scalpel.transient { ... } | 临时、作用域、线程局部、事件驱动的 patch |
能写成 @Surgeon 的长期 patch,就别先写成 DSL。只有 patch 的生命周期本身要动态控制时,再上 DSL。
Advice 类型总表
| 类型 | DSL/注解 | 典型用途 | 是否替换原逻辑 | 关键约束 |
|---|---|---|---|---|
| Lead | lead / @Lead | 入口探针、计数、参数观察 | 否 | 只能表达入口语义 |
| Trail | trail / @Trail | 正常出口或异常出口收尾 | 否 | onThrow=false 时不覆盖异常出口 |
| Splice | splice / @Splice | 环绕、短路、改参与放行 | 可选 | 必须显式 proceed/skip/override |
| Graft | graft / @Graft | 在锚点前后追加逻辑 | 否 | 原指令仍会执行 |
| Bypass | bypass / @Bypass | 把单个调用点重定向到 handler | 是,替换单点 | 只替换目标位点,不替换整个方法 |
| Trim | trim / @Trim | 改写参数、返回值、局部变量 | 改值,不改流程 | 必须保证值类型兼容 |
| Excise | excise / @Excise | 整段方法覆写 | 是,替换整个方法 | 同一目标只能有一个 Excise |
如果你从 Mixin 体系过来,可以这样对照:
| Mixin | Incision |
|---|---|
@Inject(at = @At("HEAD")) | @Lead |
@Inject(at = @At("RETURN")) | @Trail |
| around + cancel / proceed | @Splice |
@Redirect | @Bypass |
@ModifyArg / @ModifyVariable | @Trim |
@Overwrite | @Excise |
注解模式(推荐)
基础结构
import taboolib.module.incision.annotation.Lead
import taboolib.module.incision.annotation.Operation
import taboolib.module.incision.annotation.Splice
import taboolib.module.incision.annotation.Surgeon
import taboolib.module.incision.annotation.Trail
import taboolib.module.incision.api.Theatre
@Surgeon(priority = 50)
object DemoSurgeon {
@Lead(scope = "method:top.example.Target#greet(java.lang.String)java.lang.String")
fun beforeGreet(theatre: Theatre) {
println("before: ${theatre.arg<String>(0)}")
}
@Splice(scope = "method:top.example.Target#greet(java.lang.String)java.lang.String")
@Operation(id = "rewrite-name", priority = 100)
fun aroundGreet(theatre: Theatre): Any? {
val name = theatre.arg<String>(0) ?: return theatre.resume.proceed()
if (name == "Admin") {
return theatre.override("blocked")
}
return theatre.resume.proceed(name.uppercase())
}
@Trail(
scope = "method:top.example.Target#greet(java.lang.String)java.lang.String",
onThrow = true
)
fun afterGreet(theatre: Theatre) {
if (theatre.throwable != null) {
println("throw: ${theatre.throwable?.message}")
} else {
println("after greet")
}
}
}
代码说明:
@Surgeon只能标在 Kotlinobject上,声明"这个对象里放的是注解式 patch"@Surgeon(priority = 50)设置类级默认优先级@Operation可以覆盖类级默认优先级和启用状态- 扫描期会把方法翻译成
AdviceEntry,注册进 dispatcher,再触发织入
@Lead:方法入口
最适合做前置探针、参数观察、记日志、轻量前置判断。
@Lead(scope = "method:top.example.Target#greet(java.lang.String)java.lang.String")
fun beforeGreet(theatre: Theatre) {
println("before: ${theatre.arg<String>(0)}")
}
别拿 @Lead 做整段控制流接管。想"放不放行",请直接用 @Splice。
@Trail:方法出口
适合正常返回前收尾、异常抛出前补日志、做统计。
@Trail(
scope = "method:top.example.Target#greet(java.lang.String)java.lang.String",
onThrow = true
)
fun afterGreet(theatre: Theatre) {
if (theatre.throwable != null) {
println("异常: ${theatre.throwable?.message}")
} else {
println("正常返回")
}
}
onThrow = true 时同时覆盖异常出口,默认只覆盖正常返回路径。
@Splice:环绕控制
控制力最强的 advice 类型,也最容易写出坑。命中后你必须明确表态:
@Splice(scope = "method:top.example.Target#greet(java.lang.String)java.lang.String")
fun aroundGreet(theatre: Theatre): Any? {
val name = theatre.arg<String>(0) ?: return theatre.resume.proceed()
if (name == "Admin") {
// 短路:不执行原方法,直接返回
return theatre.override("blocked")
}
// 改参后放行
return theatre.resume.proceed(name.uppercase())
}
Resume 操作:
| 操作 | 含义 |
|---|---|
theatre.resume.proceed() | 放行,继续执行原方法 |
theatre.resume.proceed(newArgs...) | 改参后放行 |
theatre.resume.proceedResult(value) | 带着一个新结果继续往下传 |
theatre.override(value) / resume.skip(value) | 直接短路,不执行原方法 |
如果 @Splice 的 handler 什么都不干(既不 proceed 也不 skip),会触发 ResumeMissing。这不是温柔提示,而是明确告诉你:这段环绕逻辑没把路走完。
@Graft:锚点前后追加逻辑
在某个调用点、字段访问、构造指令等锚点前后追加逻辑,但原指令照跑。适合做探针、补日志、埋点。
import taboolib.module.incision.annotation.Graft
import taboolib.module.incision.annotation.Site
import taboolib.module.incision.api.Anchor
import taboolib.module.incision.api.Shift
@Graft(
method = "top.example.Target#greet(java.lang.String)java.lang.String",
site = Site(
anchor = Anchor.INVOKE,
target = "top.example.Logger#print(java.lang.String)void",
shift = Shift.BEFORE
)
)
fun beforePrint(theatre: Theatre) {
println("logger is about to run")
}
@Bypass:调用点替换
把某个调用点直接换掉,类似 Mixin 的 @Redirect。原来的那次调用不会再执行。
import taboolib.module.incision.annotation.Bypass
import taboolib.module.incision.annotation.Site
import taboolib.module.incision.api.Anchor
@Bypass(
method = "top.example.Target#greet(java.lang.String)java.lang.String",
site = Site(
anchor = Anchor.INVOKE,
target = "top.example.Service#load(java.lang.String)java.lang.String"
)
)
fun replaceLoad(theatre: Theatre): Any? {
return "mocked"
}
@Trim:值改写
不接管流程,只改参数、返回值或局部变量的值。
import taboolib.module.incision.annotation.Trim
@Trim(
method = "top.example.Target#greet(java.lang.String,int)java.lang.String",
kind = Trim.Kind.ARG,
index = 0
)
fun rewriteName(theatre: Theatre): Any? {
return "patched"
}
@Excise:整段方法覆写
重型武器。原方法体不跑了,全部交给你。
import taboolib.module.incision.annotation.Excise
@Excise(scope = "method:top.example.Target#greet(java.lang.String)java.lang.String")
fun overwrite(theatre: Theatre): Any? {
return "direct result"
}
@Excise 风险最大,和别人冲突的概率最高,同一 target 只允许一个 Excise。能用 @Lead、@Trail、@Splice 解决的,尽量别直接 @Excise。
DSL 模式
DSL 的入口是 Scalpel,必须放在 @SurgeryDesk object 内。
持久 patch
import taboolib.module.incision.annotation.SurgeryDesk
import taboolib.module.incision.api.Suture
import taboolib.module.incision.dsl.Scalpel
@SurgeryDesk
object DemoDesk {
val greetPatch: Suture by Scalpel {
lead("top.example.Target#greet(java.lang.String)java.lang.String") { theatre ->
println("before greet: ${theatre.args[0]}")
}
}
}
临时 patch
@SurgeryDesk
object DemoDesk {
fun patchOnce() {
Scalpel.transient {
splice("top.example.Target#greet(java.lang.String)java.lang.String") { theatre ->
println("args = ${theatre.args.contentToString()}")
theatre.resume.proceed()
}
}.use {
// 只在这个作用域里生效
}
}
}
DSL 模式一览
| 模式 | 作用 | 说明 |
|---|---|---|
Scalpel {} | 持久 patch | 通常作为属性委托,返回 Suture |
Scalpel.deferred | 惰性 patch | 延迟到首次访问或目标类加载后 arm |
Scalpel.transient {} | 一次性 patch | 需手动 heal 或 use |
Scalpel.scoped {} | 作用域 patch | 块内生效,块外自动回收 |
Scalpel.threadLocal {} | 线程局部 patch | 默认不启用,按线程激活 |
Scalpel.armOn/disarmOn | 事件驱动 patch | 返回 ArmTrigger,由调用方决定何时 arm/disarm |
Scalpel.exclusive | 互斥 patch | 块内挂起同 target 的其他 ARMED patch |
Theatre:Handler 的工作台
Theatre 是 advice 执行时看到的上下文,几乎所有 handler 都围着它转。
| 属性/方法 | 说明 |
|---|---|
self | 当前实例,静态方法时为 null |
args | 参数数组,能读也能改 |
target | 当前命中的方法坐标 |
throwable | 异常出口时能看到异常 |
arg<T>(index) | 按类型取参数,越界返回 null |
argOrThrow<T>(index) | 按类型取参数,越界抛异常 |
selfAs<T>() | 把 self 安全转型 |
resume | 只有 @Splice 最依赖,控制后续流程 |
Suture 生命周期
不管是 DSL 还是注解扫描出来的 patch,最终都会有自己的 Suture。
| 状态 | 含义 |
|---|---|
ARMED | 已织入并启用 |
TRIGGERED | 已触发过一次以上 |
SUSPENDED | 字节码仍在,但 dispatcher 跳过 handler |
HEALED | 已卸载或回滚 |
INACTIVE_UNRESOLVED | 声明未成功解析 |
控制接口:
heal():永久卸载suspend():临时停用,但不回滚织入点resume():恢复已挂起的 patchclose():等价于heal()
访问 private 字段与方法
Handler 内可以读写目标类(或任意其他类)的 private/final/static 字段,以及调用 private 方法。底层走 JVMTI JNI,完全绕过 Java 访问控制,不依赖 setAccessible,不受 JDK 17+ 模块封装影响。
Lambda 工厂(推荐)
类级声明,解析一次,处处复用:
import taboolib.module.incision.api.*
import taboolib.module.incision.annotation.Surgeon
import taboolib.module.incision.annotation.Lead
@Surgeon
object AccessDemo {
private val privateFinalName = field<String>("privateFinalName")
private val protectedValue = field<Double>("protectedValue")
private val setMutableCount = fieldSet<Int>("privateMutableCount")
private val getMutableCount = field<Int>("privateMutableCount")
private val staticSecret = staticField<String>(SomeClass::class.java, "STATIC_SECRET")
private val setStaticCounter = staticFieldSet<Int>(SomeClass::class.java, "staticCounter")
private val getStaticCounter = staticField<Int>(SomeClass::class.java, "staticCounter")
private val privateAdd = method<Int>("privateAdd", "(II)I")
private val privateGreet = method<String>("privateGreet")
@Lead(scope = "method:top.example.Target#run()void")
fun beforeRun(theatre: Theatre) {
val name = privateFinalName(theatre)
val value = protectedValue(theatre)
setMutableCount(theatre, 42)
val count = getMutableCount(theatre)
val secret = staticSecret()
setStaticCounter(99)
val counter = getStaticCounter()
val sum = privateAdd(theatre, 10, 20)
val greeting = privateGreet(theatre, "world")
}
}
工厂函数一览:
| 工厂函数 | 返回类型 | 调用形式 |
|---|---|---|
field<T>(name) | FieldAccessor<T> | accessor(theatre) 或 accessor(receiver) |
field<T>(ownerClass, name) | FieldAccessor<T> | 同上,指定声明类 |
staticField<T>(ownerClass, name) | StaticFieldAccessor<T> | accessor() |
fieldSet<T>(name) | FieldSetter<T> | setter(theatre, value) |
fieldSet<T>(ownerClass, name) | FieldSetter<T> | 同上 |
staticFieldSet<T>(ownerClass, name) | StaticFieldSetter<T> | setter(value) |
method<T>(name, descriptor?) | MethodAccessor<T> | accessor(theatre, arg1, arg2) |
staticMethod<T>(ownerClass, name, descriptor?) | StaticMethodAccessor<T> | accessor(arg1, arg2) |
Theatre 直接调用
适用于一次性、不值得声明 val 的场景:
@Lead(scope = "...")
fun handler(t: Theatre) {
val name: String? = t.field("playerName")
val count: Int? = t.staticField(SomeClass::class.java, "MAX_COUNT")
t.setField("enabled", false)
t.invoke<Unit>("notifyAll")
}
通用工具扩展
以下顶层扩展函数可在任何地方使用,不限于 Theatre 作用域:
import taboolib.module.incision.api.*
val greetable: Greetable? = someObject.cast<Greetable>()
val str: String = someObject.castOrThrow<String>()
val secret: String? = someObject.readField<String>("secret")
someObject.writeField("secret", "modified")
val result: String? = someObject.callMethod<String>("greet")
- 修改
static final原始类型或 String 字段可能不会对已 JIT 过的调用点生效(常量折叠) - JVMTI 不可用时自动降级到反射 + Unsafe,但 JDK 17+ 非开放模块的 private 字段可能降级失败
- 方法重载场景下,如果按参数类型匹配到多个候选,需要显式传入
descriptor参数
锚点与落点
锚点类型
| Anchor | 含义 | 常见用途 |
|---|---|---|
HEAD | 方法入口 | Lead、参数 Trim |
TAIL | 正常出口前 | Trail 收尾 |
RETURN | return 指令前 | 返回值 Trim |
INVOKE | 方法调用处 | Graft/Bypass 调用点 |
FIELD_GET | 字段读 | 字段读取探针 |
FIELD_PUT | 字段写 | 字段写入探针 |
NEW | new 指令 | 构造前后探针 |
THROW | 抛异常处 | 异常路径观察 |
Site 参数
| 字段 | 说明 |
|---|---|
anchor | 锚点类型 |
target | 锚点目标,例如 owner#name(desc)ret |
shift | BEFORE / AFTER |
ordinal | 第几个命中,-1 表示全部 |
offset | 相对锚点再移动几条指令 |
方法描述符
Incision 内部统一使用以下格式:
owner#method(arg1,arg2,...)returnType
示例:
| 写法 | 含义 |
|---|---|
org.bukkit.entity.Player#kickPlayer(java.lang.String)void | 实例方法 |
top.example.Target$Companion#echo(java.lang.String)java.lang.String | Kotlin companion 实例方法 |
net.minecraft.server.MinecraftServer#getPlayerCount()int | NMS 方法 |
描述符错误通常会落到 Trauma.Declaration.BadDescriptor,请仔细检查 owner、方法名、参数类型和返回类型。
版本门控与 Remap
@Version
在扫描期决定某条 advice 是否注册。默认 matcher 走 Minecraft/NMS 版本,支持自定义 matcher FQCN。
Remap
用户声明可以写逻辑上的 NMS owner/name/desc,运行时交给 RemapRouter / TabooLibNmsResolver 解析到实际类名。安装 weaver 时会先做 owner 级映射,再对已加载类做 retransform。
@KotlinTarget
解决 Kotlin companion 实例方法与 @JvmStatic 静态桥接方法是两条调用路径的问题。可分别扩展到 companionInstance 和 jvmStaticBridge。
@Operation 元信息
方法级 @Operation 可以覆盖类级 @Surgeon 的默认优先级和启用状态:
@Surgeon(priority = 50)
object DemoSurgeon {
@Lead(scope = "method:top.example.Target#run()void")
@Operation(id = "my-lead", priority = 100, enabled = true)
fun beforeRun(theatre: Theatre) {
// priority 100 覆盖类级的 50
}
@Trail(scope = "method:top.example.Target#run()void")
@Operation(id = "disabled-trail", enabled = false)
fun afterRun(theatre: Theatre) {
// 默认禁用,可通过 Suture.resume() 运行时启用
}
}
排序规则:
- 按
priority降序 - 同优先级保持注册顺序
- 类级
@Surgeon(priority)是默认值 - 方法级
@Operation(priority)可覆盖类级默认值
实战示例:锋利附魔伤害拦截
以下示例展示如何用 @Splice 精确拦截 NMS 的附魔伤害结算方法,观察锋利附魔的真实伤害贡献:
import net.minecraft.core.Holder
import net.minecraft.core.component.DataComponents
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.damagesource.DamageSource
import net.minecraft.world.entity.Entity
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.enchantment.Enchantment
import net.minecraft.world.item.enchantment.Enchantments
import net.minecraft.world.item.enchantment.ItemEnchantments
import org.apache.commons.lang3.mutable.MutableFloat
import taboolib.module.incision.annotation.Operation
import taboolib.module.incision.annotation.Splice
import taboolib.module.incision.annotation.Surgeon
import taboolib.module.incision.api.Theatre
private const val MODIFY_DAMAGE =
"method:net.minecraft.world.item.enchantment.EnchantmentHelper#modifyDamage(net.minecraft.server.level.ServerLevel,net.minecraft.world.item.ItemStack,net.minecraft.world.entity.Entity,net.minecraft.world.damagesource.DamageSource,float)float"
@Surgeon
object SharpnessModifier {
var customFormula: (level: Int) -> Float = { level ->
2.0f + 1.5f * (level - 1)
}
@Splice(scope = MODIFY_DAMAGE)
@Operation(id = "sharpness-modifier", enabled = true)
fun modifySharpness(theatre: Theatre): Any? {
val serverLevel = theatre.arg<ServerLevel>(0) ?: return theatre.resume.proceed()
val itemStack = theatre.arg<ItemStack>(1) ?: return theatre.resume.proceed()
val victim = theatre.arg<Entity>(2) ?: return theatre.resume.proceed()
val damageSource = theatre.arg<DamageSource>(3) ?: return theatre.resume.proceed()
val baseDamage = theatre.arg<Float>(4) ?: return theatre.resume.proceed()
val enchantments: ItemEnchantments =
itemStack.getOrDefault(DataComponents.ENCHANTMENTS, ItemEnchantments.EMPTY)
if (enchantments.isEmpty) return theatre.resume.proceed()
val result = MutableFloat(baseDamage)
for (entry in enchantments.entrySet()) {
@Suppress("UNCHECKED_CAST")
val holder = entry.key as Holder<Enchantment>
val level = entry.intValue
if (holder.`is`(Enchantments.SHARPNESS)) {
val customBonus = customFormula(level)
result.add(customBonus)
} else {
holder.value().modifyDamage(serverLevel, level, itemStack, victim, damageSource, result)
}
}
return result.toFloat()
}
}
代码说明:
- 使用
@Splice完全接管EnchantmentHelper.modifyDamage的逻辑 - 对锋利附魔使用自定义公式替换原版计算
- 其他附魔原样调用
Enchantment.modifyDamage() @Operation(id = "sharpness-modifier")方便运行时通过 id 查找和管理
实战示例:Essentials /list 命令钩子
import org.bukkit.command.CommandSender
import taboolib.module.incision.annotation.Lead
import taboolib.module.incision.annotation.Surgeon
import taboolib.module.incision.annotation.Trail
import taboolib.module.incision.api.Theatre
import taboolib.module.incision.api.callMethod
@Surgeon
object EssentialsListHook {
private const val TARGET = "method:com.earth2me.essentials.commands.Commandlist#run(*)"
private fun getSender(theatre: Theatre): CommandSender? {
val source = theatre.arg<Any>(1) ?: return null
return source.callMethod<CommandSender>("getSender")
}
@Lead(scope = TARGET)
fun beforeList(theatre: Theatre) {
getSender(theatre)?.sendMessage("§a[Incision] 即将执行 /list 命令...")
}
@Trail(scope = TARGET)
fun afterList(theatre: Theatre) {
getSender(theatre)?.sendMessage("§a[Incision] /list 命令执行完毕!")
}
}
代码说明:
- 使用通配描述符
run(*)匹配方法 - 通过
callMethod调用目标对象的 private 方法获取CommandSender @Lead在命令执行前发送提示,@Trail在执行后发送提示
技术原理概览
工作流程
关键设计
-
字节码里写的是桥调用(
IncisionBridge.dispatch),不是业务 handler 本体。这样 patch 可以统一启停,handler 可以热插拔。 -
桥放在不被 TabooLib Gradle 插件 relocate 的
io.izzel.*包下,所有插件的织入字节码都指向同一个桥类,解决跨插件 ClassLoader 问题。 -
同一个 owner 上的 advice 会先聚合后再织入,避免重复 retransform。
-
织入前会通过
FrameVerifier预检帧一致性,失败就回退原字节码,不把坏 class 喂给 JVM。
两个后端
| 后端 | 原理 | 生产环境可用性 |
|---|---|---|
| InstrumentationBackend | self-attach → Instrumentation.retransformClasses | 受限(JDK 21+ 需开关,Paper 常禁) |
| JvmtiBackend | 预编译 native agent → JVMTI RetransformClasses | 最稳(不依赖 attach 权限) |
JvmtiBackend 是生产环境主力,额外提供原字节码缓存、任意 ClassLoader 的 defineClass、无访问控制的字段/方法访问等独家能力。
生命周期
@Surgeon 扫描和物理织入尽量前推到 LifeCycle.CONST,这是 TabooLib 给用户代码开放的最早窗口。INIT、LOAD、ENABLE 阶段的宿主逻辑都可以被更早注册的 patch 命中。
仍然不能拦截的:
- 插件 main class 的静态初始化块(比 CONST 更早)
- TabooLib 自身启动阶段的代码
- bootstrap/system ClassLoader 上某些核心类
诊断与排错
优先看三类信息:Forensics.debug/warn、Trauma.*、对应分类测试用例。
| 现象 | 常见原因 | 先看哪里 |
|---|---|---|
| advice 未命中 | 描述符错、scope 过宽或过窄、pattern 不匹配 | DescriptorCodec、Scope、InsnPattern |
ResumeMissing | Splice 没有显式放行或短路 | handler 本身 |
| 只命中 Java,不命中 Kotlin | 漏了 companion / @JvmStatic 扩展 | @KotlinTarget |
| 某些 NMS 版本不生效 | 版本过滤或 remap 结果不一致 | @Version、RemapRouter |
| 同 target 顺序不对 | 优先级或注册顺序认知错误 | AdviceChain 排序规则 |
常见问题
能用 @Surgeon 就别先上 DSL
注解式 patch 更稳定,也更容易统一管理。DSL 的重点不是"语法更帅",而是"生命周期能不能动态管理"。
@Splice 一定要明确 proceed 还是 override
忘了不是"默认继续",而是直接触发 ResumeMissing。
先把 target 写准,再谈 pattern
更稳的顺序:先把 descriptor/scope/site 写准 → 真不够用再加 InsnPattern → 再复杂的条件用 where 做二次筛选。
Kotlin companion 和 @JvmStatic 不是一条路
你以为自己 patch 到了一个 Kotlin "静态方法",实际命中的可能只是一条路径。这时要记得 @KotlinTarget。
@Excise 别当常规手段
能 Lead、Trail、Splice 解决的,尽量别直接 Excise。
学习顺序建议
- 先学
@Surgeon、@Lead、@Trail - 再学
@Splice,把Resume的语义搞明白 - 再去看
@Graft、@Bypass、@Trim - 最后再碰
@Excise、InsnPattern、复杂Site、where - 需要动态启停时,再去学 DSL 的
Scalpel
术语对照表
| 术语 | 对外理解 | 代码对应 |
|---|---|---|
| 手术 / patch | 一组对目标方法生效的织入声明 | Suture |
| 施术者 | 持有注解式 advice 的 object | @Surgeon |
| 工作台 | 持有 DSL patch 的 object | @SurgeryDesk |
| 现场 | advice 执行时看到的上下文 | Theatre |
| 放行 | 继续执行原方法或原指令 | resume.proceed() |
| 短路 | 不再执行原方法,直接给结果 | resume.skip() / override() |
| 锚点 | 要插入或替换的字节码位置 | Anchor / Site |
| 链 | 某个 target 下的 advice 顺序集合 | AdviceChain |
| 调度器 | 运行时按 target 分发 advice 的中心 | TheatreDispatcher |
| 织入器 | 把 dispatcher 调用写回字节码的组件 | Scalpel.installWeaver / SiteWeaver |