1: 在那些依赖关系需要动态确认的场景:
2: 需要在运行时动态插入代码的场景,比如动态代理的实现。
3: 通过配置文件来实现相关功能的场景
Java中如何使用
此处我们主要说一下通过动态生成字节码的方式,其他方式可以自行查找资料。
操作java字节码的工具有两个比较流行,一个是ASM,一个是Javassit 。
ASM :直接操作字节码指令,执行效率高,要是使用者掌握Java类字节码文件格式及指令,对使用者的要求比较高。
Javassit 提供了更高级的API,执行效率相对较差,但无需掌握字节码指令的知识,对使用者要求较低。
应用层面来讲一般使用建议优先选择Javassit,如果后续发现Javassit 成为了整个应用的效率瓶颈的话可以再考虑ASM.当然如果开发的是一个基础类库,或者基础平台,还是直接使用ASM吧,相信从事这方面工作的开发者能力应该比较高。
上一张国外博客的图,展示处理Java字节码的工具的关系。
接下来介绍如何使用Javassit来操作字节码
Javassit使用方法
Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。
它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。
javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。
Javassist中最为重要的是ClassPool,CtClass ,CtMethod 以及 CtField这几个类。
ClassPool:一个基于HashMap实现的CtClass对象容器,其中键是类名称,值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的类路径,因此在某些情况下,可能需要向ClassPool添加类路径或类字节。
CtClass:表示一个类,这些CtClass对象可以从ClassPool获得。
CtMethods:表示类中的方法。
CtFields :表示类中的字段。
动态生成一个类
下面的代码会生成一个实现了Cloneable接口的类GenerateClass
public void DynGenerateClass() { ClassPool pool = ClassPool.getDefault(); CtClass ct = pool.makeClass("top.ss007.GenerateClass");//创建类 ct.setInterfaces(new CtClass[]{pool.makeInterface("java.lang.Cloneable")});//让类实现Cloneable接口 try { CtField f= new CtField(CtClass.intType,"id",ct);//获得一个类型为int,名称为id的字段 f.setModifiers(AccessFlag.PUBLIC);//将字段设置为public ct.addField(f);//将字段设置到类上 //添加构造函数 CtConstructor constructor=CtNewConstructor.make("public GeneratedClass(int pId){this.id=pId;}",ct); ct.addConstructor(constructor); //添加方法 CtMethod helloM=CtNewMethod.make("public void hello(String des){ System.out.println(des);}",ct); ct.addMethod(helloM); ct.writeFile();//将生成的.class文件保存到磁盘 //下面的代码为验证代码 Field[] fields = ct.toClass().getFields(); System.out.println("属性名称:" + fields[0].getName() + " 属性类型:" + fields[0].getType()); } catch (CannotCompileException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (NotFoundException e) { e.printStackTrace(); } } |
|
上面的代码就会动态生成一个.class文件,我们使用反编译工具,例如Bytecode Viewer,查看生成的字节码文件GenerateClass.class,如下图所示。
动态添加构造函数及方法
有很多种方法添加构造函数,我们使用CtNewConstructor.make,他是一个的静态方法,其中有一个重载版本比较方便,如下所示。第一个参数是source text 类型的方法体,第二个为类对象。
CtConstructor constructor=CtNewConstructor.make("public GeneratedClass(int pId){this.id=pId;}",ct); ct.addConstructor(constructor); |
这段代码执行后会生成如下java代码,代码片段是使用反编译工具JD-GUI产生的,可以看到构造函数的参数名被修改成了paramInt。
public GeneratedClass(int paramInt) { this.id = paramInt; } |
|
同样有很多种方法添加函数,我们使用CtNewMethod.make这个比较简单的形式
CtMethod helloM=CtNewMethod.make("public void hello(String des){ System.out.println(des);}",ct); ct.addMethod(helloM); |
这段代码执行后会生成如下java代码:
public void hello(String paramString) { System.out.println(paramString); } |
|
动态修改方法体
动态的修改一个方法的内容才是我们关注的重点,例如在AOP编程方面,我们就会用到这种技术,动态的在一个方法中插入代码。
例如我们有下面这样一个类
public class Point { private int x; private int y; public Point(){} public Point(int x, int y) { this.x = x; this.y = y; } public void move(int dx, int dy) { this.x += dx; this.y += dy; } } } |
我们要动态的在内存中在move()方法体的前后插入一些代码
public void modifyMethod() { ClassPool pool=ClassPool.getDefault(); try { CtClass ct=pool.getCtClass("top.ss007.Point"); CtMethod m=ct.getDeclaredMethod("move"); m.insertBefore("{ System.out.print("dx:"+$1); System.out.println("dy:"+$2);}"); m.insertAfter("{System.out.println(this.x); System.out.println(this.y);}"); ct.writeFile(); //通过反射调用方法,查看结果 Class pc=ct.toClass(); Method move= pc.getMethod("move",new Class[]{int.class,int.class}); Constructor<?> con=pc.getConstructor(new Class[]{int.class,int.class}); move.invoke(con.newInstance(1,2),1,2); } ... } |
|
使用反编译工具查看修改后的move方法结果:
public void move(int dx, int dy) { System.out.print("dx:" + dx);System.out.println("dy:" + dy); this.x += dx; this.y += dy; Object localObject = null;//方法返回值 System.out.println(this.x);System.out.println(this.y); } |
|
可以看到,在生成的字节码文件中确实增加了相应的代码。
函数输出结果为:
dx:1dy:2 2 4 |
Javassit 还有许多功能,例如在方法中调用方法,异常捕捉,类型强制转换,注解相关操作等,而且其还提供了字节码层面的API(Bytecode level API)。
什么原理
反射:由于Java执行过程中是将类型载入虚拟机中的,在运行时我们就可以动态获取到所有类型的信息。只能获取却不能修类型信息。
动态编译与动态生成字节码:这两种方法比较相似,原理也都是利用了Java的设计原理,存在一个虚拟机执行字节码,这就使我们在此处有了改变字节码的操作空间。