项目中有个需求:在不修改源代码的情况下,替换某个类的引用为我们自己的实现。用一个类似的简单例子来说明:
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;
}
这是一个简单例子,从它上面可以看到“神奇”的表现,就像以前看很多框架的神奇之处一样,到头来都是背后做了很多不为人知的事情。同时为了这个例子,也学习到了很多以前很少关注的知识,这才是最大的收获。