您好,登錄后才能下訂單哦!
概述
什么是動態(tài)編程?動態(tài)編程解決什么問題?Java中如何使用?什么原理?如何改進?(需要我們一起探索,由于自己也是比較菜,一般深入不到這個程度)。
什么是動態(tài)編程
動態(tài)編程是相對于靜態(tài)編程而言的,平時我們討論比較多的就是靜態(tài)編程語言,例如Java,與動態(tài)編程語言,例如JavaScript。那二者有什么明顯的區(qū)別呢?簡單的說就是在靜態(tài)編程中,類型檢查是在編譯時完成的,而動態(tài)編程中類型檢查是在運行時完成的。所謂動態(tài)編程就是繞過編譯過程在運行時進行操作的技術(shù),在Java中有如下幾種方式:
反射
這個搞Java的應(yīng)該比較熟悉,原理也就是通過在運行時獲得類型信息然后做相應(yīng)的操作。
動態(tài)編譯
動態(tài)編譯是從Java 6開始支持的,主要是通過一個JavaCompiler接口來完成的。通過這種方式我們可以直接編譯一個已經(jīng)存在的java文件,也可以在內(nèi)存中動態(tài)生成Java代碼,動態(tài)編譯執(zhí)行。
調(diào)用JavaScript引擎
Java 6加入了對Script(JSR223)的支持。這是一個腳本框架,提供了讓腳本語言來訪問Java內(nèi)部的方法。你可以在運行的時候找到腳本引擎,然后調(diào)用這個引擎去執(zhí)行腳本。這個腳本API允許你為腳本語言提供Java支持。
動態(tài)生成字節(jié)碼
這種技術(shù)通過操作Java字節(jié)碼的方式在JVM中生成新類或者對已經(jīng)加載的類動態(tài)添加元素。
動態(tài)編程解決什么問題
在靜態(tài)語言中引入動態(tài)特性,主要是為了解決一些使用場景的痛點。其實完全使用靜態(tài)編程也辦的到,只是付出的代價比較高,沒有動態(tài)編程來的優(yōu)雅。例如依賴注入框架Spring使用了反射,而Dagger2 卻使用了代碼生成的方式(APT)。
例如
1: 在那些依賴關(guān)系需要動態(tài)確認(rèn)的場景:
2: 需要在運行時動態(tài)插入代碼的場景,比如動態(tài)代理的實現(xiàn)。
3: 通過配置文件來實現(xiàn)相關(guān)功能的場景
Java中如何使用
此處我們主要說一下通過動態(tài)生成字節(jié)碼的方式,其他方式可以自行查找資料。
操作java字節(jié)碼的工具有兩個比較流行,一個是ASM,一個是Javassit 。
ASM :直接操作字節(jié)碼指令,執(zhí)行效率高,要是使用者掌握J(rèn)ava類字節(jié)碼文件格式及指令,對使用者的要求比較高。
Javassit 提供了更高級的API,執(zhí)行效率相對較差,但無需掌握字節(jié)碼指令的知識,對使用者要求較低。
應(yīng)用層面來講一般使用建議優(yōu)先選擇Javassit,如果后續(xù)發(fā)現(xiàn)Javassit 成為了整個應(yīng)用的效率瓶頸的話可以再考慮ASM.當(dāng)然如果開發(fā)的是一個基礎(chǔ)類庫,或者基礎(chǔ)平臺,還是直接使用ASM吧,相信從事這方面工作的開發(fā)者能力應(yīng)該比較高。
上一張國外博客的圖,展示處理Java字節(jié)碼的工具的關(guān)系。
接下來介紹如何使用Javassit來操作字節(jié)碼
Javassit使用方法
Javassist是一個開源的分析、編輯和創(chuàng)建Java字節(jié)碼的類庫。是由東京工業(yè)大學(xué)的數(shù)學(xué)和計算機科學(xué)系的 Shigeru Chiba (千葉 滋)所創(chuàng)建的。
它已加入了開放源代碼JBoss 應(yīng)用服務(wù)器項目,通過使用Javassist對字節(jié)碼操作為JBoss實現(xiàn)動態(tài)AOP框架。
javassist是jboss的一個子項目,其主要的優(yōu)點,在于簡單,而且快速。直接使用java編碼的形式,而不需要了解虛擬機指令,就能動態(tài)改變類的結(jié)構(gòu),或者動態(tài)生成類。
Javassist中最為重要的是ClassPool,CtClass ,CtMethod 以及 CtField這幾個類。
ClassPool:一個基于HashMap實現(xiàn)的CtClass對象容器,其中鍵是類名稱,值是表示該類的CtClass對象。默認(rèn)的ClassPool使用與底層JVM相同的類路徑,因此在某些情況下,可能需要向ClassPool添加類路徑或類字節(jié)。
CtClass:表示一個類,這些CtClass對象可以從ClassPool獲得。
CtMethods:表示類中的方法。
CtFields :表示類中的字段。
動態(tài)生成一個類
下面的代碼會生成一個實現(xiàn)了Cloneable接口的類GenerateClass
public void DynGenerateClass() { ClassPool pool = ClassPool.getDefault(); CtClass ct = pool.makeClass("top.ss007.GenerateClass");//創(chuàng)建類 ct.setInterfaces(new CtClass[]{pool.makeInterface("java.lang.Cloneable")});//讓類實現(xiàn)Cloneable接口 try { CtField f= new CtField(CtClass.intType,"id",ct);//獲得一個類型為int,名稱為id的字段 f.setModifiers(AccessFlag.PUBLIC);//將字段設(shè)置為public ct.addField(f);//將字段設(shè)置到類上 //添加構(gòu)造函數(shù) 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(); } }
上面的代碼就會動態(tài)生成一個.class文件,我們使用反編譯工具,例如Bytecode Viewer,查看生成的字節(jié)碼文件GenerateClass.class,如下圖所示。
動態(tài)添加構(gòu)造函數(shù)及方法
有很多種方法添加構(gòu)造函數(shù),我們使用CtNewConstructor.make,他是一個的靜態(tài)方法,其中有一個重載版本比較方便,如下所示。第一個參數(shù)是source text 類型的方法體,第二個為類對象。
CtConstructor constructor=CtNewConstructor.make("public GeneratedClass(int pId){this.id=pId;}",ct); ct.addConstructor(constructor);
這段代碼執(zhí)行后會生成如下java代碼,代碼片段是使用反編譯工具JD-GUI產(chǎn)生的,可以看到構(gòu)造函數(shù)的參數(shù)名被修改成了paramInt。
public GeneratedClass(int paramInt) { this.id = paramInt; }
同樣有很多種方法添加函數(shù),我們使用CtNewMethod.make這個比較簡單的形式
CtMethod helloM=CtNewMethod.make("public void hello(String des){ System.out.println(des);}",ct); ct.addMethod(helloM);
這段代碼執(zhí)行后會生成如下java代碼:
public void hello(String paramString) { System.out.println(paramString); }
動態(tài)修改方法體
動態(tài)的修改一個方法的內(nèi)容才是我們關(guān)注的重點,例如在AOP編程方面,我們就會用到這種技術(shù),動態(tài)的在一個方法中插入代碼。
例如我們有下面這樣一個類
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; } }
我們要動態(tài)的在內(nèi)存中在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(); //通過反射調(diào)用方法,查看結(jié)果 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方法結(jié)果:
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); }
可以看到,在生成的字節(jié)碼文件中確實增加了相應(yīng)的代碼。
函數(shù)輸出結(jié)果為:
dx:1dy:2 2 4
Javassit 還有許多功能,例如在方法中調(diào)用方法,異常捕捉,類型強制轉(zhuǎn)換,注解相關(guān)操作等,而且其還提供了字節(jié)碼層面的API(Bytecode level API)。
什么原理
反射:由于Java執(zhí)行過程中是將類型載入虛擬機中的,在運行時我們就可以動態(tài)獲取到所有類型的信息。只能獲取卻不能修類型信息。
動態(tài)編譯與動態(tài)生成字節(jié)碼:這兩種方法比較相似,原理也都是利用了Java的設(shè)計原理,存在一個虛擬機執(zhí)行字節(jié)碼,這就使我們在此處有了改變字節(jié)碼的操作空間。
總結(jié)
有關(guān)動態(tài)編程的知識在平時的應(yīng)用層使用不是特別多,多是用在構(gòu)建框架。例如Spring框架使用反射來構(gòu)建,而用于AOP編程的動態(tài)代理則多是采用生成字節(jié)碼的方式,例如JBoss,Spring中的AOP部分。了解這部分知識可以在日后遇到相關(guān)問題時比別人多一條思考的思路也是好的,做一個思路開闊的Developer。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。