不可修改原代码时无侵入修改Java方法返回值(兼容Java8/17)
二次开发修改无源码权限方法返回值方案
问题背景
对第三方代码做二次开发时,需要修改me.Mohamad82.MineableGems.Core.DropReader类中readCustomDrop方法的返回值,但完全不允许修改原有项目的任何代码。
原有类核心结构如下:
package me.Mohamad82.MineableGems.Core; public class DropReader { ... public DropReader() {} public CustomDrop readCustomDrop(ConfigurationSection section, String mined, @Nullable String sectionNumber) { ... } }
最初尝试通过继承重写的方式实现需求,代码如下:
package com.rogermiranda1000.mineit; public class DropReader extends me.Mohamad82.MineableGems.Core.DropReader { public DropReader() { super(); } @Override public CustomDrop readCustomDrop(ConfigurationSection section, String mined, @Nullable String sectionNumber) { CustomDrop drop = super.readCustomDrop(section, mined, sectionNumber); if (drop != null) {...} return drop; } }
该方案无法落地的核心障碍:原有代码逻辑(如Commands类)每次调用方法时都会直接new原生DropReader实例,既无法修改原有代码替换为子类实例化逻辑,也无法修改原有方法的调用链路,普通反射方案无法适配。之前考虑过Javassist字节码修改方案,但不确定类加载后注入的可行性。
强制约束
- 当
section.getString("mine") != null时,需修改readCustomDrop的返回值,为其追加额外属性 - 完全不可修改Commands类、原DropReader类的代码,必须保证Commands类拿到的是修改后的CustomDrop对象
- 实现方案需同时兼容Java 8和Java 17版本
可行实现方案
字节码增强的技术方向完全可行,不需要提前修改原有Jar包,采用Java Agent + Javassist 类加载期字节码注入的方案即可满足所有约束,不存在类加载后无法生效的问题——只要在目标类第一次被JVM加载前注册字节码转换器,就可以直接修改加载进JVM的目标类字节码,所有原有逻辑中new DropReader()的调用都会自动执行修改后的方法逻辑。
实现步骤
- 引入Javassist依赖,选择3.29.0及以上版本,该版本原生兼容Java 8到Java 17
- 注册类加载期的字节码转换器,拦截目标DropReader类的加载流程
- 用Javassist修改
readCustomDrop方法,在方法return节点前插入自定义的属性追加逻辑
核心实现代码
import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; public class DropReaderPatchAgent { // 若作为独立Java Agent启动,通过premain入口注入 public static void premain(String args, Instrumentation inst) { registerTransformer(inst); } // 如果是Bukkit/Spigot插件环境,可在onLoad阶段通过反射获取Instrumentation实例后调用该方法注册,无需用户手动添加JVM启动参数 public static void registerTransformer(Instrumentation inst) { inst.addTransformer(new ClassFileTransformer() { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classBuffer) { // 仅拦截目标DropReader类,其他类直接放行 if (!"me/Mohamad82/MineableGems/Core/DropReader".equals(className)) return null; try { ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.get("me.Mohamad82.MineableGems.Core.DropReader"); CtMethod targetMethod = ctClass.getDeclaredMethod("readCustomDrop"); // 插入方法返回前的处理逻辑:Javassist中$_代表方法当前返回值,$1/$2/$3分别对应该方法的三个入参section、mined、sectionNumber String patchCode = "Object result = $_;" + "if (result != null && $1.getString(\"mine\") != null) {" + " // 在此处编写给CustomDrop追加额外属性的逻辑,直接操作result对象即可" + "}" + "$_ = result;"; targetMethod.insertAfter(patchCode); byte[] modifiedBytes = ctClass.toBytecode(); ctClass.detach(); return modifiedBytes; } catch (Exception e) { e.printStackTrace(); return null; } } }); } }
兼容注意事项
- Java 17环境下需要在Jar包的
MANIFEST.MF中添加Add-Opens: java.base/java.lang=ALL-UNNAMED声明,绕过模块访问限制 - 必须保证转换器注册逻辑在DropReader类第一次被加载之前执行,不需要做运行时类重定义,没有HotSwap的方法体修改限制
- 该方案完全不改动原有类的源码逻辑,所有原有实例化、方法调用逻辑完全不变,最终Commands类拿到的就是被修改过属性的CustomDrop对象,满足所有约束要求。
内容的提问来源于stack exchange,提问作者miranda1000
相关产品推荐
相关产品推荐

