[toc]java
什么是动态编程?动态编程解决什么问题?Java中如何使用?什么原理?如何改进?(须要咱们一块儿探索,因为本身也是比较菜,通常深刻不到这个程度)。编程
动态编程是相对于静态编程而言的,平时咱们讨论比较多的就是静态编程语言,例如Java,与动态编程语言,例如JavaScript。那两者有什么明显的区别呢?简单的说就是在静态编程中,类型检查是在编译时完成的,而动态编程中类型检查是在运行时完成的。所谓动态编程就是绕过编译过程在运行时进行操做的技术,在Java中有以下几种方式:服务器
这个搞Java的应该比较熟悉,原理也就是经过在运行时得到类型信息而后作相应的操做。框架
动态编译是从Java 6
开始支持的,主要是经过一个JavaCompiler
接口来完成的。经过这种方式咱们能够直接编译一个已经存在的java文件,也能够在内存中动态生成Java代码,动态编译执行。编程语言
Java 6
加入了对Script(JSR223)
的支持。这是一个脚本框架,提供了让脚本语言来访问Java内部的方法。你能够在运行的时候找到脚本引擎,而后调用这个引擎去执行脚本。这个脚本API容许你为脚本语言提供Java支持。函数
这种技术经过操做Java字节码的方式在JVM
中生成新类或者对已经加载的类动态添加元素。工具
在静态语言中引入动态特性,主要是为了解决一些使用场景的痛点。其实彻底使用静态编程也办的到,只是付出的代价比较高,没有动态编程来的优雅。例如依赖注入框架Spring使用了反射,而Dagger2 却使用了代码生成的方式(APT)。this
例如 1: 在那些依赖关系须要动态确认的场景: 2: 须要在运行时动态插入代码的场景,好比动态代理的实现。 3: 经过配置文件来实现相关功能的场景编码
此处咱们主要说一下经过动态生成字节码的方式,其余方式能够自行查找资料。.net
操做java字节码的工具备两个比较流行,一个是ASM,一个是Javassit 。
ASM :直接操做字节码指令,执行效率高,要是使用者掌握Java类字节码文件格式及指令,对使用者的要求比较高。
Javassit 提供了更高级的API,执行效率相对较差,但无需掌握字节码指令的知识,对使用者要求较低。
应用层面来说通常使用建议优先选择Javassit
,若是后续发现Javassit
成为了整个应用的效率瓶颈的话能够再考虑ASM
.固然若是开发的是一个基础类库,或者基础平台,仍是直接使用ASM
吧,相信从事这方面工做的开发者能力应该比较高。
上一张国外博客的图,展现处理Java字节码的工具的关系。 接下来介绍如何使用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
)。
本文转载于http://www.javashuo.com/article/p-vhlzqlyu-nm.html
若是想对类进行修改,最好是在该类在被类加载器以前。否则在执行 CtClass.toCalss()
或者 CtClass.toBytese
,会出现duplicate class definition
的异常。固然也可使用 Javassit 提供的类加载器 Loader
,解决被同一个类加载器加载冲突的问题。可是须要注意的是,不一样类加载器加载的同一个类,不是相同的类。
关于这一点能够参考 https://blog.csdn.net/qq_26222859/article/details/52600260