如何在Java程序运行时通过JNI Hook Java方法?
可行性结论
完全可以通过C++结合JVM原生接口在运行时实现类Fabric Mixin的方法注入、覆写效果,无需提前修改目标类的class文件,你提到的JVM Handles是其中官方实现路径的核心依赖之一。
实现路径分类
1. 纯C++侧基于JVMTI+JNI实现无侵入Hook(最匹配你的技术选型要求)
这是稳定性、兼容性最好的方案,核心依赖JVM标准原生接口JVMTI(JVM Tool Interface),不需要依赖Java侧的第三方库:
- 首先用C++编写JVMTI Agent,在Agent初始化时注册
ClassFileLoadHook回调,该回调会在类首次加载、类重定义/重转换时触发,你可以在回调中直接拿到对应类的原始字节码数据。 - 如果要Hook的类已经被JVM加载,直接调用JVMTI提供的
RetransformClasses接口触发目标类的重转换即可,不需要重启JVM进程。 - 拿到字节码后做轻量改写即可实现注入效果:定位到目标方法的字节码,要么在方法入口位置插入跳转逻辑直接返回自定义值、完全跳过原有逻辑,要么在方法的return指令前插入自定义逻辑(比如修改返回值、追加埋点逻辑),如果需要调用原方法逻辑,只要提前把原方法的指令备份、在注入逻辑里跳转过去就行。
- 改写完成后把修改后的字节码传回给JVM,就能完成运行时的类重定义,整个过程对上层Java业务代码完全透明。
- 针对你提到的修改getter返回值的场景,实现成本极低:定位到目标getter方法,把方法末尾取字段值、return的逻辑替换成调用你提前写好的JNI native函数就行,native函数里可以直接返回你预设的自定义值,完全不走原有的字段读取逻辑。
- 不要尝试直接在C++侧篡改内存里的JVM方法入口指针做inline Hook,这种方式对JVM版本、甚至小版本的更新极其敏感,HotSpot不同版本的方法内存布局、入口偏移量没有兼容性承诺,生产环境完全不建议用,JVMTI是跨JVM实现的标准接口,在HotSpot、OpenJ9等主流虚拟机上都有稳定支持。
2. 基于MethodHandle实现方法替换(对应你提到的JVM Handles路径)
如果不强制要求所有逻辑都在C++侧实现,可以通过JNI调用Java侧java.lang.invoke包下的MethodHandle API完成方法覆写:
- 核心思路是先通过Agent解锁目标类的访问权限,拿到原方法的MethodHandle存为备用(需要调用原逻辑时直接触发该Handle即可)。
- 再通过
MethodHandles.Lookup生成自定义逻辑的MethodHandle,将目标方法的调用入口绑定到自定义的Handle上,就完成了运行时的方法替换。 - 这种方案的性能优势更明显,MethodHandle是JIT友好的,不会像字节码改写那样容易触发JIT去优化,但高版本JVM默认对运行时方法替换做了权限限制,需要添加额外启动参数解锁能力,灵活度不如直接改写字节码的方案。
研究入手步骤
- 第一步先跑通JVMTI基础链路:写一个最简单的C++ JVMTI Agent,实现Agent加载后打印JVM内所有已加载类名的功能,熟悉C++侧和JVM交互的基本流程。
- 第二步掌握必要的Java字节码知识:不需要啃完所有字节码指令,只要能识别方法边界、方法入口、return指令、字段读写指令,就足够应付绝大多数Hook场景。
- 第三步实现最小可用Demo:针对一个简单的getter方法,在类加载阶段拿到字节码,把返回逻辑改成固定值,通过JVMTI完成类重定义,验证Hook效果。
- 最后再逐步扩展能力:实现方法首尾代码注入、原方法调用、已加载类重转换等逻辑,就能覆盖Fabric Mixin的核心运行时能力。
注意:如果你的目标运行环境是Android ART虚拟机而非标准桌面/服务器端HotSpot JVM,JVMTI能力仅在Android 8.0及以上版本正式提供,低版本需要通过inline Hook篡改ArtMethod结构的入口指针实现,逻辑和标准JVM差异较大。
内容的提问来源于stack exchange,提问作者kpp
相关产品推荐
相关产品推荐

