Skip to content

类增强与 Hook

Rain 的增强层由 classloaderhook 两个模块组成。它的目标不是建立庞大 AOP 容器,而是在类定义前用 ASM 做少量、明确的代码改写,从而支持 Kotlin 委托注入、声明式事务和自定义方法 Hook。

框架作者应先从 Loader、Controller Process 或 Hook 等高层扩展点选择;只有必须改变类结构或调用字节码时才进入 Transformer。选择路径见扩展点总览

何时启用增强

使用完整启动器:

kotlin
fun main() {
    FullStackApplicationLauncher.launch()
}

启动器创建 AppClassloader,注册 Rain Transformer,再通过它重新加载 Application。只有随后由这个类加载器定义的类才能被增强。

以下能力依赖完整启动:

  • inject() / config() Kotlin 属性委托。
  • @HookBy 方法 Hook。
  • SmartAccess @Transactional
  • 自定义 Enchanter 与 Transformer。
  • rain-test 中与生产一致的增强行为。

AppClassloader 加载策略

AppClassloader 是“应用优先、黑名单父优先”的类加载器:

  1. 检查类是否已经加载。
  2. 类命中黑名单且未被白名单覆盖时,交给父类加载器。
  3. 否则读取 .class 字节,依次运行 Transformer,再自行 defineClass
  4. 找不到应用类时回退到父类加载器。

默认将 Rain 类加载接口和 Hook 公共类型放入黑名单,避免父子加载器各自拥有一份导致类型不兼容。

黑白名单

kotlin
appClassloader.registerBlackPackage("com.example.unenhanced")
appClassloader.registerBlackClass("com.example.LegacyType")
appClassloader.registerWhitePackage("com.example.unenhanced.feature")
appClassloader.registerWhiteClass("com.example.LegacyTypeButEnhanced")
  • 黑名单:强制父加载器加载,不做应用增强。
  • 白名单:覆盖黑名单,让 AppClassloader 尝试自行加载。

白名单不是“只有这些类才能增强”;它主要用于覆盖较宽的黑名单规则。

错误设置会造成同名类在两个加载器中各有定义,表现为看似相同类型却 ClassCastException。除框架集成外,业务代码不要随意调整。

自定义 ClassTransformer

kotlin
interface ClassTransformer {
    fun transform(node: ClassNode, className: String): Boolean
}

实现示例:

kotlin
class EntityTransformer : ClassTransformer {
    override fun transform(node: ClassNode, className: String): Boolean {
        if (node.visibleAnnotations.orEmpty().none { it.desc == ENTITY_DESC }) {
            return false
        }
        // 修改 ClassNode
        return true
    }
}

注册:

kotlin
val loader = Thread.currentThread().contextClassLoader as IRainAppClassLoader
loader.registerTransformer(EntityTransformer())

true 表示节点发生变化,需要重新输出字节码;false 表示没有修改。

Transformer 必须在目标类首次加载前注册。JVM 已定义的类不会被 AppClassloader 重新转换,这不是 Java Agent 的 retransformation。

EnchantBy:注解驱动增强

不希望 Transformer 手工检查注解时,可以定义元注解:

kotlin
@EnchantBy(TraceEnchanter::class)
annotation class TraceClass

Enchanter:

kotlin
class TraceEnchanter : Enchanter {
    override fun enchantClass(cn: ClassNode) {
        // 修改类结构
    }

    override fun enchantMethod(cn: ClassNode, mn: MethodNode) {
        // 当前 EnchantManager 主要调用 enchantClass
    }
}

业务类:

kotlin
@TraceClass
class PaymentService

EnchantManager 遍历类的运行时可见注解,加载注解类型,寻找 @EnchantBy,再实例化 Enchanter 执行 enchantClass

当前边界

当前 EnchantManager 源码只在类注解层面触发并调用 enchantClass,没有遍历方法调用 enchantMethod。不要仅实现 enchantMethod 后期待它自动生效。

HookBy:声明式方法 Hook

定义业务注解:

kotlin
@HookBy("com.example.audit.AuditHook")
annotation class Audited(val action: String)

Hook 运行器:

kotlin
class AuditHook : HookRunnable {
    override fun init(info: HookInfo) {
        val annotation = info.method.getAnnotation(Audited::class.java)
        info.saveInfo["audit.action"] = annotation.action
    }

    override fun preRun(context: HookContext): Boolean {
        context.saveInfo("startedAt", System.nanoTime())
        return false
    }

    override fun postRun(context: HookContext) {
        val startedAt = context.getInfo("startedAt") as Long
        audit(context.info.saveInfo["audit.action"], context.result, startedAt)
    }

    override fun onError(context: HookContext): Boolean {
        auditFailure(context.error)
        return false
    }
}

使用:

kotlin
@Audited("invoice.create")
fun createInvoice(command: CreateInvoice): Invoice = TODO()

HookBy.value 是 HookRunnable 的完整类名字符串。这样注解模块不需要在编译期直接依赖 Hook 实现类,但重命名时编译器无法检查字符串,应有启动测试覆盖。

Hook 的字节码结构

HookImpl 不用代理包住 Bean。它会:

  1. 把原方法改名为内部源方法。
  2. 创建同名、同签名的新方法。
  3. 把原方法和参数注解移动到新方法。
  4. 新方法创建 HookContext 并装入参数。
  5. 执行 preRun;未拦截时调用改名后的源方法。
  6. 正常返回执行 postRun;异常执行 onError。

等价伪代码:

kotlin
fun enhancedMethod(args): ReturnType {
    val context = HookContext(info, args)
    if (info.preRun(context)) return context.result as ReturnType
    try {
        context.result = sourceMethod(*context.params)
    } catch (error: Throwable) {
        context.error = error
        if (info.onError(context)) return context.result as ReturnType
        throw context.error
    }
    info.postRun(context)
    return context.result as ReturnType
}

因为参数从 context.params 再传给源方法,preRun 可以替换参数;postRun 可以替换返回值;onError 可以替换异常或把异常恢复为正常结果。

HookRunnable 生命周期

java
interface HookRunnable {
    void init(HookInfo info);
    boolean preRun(HookContext context);
    void postRun(HookContext context);
    boolean onError(HookContext context);
}

init

建立某个 HookInfo 时执行,可读取目标 clazz、增强后 method、改名后的 sourceMethod 和参数类型,把静态元数据保存到 info.saveInfo

preRun

返回 false:继续调用目标方法。

返回 true:中止目标方法,使用 context.result 作为返回值。对于 primitive 返回类型,必须提供兼容值,否则拆箱会失败。

kotlin
override fun preRun(context: HookContext): Boolean {
    if (cache.contains(key(context.params))) {
        context.result = cache[key(context.params)]
        return true
    }
    context.params[0] = normalize(context.params[0])
    return false
}

postRun

目标方法正常返回后执行。context.result 是原返回值,可替换:

kotlin
override fun postRun(context: HookContext) {
    context.result = sanitize(context.result)
}

onError

目标异常放在 context.error

  • 返回 false:继续抛出当前 context.error
  • 返回 true:认为异常已处理,返回 context.result
kotlin
override fun onError(context: HookContext): Boolean {
    if (context.error is OptionalRemoteException) {
        context.result = emptyList<Item>()
        return true
    }
    return false
}

不要用 onError 吞掉编程错误。只恢复有明确业务降级语义的异常。

多个 Hook

HookInfo 保存按注册顺序排列的 HookRunnable:

  • preRun 顺序执行,任一返回 true 就停止。
  • postRun 顺序执行全部,不是反向执行。
  • onError 顺序执行,任一返回 true 就认为异常已处理。

多个 Hook 彼此修改同一个 context。存在强顺序依赖时应合并为一个 Hook,或用框架级注册顺序测试锁定行为。

InstanceMode

HookRunnableInfo 默认可缓存共享实例。给 Hook 类标记 @InstanceMode 后,HookImpl 为目标实例建立对应 Hook 信息,适合 Hook 自身依赖目标对象实例语义的场景。

SmartAccess TransactionHook 使用 @InstanceMode。普通无状态 Hook 优先保持默认模式,避免每实例元数据和字段开销。

编程式 Hook

IHook.registerHook(HookItem) 可以按类名、方法名、descriptor 或自定义 ASM checker 注册:

kotlin
val hook = YuHook.findHook()
hook.registerHook(
    FullMatchHookItem(
        className = "com.example.PaymentService",
        methodName = "pay",
        descriptor = null,
        runnable = hookRunnableInfo,
    )
)

这是低层 API,需要理解 JVM descriptor、类加载时机和 HookRunnableInfo。业务扩展优先使用 @HookBy

Hook 与 Spring AOP 对照

Rain HookSpring AOP
类定义时改写方法通常使用 JDK/CGLIB 代理
可 Hook final 类/方法代理方案通常受 final 限制
同类内部调用仍走增强方法代理自调用常绕过 advice
直接修改参数/结果/异常Around advice 可实现类似能力
必须使用 AppClassloader依赖 Bean 代理与 AOP 容器
扩展 API 较底层Pointcut/Advisor 生态成熟

Rain 的实现核心是一段 ASM 包装方法逻辑,直接且强大;代价是类加载、调试、字节码验证和第三方兼容风险明显更高。

调试与安全建议

  • 先确认目标类的 classLoader 是 AppClassloader。
  • Transformer 注册必须早于目标类加载。
  • 为增强后的 primitive、suspend、静态、异常路径分别写测试。
  • Hook 中避免调用同一个被 Hook 方法造成递归。
  • 不要增强 JDK、Kotlin、ASM、日志等基础包。
  • 遇到 VerifyError 时保存/反编译转换后 class,检查栈和 descriptor。
  • 业务能用普通组合或 Decorator 解决时,不要优先使用字节码增强。

基于 Apache License 2.0 发布