遲慧智,孔德智
(工業(yè)和信息化部電子第五研究所,廣東 廣州 511370)
近年來(lái),隨著計(jì)算機(jī)與互聯(lián)網(wǎng)技術(shù)的快速發(fā)展,軟件系統(tǒng)被廣泛地應(yīng)用于各行各業(yè)中,為傳統(tǒng)行業(yè)帶來(lái)新的機(jī)遇與發(fā)展。但是,隨著人們生活水平和科學(xué)文化素養(yǎng)的日益提高,用戶對(duì)軟件開(kāi)發(fā)交付的質(zhì)量與速度也有了更高的要求。尤其是在規(guī)模和業(yè)務(wù)需求不斷增加的情況下,大多數(shù)企業(yè)選擇微服務(wù)架構(gòu),使得開(kāi)發(fā)周期與成本增加。因此,在軟件工程中如何保質(zhì)保量地完成開(kāi)發(fā),讓開(kāi)發(fā)者更加專(zhuān)注于業(yè)務(wù)處理方面成為了一個(gè)研究熱點(diǎn)。
根據(jù)JetBrains多方面收集的數(shù)據(jù)顯示[1],全球大約有520萬(wàn)專(zhuān)業(yè)Java開(kāi)發(fā)者,雖然已有25歲高齡,但Java仍是具有活力的開(kāi)發(fā)語(yǔ)言。在Java項(xiàng)目開(kāi)發(fā)中,開(kāi)發(fā)人員應(yīng)該更加專(zhuān)注于業(yè)務(wù)邏輯處理,每個(gè)方法應(yīng)該遵循單一原則,日志事務(wù)等功能應(yīng)作為通用功能附加于主功能之上,因此誕生了方法增強(qiáng)技術(shù)。方法增強(qiáng)是指在不改動(dòng)源代碼的情況下對(duì)方法進(jìn)行包裝以附加額外的功能。Java開(kāi)發(fā)中方法增強(qiáng)常用以下幾種方式:類(lèi)繼承、裝飾者模式和動(dòng)態(tài)代理。類(lèi)繼承是在子類(lèi)方法中調(diào)用父類(lèi)方法,通過(guò)方法覆蓋以實(shí)現(xiàn)父類(lèi)方法的增強(qiáng),但使用時(shí)必須控制對(duì)象創(chuàng)建;裝飾者模式則利用包裝對(duì)象與目標(biāo)對(duì)象實(shí)現(xiàn)相同接口或繼承相同父類(lèi),通過(guò)構(gòu)造器調(diào)用的方法來(lái)實(shí)現(xiàn)方法增強(qiáng),但當(dāng)繼承或?qū)崿F(xiàn)的接口較多時(shí)各個(gè)方法容易互相干擾;動(dòng)態(tài)代理方式則通過(guò)實(shí)現(xiàn)Java中的特定接口實(shí)現(xiàn)方法增強(qiáng)。上述方式均可以實(shí)現(xiàn)方法增強(qiáng),但與項(xiàng)目的耦合度較高,難以實(shí)現(xiàn)高內(nèi)聚低耦合。本文通過(guò)對(duì)面向切面編程(AOP:Aspect Orient Programming)和字節(jié)碼插樁這兩種非侵入式方法增強(qiáng)方式對(duì)比,分析了這些技術(shù)的優(yōu)缺點(diǎn)和使用范圍,并探討了非侵入式方法增強(qiáng)的新進(jìn)展與應(yīng)用。
AOP可以看做面向?qū)ο缶幊蹋∣OP)的一種擴(kuò)展,一種顆粒度更細(xì)的面向?qū)ο蟮乃枷胙由臁OP利用橫切的方法,將封裝對(duì)象的內(nèi)部暴露出來(lái),以方法為單位,將多個(gè)方法的公共行為封裝到一個(gè)模塊,實(shí)現(xiàn)公共方法的重用,如圖1所示。這個(gè)模塊就被稱(chēng)為切面(Aspect),即與業(yè)務(wù)邏輯無(wú)關(guān),但卻是系統(tǒng)邏輯中所需要的代碼。將這些代碼封裝起來(lái)可以減少系統(tǒng)代碼的冗余,實(shí)現(xiàn)各個(gè)模塊的高內(nèi)聚低耦合。AOP是軟件開(kāi)發(fā)中的一個(gè)熱點(diǎn),其中包含眾多的組成元素:連接點(diǎn)(Join point),程序執(zhí)行的一個(gè)點(diǎn),也就是需要切入的方法;切入點(diǎn)(Pointcut),連接點(diǎn)的集合,需要切入點(diǎn)的位置;增強(qiáng)(Advice),需要抽取出來(lái)的具體邏輯,運(yùn)行在切入點(diǎn)前后;切面(Aspect),連接點(diǎn)和增強(qiáng)結(jié)合形成了切面。AOP存在多種增強(qiáng)方式,包括:前置增強(qiáng)、后置增強(qiáng)、異常后增強(qiáng)、前置環(huán)繞增強(qiáng)和后置環(huán)繞增強(qiáng),如圖2所示。通常Java中AOP的實(shí)現(xiàn)方式有靜態(tài)AOP和動(dòng)態(tài)AOP兩種。其中,靜態(tài)AOP是在Java文件編譯期間將增強(qiáng)方法編織進(jìn)被代理對(duì)象的生成class文件中[2];動(dòng)態(tài)AOP[3]則是在內(nèi)存中動(dòng)態(tài)地生成增強(qiáng)后的class文件加載進(jìn)Java虛擬機(jī)中。使用AOP時(shí)切面與增強(qiáng)方法在同一個(gè)工程中,這種強(qiáng)關(guān)聯(lián)性使AOP在生產(chǎn)環(huán)境中的應(yīng)用有一定的限制。
圖1 系統(tǒng)實(shí)現(xiàn)AOP方式
圖2 AOP增強(qiáng)方式
SpringAOP是利用JDK動(dòng)態(tài)代理或CGLIB基于代理模式對(duì)需要增強(qiáng)的方法進(jìn)行代理實(shí)現(xiàn)方法增強(qiáng)。在Spring中,有兩種AOP的使用方式:即基于XML配置的方式和基于注解的方式。基于XML配置方式首先定義增強(qiáng)方法類(lèi):
在Java項(xiàng)目中,Java文件需要編譯為class文件后裝載到Java虛擬機(jī)(JVM)中才可以發(fā)揮作用,JVM通過(guò)讀取class文件并解釋class文件中的各個(gè)字節(jié)碼含義轉(zhuǎn)義為操作系統(tǒng)可以識(shí)別的數(shù)據(jù)[4]。字節(jié)碼插樁則是在運(yùn)行時(shí)動(dòng)態(tài)修改class文件,IDEA的debug功能就是以此技術(shù)為基本原理實(shí)現(xiàn)的。字節(jié)碼插樁一般是由Javaagent+Javaassist/ByteBuddy兩種技術(shù)組合實(shí)現(xiàn)的。Javaagent技術(shù)在運(yùn)行jar包時(shí)指定JVM的-Javaagent參數(shù)與Javaagent工程相關(guān)聯(lián),在主工程的main方法運(yùn)行前調(diào)用Javaagent工程里Premain類(lèi)的premain方法[5]。Javaagent技術(shù)的使用需要定義包含被Javaagent代理類(lèi)的項(xiàng)目工程和Javaagent工程,Javaagent工程內(nèi)部修改MANIFEST.MF文件以指定premain方法的全路徑,將Javaagent工程單獨(dú)打包后再項(xiàng)目工程啟動(dòng)時(shí)利用-Javaagent參數(shù)與項(xiàng)目工程關(guān)聯(lián)[6]。JVM運(yùn)行時(shí)先調(diào)用premain方法,再通過(guò)premain方法中的Instrumentation對(duì)象獲取類(lèi)轉(zhuǎn)換器,將目標(biāo)字節(jié)碼文件替換成增強(qiáng)后的字節(jié)碼文件到類(lèi)加載器中,實(shí)現(xiàn)方法增強(qiáng)。
Javaassist是在Java中編輯字節(jié)碼文件的類(lèi)庫(kù),通過(guò)干預(yù)JVM內(nèi)部類(lèi)加載過(guò)程,Javaassist可以在加載過(guò)程中修改原有的類(lèi)或自定義新類(lèi),包括類(lèi)名、方法名、方法簽名、具體執(zhí)行字節(jié)碼、返回語(yǔ)句和操作數(shù)棧等,class文件中的構(gòu)成部分大多可以使用此框架進(jìn)行操作。但Javaassist自身不能把生成的類(lèi)加載到JVM中,因此通過(guò)Javaagent+Javaassist技術(shù),在實(shí)際運(yùn)行中利用Javaagent執(zhí)行premain方法獲取將要被加載的class文件,利用Javaassist分析轉(zhuǎn)換類(lèi)并返回修改后的class文件,最終利用Javaagent將返回的class文件加載到JVM中實(shí)現(xiàn)類(lèi)的動(dòng)態(tài)功能增強(qiáng)。具體使用時(shí)則主要利用Javaassist中的ClassPool獲取字節(jié)碼文件對(duì)象CtClass,通過(guò)CtClass的getDeclareMethod方法獲取到字節(jié)碼內(nèi)部的方法對(duì)象CtMethod,獲取到方法對(duì)象后可對(duì)指定的方法進(jìn)行改寫(xiě)以達(dá)到增強(qiáng)的目的。
ByteBuddy是一個(gè)代碼生成和操作庫(kù),可在Java應(yīng)用程序運(yùn)行時(shí)不借助編譯器創(chuàng)建和修改Java類(lèi)。除了Java類(lèi)庫(kù)附帶的代碼生成程序外,ByteBuddy還允許創(chuàng)建任意類(lèi),不限于實(shí)現(xiàn)用于創(chuàng)建運(yùn)行時(shí)代理的接口。同時(shí),ByteBuddy提供了一種方便的API,可以使用Java代理或在構(gòu)建過(guò)程中手動(dòng)更改類(lèi)[7]。相較于其他字節(jié)碼操作框架,ByteBuddy的使用過(guò)程無(wú)需理解字節(jié)碼指令,通過(guò)簡(jiǎn)單的API就可以操作字節(jié)碼。ByteBuddy作為一款輕量級(jí)框架,使用時(shí)僅依賴(lài)Java字節(jié)碼解析庫(kù)ASM,同時(shí)支持各個(gè)版本的Java。通過(guò)官方文檔提供的樣例可以看出,ByteBuddy相比其他字節(jié)碼操作框架使用更為簡(jiǎn)潔且可讀性高。
AOP和字節(jié)碼插樁都可以實(shí)現(xiàn)非侵入式方法增強(qiáng),但使用步驟和實(shí)現(xiàn)方式有較大的不同,對(duì)系統(tǒng)資源的消耗也不同,下面將通過(guò)一個(gè)web樣例對(duì)比不同增強(qiáng)方式的性能。
圖4 AOP、JavaAssist、ByteBuddy工程結(jié)構(gòu)
測(cè)試工程利用AOP、Javaassist和ByteBuddy技術(shù),實(shí)現(xiàn)不同方式的方法增強(qiáng),整體項(xiàng)目結(jié)構(gòu)如圖3所示。
圖3 測(cè)試項(xiàng)目結(jié)構(gòu)
其中,test是基于SpringBoot的簡(jiǎn)單測(cè)試工程,test工程中的controller如下:
對(duì)于不同的技術(shù),實(shí)現(xiàn)方法增強(qiáng)的方式也是不同的。AOP主要是利用注解形式,將自定義注解自定義注解的方法;Javaassist則是利用Javaagent的premain方法在main方法前執(zhí)行,premian方法通過(guò)對(duì)ClassFileTransformer接口的實(shí)現(xiàn)類(lèi)MyMonitorTransformer中的方法transform中進(jìn)行字節(jié)碼的獲取及處理;ByteBuddy與Javaassist方法類(lèi)似,利用premain方法在main方法前執(zhí)行,利用ByteBuddy的框架將帶有特定注解@Domonitor添加至test工程中的controller,利用DoJoinPoint攔截和處理帶有(@RuntimeType)的方法用于攔截和處理運(yùn)行時(shí)的字節(jié)碼。使用命令行Java-Javaagent:<增強(qiáng)工程jar包路徑>即可在test工程啟動(dòng)前需關(guān)聯(lián)增強(qiáng)工程與test工程。
項(xiàng)目啟動(dòng)后利用瀏覽器訪問(wèn)controller中的地址,返回結(jié)果如圖5所示。
圖5 AOP工程、JavaAssist工程、ByteBuddy工程的測(cè)試結(jié)果
通過(guò)試驗(yàn)的返回結(jié)果可以看出:
1)利用Javaassist方式的耗時(shí)最長(zhǎng),這是因?yàn)镴avaassist是將ASM框架封裝后向外暴露接口,降低了使用難度,同時(shí)也增加了資源損耗;
2)AOP方式耗時(shí)適中且操作簡(jiǎn)單,但是需在測(cè)試工程上進(jìn)行硬編碼,使用場(chǎng)景會(huì)受限;
3)ByteBuddy的使用時(shí)間最短,測(cè)試結(jié)果與ByteBuddy官方提供的性能測(cè)試的結(jié)果(如圖6所示)相似。
圖6 ByteBuddy官方測(cè)試字節(jié)碼操作的運(yùn)行時(shí)間
本文對(duì)Java開(kāi)發(fā)中的方法增強(qiáng)方式AOP、Javaagent+Javaassist和Javaagent+ByteBuddy技術(shù)進(jìn)行了分析研究,論述了不同技術(shù)的原理及優(yōu)缺點(diǎn)。隨著人們對(duì)軟件應(yīng)用的要求不斷提高,方法增強(qiáng)技術(shù)也將得到長(zhǎng)足的發(fā)展,尤其是字節(jié)碼插樁技術(shù),其與被關(guān)聯(lián)系統(tǒng)完全解耦的優(yōu)點(diǎn),將有更多的應(yīng)用場(chǎng)景。