[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