`
langyu
  • 浏览: 884405 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

[Java拾遗]初次尝试BCEL:修改类实现的例子

    博客分类:
  • java
jvm 
阅读更多

    项目中有个需求:在不修改源代码的情况下,替换某个类的引用为我们自己的实现。用一个类似的简单例子来说明:


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;
}


    这是一个简单例子,从它上面可以看到“神奇”的表现,就像以前看很多框架的神奇之处一样,到头来都是背后做了很多不为人知的事情。同时为了这个例子,也学习到了很多以前很少关注的知识,这才是最大的收获。
1
1
分享到:
评论
3 楼 s929498110 2012-04-07  
博主你好, 我有一个类似你这个的需求。

具体就是:

class A extends B
new A() 时候肯定首先是调用B的构造方法,我想在B的构造方法中将A中的某个方法修改了,这样可以吗? 静态方法呢?

   希望博主不吝赐教
2 楼 budairenqin 2012-02-01  
你好,例子貌似有点问题吧?
int fieldIndex = cPoolGen  
16.                                 .addFieldref("bcel.changeimpl.CarHolder",   
17.                                        "car", "Lbcel/changeimpl/Car;");

上面这句加上之后常量池里面不是两个Lbcel/changeimpl/Car了?ClassLoader装载的时候通不过验证吧?
1 楼 jiafu1115 2011-09-16  
你这个需求很有意思,哈哈

相关推荐

Global site tag (gtag.js) - Google Analytics