林亞明,林葉郁,李佐勇,蘇 草
Spring 框架[1-2]通過依賴注入[3]和面向方面編程[4]技術(shù)給組件管理帶來便利,因此Java項目的技術(shù)選型經(jīng)常采用Spring框架作為項目的核心基礎(chǔ)框架.Java語言的注解機制能夠給類、方法甚至是參數(shù)等[5]添加元信息.由于注解信息與相關(guān)的代碼放在一起,方便維護和理解,注解技術(shù)已經(jīng)成為XML配置方式之后另一種高效的配置手段.某些第三方框架由于各種原因只有注解配置一種方式,然而Spring框架的AOP生成的代理對象不含有原始類的非@inherited注解信息,這時AOP代理將會影響第三方框架的執(zhí)行.例如,ZK框架的表現(xiàn)層支持MVVM設(shè)計模式[6],其配置只能通過注解進行,如果利用Spring的AOP對ViewModel的事件進行日志,將導(dǎo)致運行異常.原因在于ViewModel對象由于使用了代理對象使得ZK引擎無法在運行時刻訪問其注解信息.因此,有必要分析Spring AOP與第三方框架注解配置的功能整合問題.
Spring框架有多種代理對象的配置方式,不失一般性,通過支持Xml schema方式的配置代碼作為說明例子,以下是主要配置信息:
<aop:config>
<aop:pointcut
expression="execution(* org.lym.kancha.vm.
*.*.*(..))"
id="logPoint"/>
<aop:advisor advice-ref="logAdvice"
pointcut-ref="logPoint"/>
</aop:config>
當創(chuàng)建org.lym.kancha.vm包中的Spring組件時,Spring將日志Advice織入到視圖控制器組件中,Spring通過反射機制動態(tài)給該Spring組件包裝代理對象.如果該Spring組件有實現(xiàn)接口,Spring調(diào)用 JDK dynamic proxy API.如果該Spring組件沒有實現(xiàn)接口,Spring調(diào)用CGLIB庫API通過創(chuàng)建目標類的子類來實現(xiàn)代理.不管哪種情況,生成的代理類都不再含有原始類上的非@inherited注解,代理類創(chuàng)建的Spring代理組件也不含有非@inherited注解.由于JDK dynamic proxy API和CGLIB庫API都無法在運行時刻給對象動態(tài)添加注解信息[7],因此經(jīng)過Spring織入方面后的代理對象都不含有非@inherited注解.
Spring MVC框架和Spring Security框架也都使用注解配置,這些框架能夠與Spring AOP和諧共存,原因是Spring系列子框架的注解處理流程做了額外的工作.通過分析 Spring框架源碼,Spring MVC框架、Spring Security框架采用沿著代理對象、父類、實現(xiàn)接口這樣的路線依次查找目標注解,因此Spring各種子框架的注解不受代理對象的影響.
如果第三方框架方便修改源碼,很自然想到的一種解決方法是參考Spring的注解查找方法,修改第三方框架的注解查找源代碼流程.由于Spring框架提供AnnotationUtils類API方法支持這樣的注解查找過程,第三方框架只需調(diào)用Spring提供的API方法就能夠?qū)崿F(xiàn)注解配置的整合,做法如下:
(1)第三方框架首先判斷Spring框架是否在構(gòu)建路徑中.若Spring框架不在構(gòu)建路徑中,就按原來的方式查找.
(2)若Spring框架在構(gòu)建路徑中,就調(diào)用如下的方法實現(xiàn)注解查找.
@Override
public boolean apply(Method input,Class annotation){
return AnnotationUtils.findAnnotation(input,annotation)!=null;
}
其中,annotation表示第三方框架支持的待查找注解,第三方框架通過apply方法判斷代理對象的原始類是否具有指定的注解.然而,由于結(jié)合版權(quán)和修改困難度等原因,許多第三方框架根本不適合自己修改源代碼,第三方框架的開發(fā)團隊由于自身路線圖等原因,不一定對這種特性請求給予及時有效的支持.例如,ZK、Wicket官網(wǎng)上都有用戶碰到注解配置和Spring AOP框架整合的問題,提交的特性更新請求至今處于未解決狀態(tài)[8].
新方法模仿Spring生成代理對象的方法,通過BeanPostProcesser后處理器機制給Spring生成的代理對象再添加一層適配器對象,最外層的適配器對象含有原始類對象的所有注解信息.同時,最外層適配器對象作為內(nèi)層Spring AOP代理對象的代理.其設(shè)計思想如圖1所示.
圖1 兩種二次代理生成方式的類圖
在圖1(a)、(b)中,原始類 BusinessClass含有第三方框架的注解,通過注解配置或者Xml配置產(chǎn)生的Spring AOP代理對象SubClass實現(xiàn)面向方面編程功能,SubClass類不再含有第三方框架的非@inherited注解;而OuterClass是由Bean-PostProcesser動態(tài)產(chǎn)生的適配器類,OuterClass適配器類具有和原始類相同的方法簽名和成員屬性.根據(jù)開閉原則,為了保持引用相容性,設(shè)計OuterClass繼承 SubClass類,同時 OuterClass類組合SubClass類,從而實現(xiàn)SubClass類的代理.采用這樣的結(jié)構(gòu)生成的Bean對象既保留了原有類的所有注解,又具有面向方面編程的功能.
采用上述思想設(shè)計實現(xiàn)了一個通用注解適配器工具類.該類借助Javassist框架[9-10]采用運行時刻動態(tài)生成類的技術(shù)實現(xiàn)注解適配器功能,以BeanPostProcessor方式作用于Spring組件.工具類BeanPostProcessor對所有Spring組件進行搜索,只對滿足條件的組件進行二次代理.條件通過模式字符串參數(shù)regex指定.該模式表達式用來過濾要添加適配器的bean組件.其處理流程如下:
(1)對于每個bean,通過反射獲得bean的類信息字符串,如果類信息字符串不符合參數(shù)regex表示的模式表達式,表明該bean不需要代理,則返回.
(2)根據(jù)bean的類信息,構(gòu)造原始類BusinessClass的類全稱字符串表示,調(diào)用通過Javassist的ClassPool,獲得原始類的CtClass實例.
(3)通過調(diào)用ClassPool.getAndRename方法復(fù)制原始類,產(chǎn)生的新類為CopyCtClass,為了區(qū)分,其類名全稱為原始類包路徑名后面加上“.Javassist”子路徑名.
(4)給CopyCtClass添加target引用成員變量定義,類型為原始類的CtClass.
(5)對于CopyCtClass的每個方法method,判斷method返回類型;如果類型為void,其方法體改為代理目標類的同名方法,即target.method(…);如果類型為非void,其方法體改為return target.method(…).
(6)給CopyCtClass添加一個名為setTarget方法,該方法有一個方法參數(shù),類型為原始類的CtClass.
(7)如果CopyCtClass沒有缺省構(gòu)造方法,添加一個缺省構(gòu)造方法.
(8)調(diào)用CopyCtClass類的toClass方法獲得新代理類,接著再調(diào)用新代理類的newInstance方法創(chuàng)建一個新代理類實例,最后調(diào)用新代理類實例的setTarget方法,將目標Bean作為參數(shù),返回新代理類實例,替換原來的目標Bean.
實際應(yīng)用中,根據(jù)以上處理流程思想實現(xiàn)的工具類在配置 ZK 6.5.1、Spring 3.0.6.Release和Hibernate3.6.8.Final附帶的Javassist框架的環(huán)境下成功整合了ZK MVVM注解和Spring AOP注解.
適配器由于采用動態(tài)生成類的方式實現(xiàn)注解的適配,若目標Bean是非單子模式,這將會導(dǎo)致產(chǎn)生多個目標代理,增加了系統(tǒng)負載.為了減少動態(tài)生成類的負擔,注解適配器工具類實現(xiàn)時同時采用緩存機制.對于某個目標Bean,當其適配器對象創(chuàng)建后,其適配器類以原始類名全稱作為關(guān)鍵字加入到緩存哈希表中.后續(xù)請求就無需再重復(fù)生成適配器類,直接返回緩沖區(qū)中的類對象.為了驗證創(chuàng)建適配器類對運行時間的影響,在 HP ProBook 4411s、JDK 7.0 update 11、Eclipse 4.2軟硬件實驗環(huán)境下,設(shè)計如下仿真實驗:分別對采用CGLIB實現(xiàn)的目標Bean,和采用JDK PROXY實現(xiàn)的目標Bean進行適配代理.為模擬多次訪問,客戶程序?qū)γ總€Bean(非單子模式)請求10次,記錄第一次獲取Bean的時間和后9次的平均值.作為比較,對于沒有采用適配代理的目標Bean,也記錄第一次獲取Bean的時間和后9次的平均時間.實驗結(jié)果如表1所示.
表1 采用不同代理機制的對象請求操作耗費時間/ms
從表1可以看出,第一次請求由于要動態(tài)構(gòu)造新適配器類,適配代理方式耗費時間比無適配代理方式耗費時間大約多2倍(57.8-17.3=40.5).后9次請求創(chuàng)建同樣的對象,這時由于緩沖區(qū)中已經(jīng)存在該對象的適配器類,多出來的時間(7.1-5.3=1.9)主要耗費在beanPostProcessor的處理上.適配代理方式生成對象的平均耗費時間與無適配代理方式耗費時間非常接近,因此適配工具類對整體項目的影響小.
Spring AOP對于注解配置生成的代理對象隱藏了原始類的注解,導(dǎo)致其他框架的注解配置無法與Spring AOP注解配置共存.為了實現(xiàn)兩者的共存,Spring AOP為第三方框架提供了查找隱藏類注解的API函數(shù).但是,當?shù)谌娇蚣軣o法修改源代碼時,Spring AOP的解決方案將失效.為了解決這個問題,本文利用Javassist框架的動態(tài)代碼生成技術(shù),對Spring AOP生成的代理對象進行二次代理.生成的二次代理對象既保持了AOP代理對象的功能,又含有原始類的注解信息,第三方框架無需修改源碼就能夠順利訪問到原始類的注解信息.此外,考慮到動態(tài)代碼生成的耗時性,引入緩存機制以減少其對運行效率的影響.仿真實驗結(jié)果驗證了本文解決方案的可行性.另外,利用動態(tài)代碼生成技術(shù)在程序運行時改變對象組合結(jié)構(gòu)的思路,對其他框架集成問題的解決也具有一定的借鑒意義.
[1]Johnson R,Hoeller J,Arendsen A,et al.Professional Java development with the Spring Framework[M].Wiley.com,2009:7.
[2]Kayal D.Pro Java EE Spring patterns:best practices and design strategies implementing Java EE patterns with the Spring Framework[M].Apress,2008:13-15.
[3]婁鋒,孫涌.輕量級IoC容器的研究與設(shè)計[J].計算機技術(shù)與發(fā)展,2007,17(1):91-97.
[4]Laddad R.Aspectj in action:enterprise AOP with spring applications[M].Manning Publications Co.,2009:57-69.
[5]丁振凡.Spring 3.X的事務(wù)處理機制的研究比較[J].微型機與應(yīng)用,2012,31(10):4-6.
[6]林亞明.基于ZK的 MVVM與 MVP設(shè)計模式應(yīng)用研究[J].重慶文理學(xué)院學(xué)報:自然科學(xué)版,2012,31(6):72-74.
[7]劉榮輝,薛冰.基于 Annotation的 Spring AOP系統(tǒng)設(shè)計[J].計算機應(yīng)用與軟件,2009,26(9):18-20.
[8]ZK team.request for view model factory[EB/OL].(2013-02-28)[2013-11-15].http://tracker.zkoss.org/browse/ZK-1648.
[9]Welch I,Stroud R J.Kava:Using byte code rewriting to add behavioural reflection in Java[C].Coots,2001(1):119-130.
[10]符強.基于Java動態(tài)編程技術(shù)的軟件自愈合構(gòu)架研究[D].西安:西北工業(yè)大學(xué),2007.