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