动态编程是相对于静态编程而言的,平时我们讨论比较多的静态编程语言例如Java, 与动态编程语言例如JavaScript相比,二者有什么明显的区别呢? 简单的说就是在静态编程中,类型检查是在编译时完成的,而动态编程中类型检查是在运行时完成的, 所谓动态编程就是绕过编译过程在运行时进行操作的技术。
我们常用到的动态特性主要是反射,在运行时查找对象的属性和方法,修改作用域,通过方法名称调用方法等。在线的应用不建议频繁使用反射,因为反射的性能开销较大。
在java的java.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口,通过这个类和这个接口可以生成JDK动态代理类和动态代理对象。
动态编译是从Java 6开始支持的,主要是通过一个JavaCompiler接口来完成的。通过这种方式我们可以直接编译一个已经存在的java文件,也可以在内存中动态生成Java代码,动态编译执行。
Java 6加入了对Script(JSR223)的支持。这是一个脚本框架,提供了让脚本语言来访问Java内部的方法。你可以在运行的时候找到脚本引擎,然后调用这个引擎去执行脚本,这个脚本API允许你为脚本语言提供Java支持。
操作java字节码的工具有BECL/ASM/CGLIB/Javassist,其中有两个比较流行的,一个是ASM,一个是Javassist。 ASM直接操作字节码指令,执行效率高,要求使用者掌握Java类字节码文件格式及指令,对使用者的要求比较高。 Javassist提供了更高级的API,执行效率相对较差,但无需掌握字节码指令的知识,对使用者要求较低,所以接下来我们重点讲讲Javassist。
Javassist是一个开源的分析、编辑和创建Java字节码的类库。 它是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶滋) 所创建的,目前已经加入到开放源代码JBoss应用服务器项目,JBoss通过使用Javassist对字节码进行操作,实现动态AOP框架。
Javassist(Java Programming Assistant) 使对Java字节码的操作变得简单,它使Java程序能够在运行时定义新类,并且可以在JVM加载时修改类文件。 与其它类似的字节码编辑器不同,它提供两个级别的API:源级别和字节码级别。 如果用户使用源级别API,他们可以在不知道Java字节码规范的情况下编辑类文件。整个API仅使用Java语言的词汇表进行设计,你甚至可以使用Java源代码的方式插入字节码。 另外,用户也可以使用字节码级别的API去直接编辑类文件。
// ClassPool 是 CtClass 对象的容器,存储着CtClass的Hash表。它按需读取类文件来构造CtClass对象,并且保存CtClass对象以便之后使用 ClassPool classPool = ClassPool.getDefault(); // CtClass 表示一个class文件,一个 GtClass(compile-time class)对象用来处理一个class文件,下面是从classpath中查找该类 CtClass ctClass = classPool.get("test.config.ConfigHandle"); // 通知编辑器去寻找对应的包 classPool.importPackage("org.mockito.Mockito"); classPool.importPackage("test.adapter.ext.IDowngrade"); classPool.importPackage("test.utils.property.IProperties"); // 使用removeField() removeMethod() 去删除对应的属性和方法 ctClass.removeField(ctClass.getDeclaredField("serviceHandle")); ctClass.removeField(ctClass.getDeclaredField("switchHandle")); ctClass.removeField(ctClass.getDeclaredField("configHandle")); // CtMethod 和 CtConstructor 提供了 setBody() 方法去修改方法体 CtConstructor ctConstructor = ctClass.getDeclaredConstructors()[0]; ctConstructor.setBody("{this.mySwitch = Mockito.mock(IDowngrade.class);/n" + " this.myConfig = Mockito.mock(IProperties.class);}"); // toClass() 请求当前线程的 ClassLoader 去加载 CtClass 所代表的类文件 ctClass.toClass(); //输出成二进制格式 //byte[] b = ctClass.toBytecode(); //输出class文件到目录中 //ctClass.writeFile("/tmp"); 复制代码
ClassPool是CtClass对象的容器,因为编译器在编译引用CtClass代表的Java类的源代码时,可能会引用CtClass对象,所以一旦一个CtClass被创建,它就被保存在ClassPool中。
如果事先知道要修改哪些类,修改类的最简单方法如下:
如果需要定义一个新类,只需要
ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("HelloWorld"); 复制代码
如果一个 CtClass 对象通过 writeFile(), toClass(), toBytecode()被转换成一个类文件,该CtClass对象会被冻结起来,不允许再修改,因为一个类只能被JVM加载一次。
CtClasss cc = ...; : cc.writeFile(); cc.defrost(); cc.setSuperclass(...); // 类已经被解冻 复制代码
通过 ClassPool.getDefault() 获取的ClassPool默认使用JVM的类搜索路径。如果程序运行在JBoss或者Tomcat等Web服务器上,ClassPool可能无法找到用户自己定义的类,因为这种Web服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool必须添加额外的类搜索路径。
pool.insertClassPath(new ClassClassPath(this.getClass())); // 当前的类使用的类路径,注册到类搜索路径 pool.insertClassPath("/usr/local/javalib"); // 添加目录 /usr/local/javalib 到类搜索路径 ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist."); pool.insertClassPath(cp); // 注册URL到搜索路径 复制代码
在Java中,多个类加载器是可以共存的。每个类加载器创建了自己的命名空间,不同的类加载器可以加载具有相同类名的不同类文件,被加载的类也会被视为不同的类。此功能使我们能够在单个JVM上面运行多个应用程序,即使这些程序包含具有相同名称的类。
注意,JVM不允许动态重新加载类,一旦类加载器加载了一个类,就不能再在运行时重新加载该类的其它版本。因此,在JVM加载类之后,就不能再更改该类的定义。 但是,JPDA(Java平台调试器架构)提供有限的重新加载类的能力,如果相同的类文件由两个不同的类加载器加载,则JVM内会创建两个具有相同名称但是定义的不同的类。由于两个类不相同,所以一个类的实例不能被分配给另一个类的变量,两个类之间的转换操作也会失败并且抛出一个ClassCastException异常。