项目中有个需求:在不修改源代码的情况下,替换某个类的引用为我们自己的实现。用一个类似的简单例子来说明:
public class CarHolder {
private Car car;
public CarHolder() {
init();
}
private void init() {
car = new Benz();
}
public void displayCarName() {
System.out.println(car.getCarName());
}
}
正常情况下执行这个类,当调用displayCarName这个方法时会得到"Hi, my name is Benz",但需求是当调用displayCarName时需要输出"Yeah, I am BMW",其实是用BMW这个Car的实现类替换已经预先定义的Benz实现。
需求的本质是修改CarHolder的字节码,让其在运行期的行为与源代码上看起来不一样。修改字节码有两个时机:1. 静态修改,把java文件编译后的class文件替换成我们修改后的class文件,classLoader会加载我们的实现;2. 动态修改,通过特殊的classLoader加载源class文件并修改成我们想要的实现,或是在classLoader加载class文件时,通过JDK Instrumentation组件所提供的ClassFileTransformer机制修改字节码(java.lang.instrument.ClassFileTransformer)。两种策略的结果是一致的,JVM都能执行修改后的字节码。
当前可以修改字节码的组件有十几种,有些体现在源代码级别,有些体现在JVM执行指令级别。最终我选择尝试下BCEL,就是因为通过它可以熟悉class文件的组织结构和JVM指令集的细节,同时它还被加入到sun的内部JDK中,多个框架都在用它,也是学习的一种契机。
在使用BCEL之前,我翻了JVM规范里相关的内容,大致理解了常量池的使用及JVM的常用指令。最终使用的代码像这样
public class StaticChangedCode {
public static void main(String[] args) {
try {
//以对象的方式操纵class文件
JavaClass clazz = Repository.lookupClass(CarHolder.class);
ClassGen classGen = new ClassGen(clazz);
//由于是替换旧类型,所以对于当前常量池中没有的类型/方法/属性等都得一一加入
//常量池项的引用索引,在方法指令中需要调用
ConstantPoolGen cPoolGen = classGen.getConstantPool();
int value = cPoolGen.addClass("bcel.changeimpl.BMW");
int methodIndex = cPoolGen
.addMethodref("bcel.changeimpl.BMW", "<init>", "()V");
int fieldIndex = cPoolGen
.addFieldref("bcel.changeimpl.CarHolder",
"car", "Lbcel/changeimpl/Car;");
//获取想要操纵的方法,因为我知道init方法排行第二,所以这里就写死了
Method sourceMethod = classGen.getMethods()[1];
MethodGen methodGen = new MethodGen(sourceMethod, clazz.getClassName(), cPoolGen);
InstructionList instructionList = methodGen.getInstructionList();
//从原有的指令列表中删去初始化Benz的那部分指令
InstructionHandle[] handles = instructionList.getInstructionHandles();
InstructionHandle from = handles[1];
InstructionHandle to = handles[4];
instructionList.delete(from, to);
//这里开始添加初始化BMW的对象
//对象的创建指令有三步:1. new, 在heap上创建对象结构,分配内存;
//2. dup, 在操作数栈上保留对刚创建对象的引用,然后复制此引用;
//3. invokespecial,利用刚复制出的对象引用标识出对象,然后调用它的<init>方法
//经过上面三步后,对象就可以被使用了,此时做赋值动作,将当前对象赋给car这个变量
InstructionHandle newHandle = instructionList
.append(handles[0], new NEW(value));
InstructionHandle dumpHandle = instructionList
.append(newHandle, new DUP());
InstructionHandle initHandle = instructionList
.append(dumpHandle, new INVOKESPECIAL(methodIndex));
instructionList.append(initHandle, new PUTFIELD(fieldIndex));
//因为上面经历过指令修改的过程,所以这里用新指令的方法去替代旧指令的方法
classGen.replaceMethod(sourceMethod, methodGen.getMethod());
//工作完成,再次生成新的class文件
JavaClass target = classGen.getJavaClass();
target.dump("bin/bcel/changeimpl/CarHolder.class");
} catch (Exception e) {
// TODO: handle exception
}
}
}
之后当你再次运行CarHolder时,它的结果就改变了。上面是静态修改的例子,如果是想要动态修改,那么就利用自己的ClassFileTransformer,把byte数组转化成可操纵的对象,然后与上面的流程一样,最后再返回byte数组给classloader
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
try {
//由byte数组生成JavaClass对象
InputStream inStream = new ByteArrayInputStream(classfileBuffer);
JavaClass jc = new ClassParser(inStream, className).parse();
//这里与上面流程是一样
//再次转化成byte数组,然后给classloader
JavaClass final = ***;
return final.getBytes();
} catch (Exception e) {
// TODO: handle exception
}
return classfileBuffer;
}
这是一个简单例子,从它上面可以看到“神奇”的表现,就像以前看很多框架的神奇之处一样,到头来都是背后做了很多不为人知的事情。同时为了这个例子,也学习到了很多以前很少关注的知识,这才是最大的收获。
分享到:
相关推荐
bcel-6.0
NULL 博文链接:https://andilyliao.iteye.com/blog/899925
java源码:JAVA字节码操作库 BCEL.zip
BCEL在Java字节码工程中的应用分析,王宝龙,,Byte Code Engineering Library(BCEL)是Apache Software Foundation的Jakarta项目的一部分。BCEL是Java class工程中广泛使用的一种框架, 由于它在单独的JVM指令
JAVA字节码操作库 BCEL
使用 BCEL 库优化 Java 字节码 作为 UCL 2nd Year 课程的一部分。 使用 BCEL 库接收 .class 文件(在 testfiles 文件夹中)。 遍历并优化冗余 Go to 语句的字节码,即 goto 指向另一个 go to。 处理将第一个 goto ...
可以动态修改CLASS文件,但是修改后的CLASS文件无法反编译,是一个郁闷的事情,但是这个咚咚还是很好用 提供的只是jar包,没有demo,下载的朋友请注意了!! 只是为了提供jar包下载的方便
JAVA字节码操作库 BCEL.7z
官方离线安装包,测试可用。使用rpm -ivh [rpm完整包名] 进行安装
基于Java的字节码操作库 BCEL.zip
基于java的字节码操作库 BCEL.zip
commons-bcel,阿帕奇公地BCEL.zip
官方离线安装包,测试可用。使用rpm -ivh [rpm完整包名] 进行安装
apache bcel
内容索引:JAVA源码,综合应用,J2me游戏,PNG,图形处理 这是个J2ME控制台程序,它能剔除PNG文件中的非关键数据段,减少文件大小从而达到压缩图片的目的。而图片的质量并不会受到损失。使用时候只需在控制台窗口执行...
内容索引:JAVA源码,综合应用,J2me游戏,PNG,图形处理 这是个J2ME控制台程序,它能剔除PNG文件中的非关键数据段,减少文件大小从而达到压缩图片的目的。而图片的质量并不会受到损失。使用时候只需在控制台窗口执行...
Class Dependency Analyzer (CDA) 是一个免费的,通过分析Java class文件(Apache Commons BCEL™ -Byte Code Engineering Library)来学习类之间依赖关系的工具。 它可以在以下不同的级别上分析依赖关系:一个单独...
该存储库是官方存储库的未经修改的副本,但是BCEL依赖项已更新,以指向Java 8兼容版本,该版本是FindBugs软件包的一部分。 要在本地使用它: git clone https://github.com/thesegovia/maven-shared-jar.git cd ...