使用AspectJ,Javassist和Java Proxy进行代码注入的实用介绍
這篇文章旨在為您提供您可能(或我寧愿說(shuō)“將要”)需要的知識(shí),并說(shuō)服您學(xué)習(xí)代碼注入的基礎(chǔ)確實(shí)值得您花很少的時(shí)間。 我將介紹三種不同的現(xiàn)實(shí)情況,在這些情況下我需要進(jìn)行代碼注入,并使用不同的工具解決每個(gè)問(wèn)題,最適合手頭的約束。
為什么您需要它
關(guān)于AOP的優(yōu)勢(shì)(因此有代碼注入),已經(jīng)有很多論述 ,因此,從故障排除的角度來(lái)看,我將只專(zhuān)注于一些要點(diǎn)。
最酷的事情是,它使您能夠修改第三方封閉源類(lèi) ,甚至實(shí)際上是JVM類(lèi)。 我們大多數(shù)人使用的是遺留代碼和我們沒(méi)有源代碼的代碼,因此不可避免地我們偶爾會(huì)遇到這些第三方二進(jìn)制文件的局限性或錯(cuò)誤,因此非常需要更改其中的一些小東西或深入了解代碼的行為。 如果沒(méi)有代碼注入,則無(wú)法修改代碼或添加對(duì)代碼增加可觀察性的支持。 同樣,您通常需要在生產(chǎn)環(huán)境中處理問(wèn)題或收集信息,在生產(chǎn)環(huán)境中,您不能使用調(diào)試器和類(lèi)似工具,而您通常至少可以以某種方式管理應(yīng)用程序的二進(jìn)制文件和依賴(lài)項(xiàng)。 請(qǐng)考慮以下情況:
- 您正在將數(shù)據(jù)集合傳遞到閉源庫(kù)進(jìn)行處理,并且?guī)熘械囊粋€(gè)方法對(duì)其中一個(gè)元素失敗,但是異常未提供有關(guān)它是哪個(gè)元素的信息。 您需要對(duì)其進(jìn)行修改以記錄有問(wèn)題的參數(shù)或?qū)⑵浒ㄔ诋惓V小?(并且您不能使用調(diào)試器,因?yàn)樗鼉H在生產(chǎn)應(yīng)用程序服務(wù)器上發(fā)生。)
- 您需要收集應(yīng)用程序中重要方法的性能統(tǒng)計(jì)信息,包括在典型生產(chǎn)負(fù)載下的某些封閉源組件。 (在生產(chǎn)環(huán)境中,您當(dāng)然不能使用探查器,并且您希望產(chǎn)生最小的開(kāi)銷(xiāo)。)
- 您使用JDBC批量發(fā)送大量數(shù)據(jù)到數(shù)據(jù)庫(kù),而其中一個(gè)批量更新失敗。 您將需要一些不錯(cuò)的方法來(lái)找出批次和包含的數(shù)據(jù)。
實(shí)際上,我已經(jīng)遇到了這三種情況(在其他情況下),稍后您將看到可能的實(shí)現(xiàn)。
閱讀本文時(shí),您應(yīng)該牢記代碼注入的以下優(yōu)點(diǎn):
- 代碼注入使您能夠修改您沒(méi)有源代碼的二進(jìn)制類(lèi)
- 注入的代碼可用于在無(wú)法使用傳統(tǒng)開(kāi)發(fā)工具(例如探查器和調(diào)試器)的環(huán)境中收集各種運(yùn)行時(shí)信息。
- 不要重復(fù)自己:當(dāng)您需要在多個(gè)地方使用相同的邏輯時(shí),可以定義一次,然后將其注入所有這些地方。
- 使用代碼注入時(shí),您無(wú)需修改??原始源文件,因此非常適合僅在有限時(shí)間內(nèi)進(jìn)行的(可能是大規(guī)模的)更改,尤其是借助可以輕松打開(kāi)和關(guān)閉代碼注入的工具(例如,具有加載時(shí)編織功能的AspectJ)。 典型的情況是性能指標(biāo)收集和故障排除期間增加的日志記錄
- 您可以在構(gòu)建時(shí)靜態(tài)或靜態(tài)地注入代碼,或者在JVM加載目標(biāo)類(lèi)時(shí)動(dòng)態(tài)注入代碼。
迷你詞匯
您可能會(huì)遇到以下與代碼注入和AOP有關(guān)的術(shù)語(yǔ):
忠告
要注入的代碼。 通常,我們談?wù)摻ㄗh之前,之后和周?chē)?#xff0c;這些建議是在目標(biāo)方法之前,之后或代替目標(biāo)方法執(zhí)行的。 除了將代碼注入方法之外,還可以進(jìn)行其他更改,例如,向類(lèi)添加字段或接口。
AOP(面向方面??的編程)
一個(gè)編程范例聲稱(chēng),“跨領(lǐng)域關(guān)注點(diǎn)”(在許多地方都需要的邏輯,沒(méi)有一個(gè)單獨(dú)的類(lèi)在哪里實(shí)現(xiàn))應(yīng)該實(shí)施一次,然后注入這些地方。 檢查維基百科以獲得更好的描述。
方面
AOP中的模塊化單位大致對(duì)應(yīng)于一個(gè)類(lèi)–它可以包含不同的建議和切入點(diǎn)。
聯(lián)合點(diǎn)
程序中可能成為代碼注入目標(biāo)的特定點(diǎn),例如方法調(diào)用或方法條目。
切入點(diǎn)
粗略地說(shuō),切入點(diǎn)是一個(gè)表達(dá)式,它告訴代碼注入工具在哪里注入特定代碼段,即在哪個(gè)聯(lián)合點(diǎn)上應(yīng)用特定建議。 它只能選擇一個(gè)這樣的點(diǎn)(例如,單個(gè)方法的執(zhí)行),也可以選擇許多類(lèi)似的點(diǎn),例如,所有帶有自定義注釋(例如@MyBusinessMethod)的方法的執(zhí)行。
織造
將代碼(建議)注入目標(biāo)位置(聯(lián)合點(diǎn))的過(guò)程。
工具
有很多非常不同的工具可以完成這項(xiàng)工作,因此我們將首先了解它們之間的差異,然后我們將熟悉代碼注入工具的不同演化分支的三個(gè)杰出代表。
代碼注入工具的基本分類(lèi)
一,抽象水平
表達(dá)要注入的邏輯以及表達(dá)應(yīng)在其中插入邏輯的切入點(diǎn)有多困難?
關(guān)于“建議”代碼:
methodVisitor.visitFieldInsn(Opcodes.GETSTATIC,“ java / lang / System”,“ out”,“ Ljava / io / PrintStream;”);
由于級(jí)別太低,因此難以使用,但功能最強(qiáng)大。 通常,它們用于實(shí)現(xiàn)更高級(jí)別的工具,實(shí)際上很少需要使用它們。
關(guān)于將代碼注入到哪里的規(guī)范:
二。 當(dāng)魔術(shù)發(fā)生時(shí)
可以在不同的時(shí)間點(diǎn)注入代碼:
- 在運(yùn)行時(shí)手動(dòng)–您的代碼必須明確要求增強(qiáng)代碼,例如,通過(guò)手動(dòng)實(shí)例化包裝目標(biāo)對(duì)象的自定義代理(可以說(shuō)這不是真正的代碼注入)
- 在加載時(shí)–在JVM加載目標(biāo)類(lèi)時(shí)執(zhí)行修改
- 在構(gòu)建時(shí)–在打包和部署應(yīng)用程序之前,您需要在構(gòu)建過(guò)程中添加額外的步驟來(lái)修改已編譯的類(lèi)。
這些注射方式中的每一種都可能更適合于不同情況。
三, 它能做什么
代碼注入工具可以做什么或不能做什么都存在很大差異,其中一些可能性是:
- 在方法之前/之后/而不是方法中添加代碼–僅成員級(jí)方法還是靜態(tài)方法?
- 將字段添加到班級(jí)
- 添加新方法
- 制作一個(gè)類(lèi)以實(shí)現(xiàn)接口
- 修改方法體內(nèi)的指令(例如方法調(diào)用)
- 修改泛型,注釋,訪問(wèn)修飾符,更改常量值,…
- 刪除方法,字段等
選定的代碼注入工具
最著名的代碼注入工具是:
Java Proxy,Javassist和AspectJ實(shí)用介紹
我選擇了三種非常不同的成熟和流行的代碼注入工具,并將它們呈現(xiàn)在我親身經(jīng)歷的真實(shí)示例中。
無(wú)所不在的動(dòng)態(tài)Java代理
Java.lang.reflect.Proxy使動(dòng)態(tài)創(chuàng)建接口的代理成為可能,將所有調(diào)用轉(zhuǎn)發(fā)到目標(biāo)對(duì)象。 它不是代碼注入工具,因?yàn)槟荒茉谌魏蔚胤阶⑷胨?#xff0c;必須手動(dòng)實(shí)例化并使用代理而不是原始對(duì)象,并且只能對(duì)接口執(zhí)行此操作,但是如我們所見(jiàn),它仍然非常有用。
優(yōu)點(diǎn):
- 它是JVM的一部分,因此隨處可見(jiàn)
- 您可以對(duì)不兼容的對(duì)象使用相同的代理(更確切地說(shuō)是InvocationHandler) ,從而比平時(shí)更多地重用代碼
- 您可以節(jié)省精力,因?yàn)槟梢暂p松地將所有調(diào)用轉(zhuǎn)發(fā)到目標(biāo)對(duì)象,而僅修改您感興趣的那些調(diào)用。 如果要手動(dòng)實(shí)現(xiàn)代理,則需要實(shí)現(xiàn)相關(guān)接口的所有方法
缺點(diǎn):
- 您只能為接口創(chuàng)建動(dòng)態(tài)代理,如果代碼需要具體的類(lèi),則不能使用它
- 您必須實(shí)例化并手動(dòng)應(yīng)用它,沒(méi)有神奇的自動(dòng)注入功能
- 有點(diǎn)太冗長(zhǎng)
- 它的功能非常有限,它只能在方法之前/之后/周?chē)鷪?zhí)行一些代碼
沒(méi)有代碼注入步驟-您必須手動(dòng)應(yīng)用代理。
例
我正在使用JDBC PreparedStatement的批處理更新來(lái)修改數(shù)據(jù)庫(kù)中的許多數(shù)據(jù),并且由于違反完整性約束而導(dǎo)致其中一個(gè)批處理的處理失敗。 該異常沒(méi)有足夠的信息來(lái)找出導(dǎo)致失敗的數(shù)據(jù),因此我為PreparedStatement創(chuàng)建了一個(gè)動(dòng)態(tài)代理,該代理記住了傳遞給每個(gè)批處理更新的值,并且在失敗的情況下會(huì)自動(dòng)打印該批處理數(shù)字和數(shù)據(jù)。 有了這些信息,我就可以修復(fù)數(shù)據(jù),并保持解決方案就位,這樣,如果再次發(fā)生類(lèi)似的問(wèn)題,我將能夠找到原因并Swift解決。
該代碼的關(guān)鍵部分:
LoggingStatementDecorator.java –片段1
class LoggingStatementDecorator implements InvocationHandler {private PreparedStatement target;...private LoggingStatementDecorator(PreparedStatement target) { this.target = target; }@Overridepublic Object invoke(Object proxy, Method method, Object[] args)throws Throwable {try {Object result = method.invoke(target, args);updateLog(method, args); // remember data, reset upon successful executionreturn result;} catch (InvocationTargetException e) {Throwable cause = e.getTargetException();tryLogFailure(cause);throw cause;}}private void tryLogFailure(Throwable cause) {if (cause instanceof BatchUpdateException) {int failedBatchNr = successfulBatchCounter + 1;Logger.getLogger("JavaProxy").warning("THE INJECTED CODE SAYS: " +"Batch update failed for batch# " + failedBatchNr +" (counting from 1) with values: [" +getValuesAsCsv() + "]. Cause: " + cause.getMessage());}} ...筆記:
要?jiǎng)?chuàng)建代理,您首先需要實(shí)現(xiàn)一個(gè)InvocationHandler及其調(diào)用方法,只要在代理上調(diào)用任何接口的方法,就會(huì)調(diào)用該方法
您可以通過(guò)java.lang.reflect。*對(duì)象訪問(wèn)有關(guān)該調(diào)用的信息,例如,通過(guò)method.invoke將調(diào)用委派給代理對(duì)象
我們還有一個(gè)實(shí)用方法,用于為Prepared語(yǔ)句創(chuàng)建代理實(shí)例:
LoggingStatementDecorator.java –代碼段2
public static PreparedStatement createProxy(PreparedStatement target) {return (PreparedStatement) Proxy.newProxyInstance(PreparedStatement.class.getClassLoader(),new Class[] { PreparedStatement.class },new LoggingStatementDecorator(target)); };筆記:
- 您可以看到newProxyInstance調(diào)用使用一個(gè)類(lèi)加載器,代理應(yīng)實(shí)現(xiàn)的接口數(shù)組以及應(yīng)將調(diào)用委派給其的調(diào)用處理程序(如果需要,處理程序本身必須管理對(duì)代理對(duì)象的引用)
然后按以下方式使用:
Main.java
... PreparedStatement rawPrepStmt = connection.prepareStatement("..."); PreparedStatement loggingPrepStmt = LoggingStatementDecorator.createProxy(rawPrepStmt); ... loggingPrepStmt.executeBatch(); ...筆記:
- 您會(huì)看到我們必須使用代理手動(dòng)包裝原始對(duì)象,并在以后繼續(xù)使用代理
替代解決方案
可以通過(guò)不同的方式解決此問(wèn)題,例如,通過(guò)創(chuàng)建一個(gè)實(shí)現(xiàn)PreparedStatement的非動(dòng)態(tài)代理,并在記住批處理數(shù)據(jù)的同時(shí)將所有調(diào)用轉(zhuǎn)發(fā)到實(shí)際語(yǔ)句,但是對(duì)于具有許多方法的接口,這將是很多無(wú)聊的鍵入。 調(diào)用方還可以手動(dòng)跟蹤已發(fā)送到準(zhǔn)備好的語(yǔ)句的數(shù)據(jù),但這會(huì)因無(wú)關(guān)的關(guān)注而使邏輯模糊。
使用動(dòng)態(tài)Java代理,我們可以得到非常干凈且易于實(shí)現(xiàn)的解決方案。
獨(dú)立的Javassist
JBoss Javassist是一個(gè)中間代碼注入工具,它提供了比字節(jié)碼操作庫(kù)更高級(jí)別的抽象,并且提供了有限的但仍然非常有用的操作功能。 要注入的代碼以字符串表示,您必須手動(dòng)進(jìn)入將其注入的類(lèi)方法。 它的主要優(yōu)點(diǎn)是修改后的代碼對(duì)Javassist或其他任何東西都沒(méi)有新的運(yùn)行時(shí)依賴(lài)項(xiàng)。 如果您在一家大公司工作,這可能是決定性因素,而在大公司中,由于法律和其他原因,很難部署其他開(kāi)放源代碼庫(kù)(或幾乎任何其他庫(kù)),例如AspectJ。
優(yōu)點(diǎn):
- Javassist修改的代碼不需要任何新的運(yùn)行時(shí)依賴(lài)項(xiàng),注入會(huì)在構(gòu)建時(shí)發(fā)生,并且注入的建議代碼本身不依賴(lài)于任何Javassist API
- 盡管比字節(jié)碼操作庫(kù)更高級(jí),但注入的代碼是用Java語(yǔ)法編寫(xiě)的,盡管包含在字符串中
- 可以完成您可能需要的大多數(shù)事情,例如“建議”方法調(diào)用和方法執(zhí)行
- 您可以同時(shí)實(shí)現(xiàn)構(gòu)建時(shí)注入(通過(guò)Java代碼或定制的Ant任務(wù)來(lái)執(zhí)行執(zhí)行/調(diào)用建議 )和加載時(shí)注入(通過(guò)實(shí)現(xiàn)自己的Java 5+代理 [thx to Anton])
缺點(diǎn):
- 仍然有些太底層,因此難于使用–您必須處理一些方法的結(jié)構(gòu),并且注入的代碼未經(jīng)語(yǔ)法檢查
- Javassist沒(méi)有執(zhí)行注入的工具,因此您必須實(shí)現(xiàn)自己的注入代碼-包括不支持根據(jù)模式自動(dòng)注入代碼
(有關(guān)沒(méi)有Javassist的大多數(shù)缺點(diǎn)的解決方案,請(qǐng)參見(jiàn)下面的GluonJ。)
使用Javassist,您可以創(chuàng)建一個(gè)類(lèi),該類(lèi)使用Javassist API注入int目標(biāo)代碼,并在編譯后將其作為構(gòu)建過(guò)程的一部分運(yùn)行,例如,就像我曾經(jīng)通過(guò)自定義Ant任務(wù)所做的那樣。
例
我們需要在Java EE應(yīng)用程序中添加一些簡(jiǎn)單的性能監(jiān)控,并且不允許我們部署任何未經(jīng)批準(zhǔn)的開(kāi)源庫(kù)(至少在沒(méi)有經(jīng)過(guò)耗時(shí)的審批過(guò)程的情況下)。 因此,我們使用Javassist將性能監(jiān)視代碼注入到我們的重要方法中,以及將重要的外部方法調(diào)用到的地方。
代碼注入器:
JavassistInstrumenter.java
public class JavassistInstrumenter {public void insertTimingIntoMethod(String targetClass, String targetMethod) throws NotFoundException, CannotCompileException, IOException {Logger logger = Logger.getLogger("Javassist");final String targetFolder = "./target/javassist";try {final ClassPool pool = ClassPool.getDefault();// Tell Javassist where to look for classes - into our ClassLoaderpool.appendClassPath(new LoaderClassPath(getClass().getClassLoader()));final CtClass compiledClass = pool.get(targetClass);final CtMethod method = compiledClass.getDeclaredMethod(targetMethod);// Add something to the beginning of the method:method.addLocalVariable("startMs", CtClass.longType);method.insertBefore("startMs = System.currentTimeMillis();");// And also to its very end:method.insertAfter("{final long endMs = System.currentTimeMillis();" +"iterate.jz2011.codeinjection.javassist.PerformanceMonitor.logPerformance(\"" +targetMethod + "\",(endMs-startMs));}");compiledClass.writeFile(targetFolder);// Enjoy the new $targetFolder/iterate/jz2011/codeinjection/javassist/TargetClass.classlogger.info(targetClass + "." + targetMethod +" has been modified and saved under " + targetFolder);} catch (NotFoundException e) {logger.warning("Failed to find the target class to modify, " +targetClass + ", verify that it ClassPool has been configured to look " +"into the right location");}}public static void main(String[] args) throws Exception {final String defaultTargetClass = "iterate.jz2011.codeinjection.javassist.TargetClass";final String defaultTargetMethod = "myMethod";final boolean targetProvided = args.length == 2;new JavassistInstrumenter().insertTimingIntoMethod(targetProvided? args[0] : defaultTargetClass, targetProvided? args[1] : defaultTargetMethod);} }筆記:
- 您可以看到“底層” –您必須顯式處理CtClass,CtMethod之類(lèi)的對(duì)象,顯式添加局部變量等。
- Javassist在查找要修改的類(lèi)方面非常靈活-它可以搜索類(lèi)路徑,特定文件夾,JAR文件或包含JAR文件的文件夾
- 您將在編譯過(guò)程中編譯此類(lèi)并運(yùn)行其主要內(nèi)容
類(lèi)固醇的Javassist:GluonJ
GluonJ是一個(gè)基于Javassist的AOP工具。 它可以使用自定義語(yǔ)法或Java 5注釋,并且圍繞“修訂器”的概念構(gòu)建。 Reviser是一個(gè)類(lèi)(一個(gè)方面),它可以修改(即修改)特定的目標(biāo)類(lèi)并覆蓋其一個(gè)或多個(gè)方法(與繼承相反,修訂者的代碼實(shí)際上被強(qiáng)加于目標(biāo)類(lèi)內(nèi)部的原始代碼)。
優(yōu)點(diǎn):
- 如果使用構(gòu)建時(shí)編織,則沒(méi)有運(yùn)行時(shí)依賴(lài)性(加載時(shí)編織需要GluonJ代理庫(kù)或gluonj.jar)
- 使用GlutonJ的注釋的簡(jiǎn)單Java語(yǔ)法-盡管自定義語(yǔ)法也很容易理解和易于使用
- 使用GlutonJ的JAR工具,Ant任務(wù)或在加載時(shí)動(dòng)態(tài)輕松地自動(dòng)織入目標(biāo)類(lèi)
- 支持構(gòu)建時(shí)和加載時(shí)編織
缺點(diǎn):
- 一個(gè)方面只能修改一個(gè)類(lèi),而不能將同一段代碼注入多個(gè)類(lèi)/方法
- 功率有限–僅在執(zhí)行任何代碼時(shí)或僅在特定上下文中執(zhí)行時(shí)(即從特定的類(lèi)/方法中調(diào)用時(shí)),才提供字段/方法的添加和代碼的執(zhí)行,而不是在目標(biāo)方法周?chē)?
如果您不需要將同一段代碼注入多個(gè)方法中,那么GluonJ比Javassist更加容易和更好地選擇,并且如果它的簡(jiǎn)單性對(duì)您來(lái)說(shuō)不是問(wèn)題,那么它也比AspectJ更好的選擇。簡(jiǎn)單。
全能方面
AspectJ是功能完善的AOP工具,它幾乎可以完成您可能想要的任何事情,包括修改靜態(tài)方法,添加新字段,在類(lèi)的已實(shí)現(xiàn)接口列表中添加接口等。
AspectJ建議的語(yǔ)法有兩種,一種是Java語(yǔ)法的超集,具有諸如Aspect和Pointcut的其他關(guān)鍵字,另一種稱(chēng)為@AspectJ –是標(biāo)準(zhǔn)Java 5,具有諸如@ Aspect,@ Pointcut,@ Around的批注。 后者也許更容易學(xué)習(xí)和使用,但功能卻不那么強(qiáng)大,因?yàn)樗幌褡远xAspectJ語(yǔ)法那樣具有表現(xiàn)力。
使用AspectJ,您可以定義要用非常有力的表達(dá)建議的聯(lián)合點(diǎn),但是學(xué)習(xí)它們并使其正確起來(lái)可能并不困難。 對(duì)于AspectJ開(kāi)發(fā),有一個(gè)有用的Eclipse插件– AspectJ開(kāi)發(fā)工具 (AJDT)–但是上次嘗試時(shí),它沒(méi)有我想要的那樣有用。
優(yōu)點(diǎn):
- 功能強(qiáng)大,幾乎可以完成您可能需要的所有操作
- 強(qiáng)大的切入點(diǎn)表達(dá)式,用于定義在何處注入建議以及何時(shí)激活建議(包括一些運(yùn)行時(shí)檢查)–完全啟用DRY,即寫(xiě)入一次并多次注入
- 編譯時(shí)和加載時(shí)代碼注入(編織)
缺點(diǎn):
- 修改后的代碼取決于AspectJ運(yùn)行時(shí)庫(kù)
- 切入點(diǎn)表達(dá)式非常強(qiáng)大,但是可能很難正確使用它們,盡管AJDT插件可以部分可視化它們的效果,但對(duì)“調(diào)試”它們的支持不多
- 盡管基本用法非常簡(jiǎn)單(可能會(huì)花一些時(shí)間才能開(kāi)始使用(使用@ Aspect,@ Around和一個(gè)簡(jiǎn)單的切入點(diǎn)表達(dá)式,如我們?cè)谑纠兴?jiàn)))
例
曾幾何時(shí),我為具有相關(guān)性的封閉式LMS J2EE應(yīng)用程序編寫(xiě)了一個(gè)插件,以致于無(wú)法在本地運(yùn)行它。 在API調(diào)用期間,應(yīng)用程序內(nèi)部的某個(gè)方法失敗了,但該異常未包含足夠的信息來(lái)跟蹤問(wèn)題的原因。 因此,我需要更改方法以在失敗時(shí)記錄其參數(shù)的值。
AspectJ代碼非常簡(jiǎn)單:
LoggingAspect.java
@Aspect public class LoggingAspect {@Around("execution(private void TooQuiet3rdPartyClass.failingMethod(..))")public Object interceptAndLog(ProceedingJoinPoint invocation) throws Throwable {try {return invocation.proceed();} catch (Exception e) {Logger.getLogger("AspectJ").warning("THE INJECTED CODE SAYS: the method " +invocation.getSignature().getName() + " failed for the input '" +invocation.getArgs()[0] + "'. Original exception: " + e);throw e;}} }筆記:
- 方面是帶有@Aspect批注的普通Java類(lèi),它只是AspectJ的標(biāo)記
- @Around注釋指示AspectJ執(zhí)行該方法,而不是與表達(dá)式匹配的方法,即代替TooQuiet3rdPartyClass的failingMethod。
- 周?chē)ㄗh方法需要是公共的,返回一個(gè)對(duì)象,并采用一個(gè)特殊的AspectJ對(duì)象作為參數(shù),該對(duì)象攜帶有關(guān)調(diào)用的信息– ProceedingJoinPoint –并且可以具有任意名稱(chēng)(實(shí)際上,這是簽名的最小形式,它可以更復(fù)雜。)
- 我們使用ProceedingJoinPoint將調(diào)用委派給原始目標(biāo)(TooQuiet3rdPartyClass的實(shí)例),并在發(fā)生異常的情況下獲取參數(shù)的值
- 我使用了@Around建議,盡管@AfterThrowing會(huì)更簡(jiǎn)單,更合適,但這可以更好地顯示AspectJ的功能,并且可以與上述動(dòng)態(tài)Java代理示例進(jìn)行很好的比較
由于我無(wú)法控制應(yīng)用程序的環(huán)境,因此無(wú)法啟用加載時(shí)編織,因此不得不在構(gòu)建時(shí)使用AspectJ的Ant任務(wù)來(lái)編織代碼,重新打包受影響的JAR并將其重新部署到服務(wù)器。
替代解決方案
好吧,如果您不能使用調(diào)試器,那么您的選擇就非常有限。 我唯一想到的替代解決方案是反編譯該類(lèi)(非法!),將日志記錄添加到該方法中(前提是反編譯成功),重新編譯它,然后將原始.class替換為修改后的.class。
黑暗的一面
代碼注入和面向方面的編程非常強(qiáng)大,有時(shí)對(duì)于故障排除和作為應(yīng)用程序體系結(jié)構(gòu)的常規(guī)部分來(lái)說(shuō)都是必不可少的,例如我們可以看到,例如在Java EE的Enterprise Java Beans中,諸如事務(wù)管理和安全性檢查等業(yè)務(wù)問(wèn)題是注入到POJO中(盡管實(shí)現(xiàn)實(shí)際上更可能使用代理)或在Spring中。
但是,由于可能會(huì)降低可理解性,因此需要付出一定的代價(jià),因?yàn)檫\(yùn)行時(shí)行為和結(jié)構(gòu)與您根據(jù)源代碼所期望的不同(除非您知道還要檢查方面的源代碼,或者除非進(jìn)行了注入)通過(guò)對(duì)目標(biāo)類(lèi)(例如Java EE的@Interceptors )的注釋進(jìn)行顯式顯示。 因此,您必須仔細(xì)權(quán)衡代碼注入/ AOP的優(yōu)缺點(diǎn)-盡管合理使用它們不會(huì)比接口,工廠等掩蓋程序流。 關(guān)于掩蓋代碼的爭(zhēng)論可能經(jīng)常被高估了 。
如果您想看一下AOP的例子,請(qǐng)查看Glassbox的源代碼 ,它是JavaEE性能監(jiān)視工具(為此,您可能需要一張地圖 ,以免丟失太多)。
花式使用代碼注入和AOP
在故障排除過(guò)程中,代碼注入的主要應(yīng)用領(lǐng)域是日志記錄,通過(guò)提取并以某種方式傳達(dá)有關(guān)它的有趣運(yùn)行時(shí)信息,可以更準(zhǔn)確地了解應(yīng)用程序正在做什么。 但是,AOP除了簡(jiǎn)單或復(fù)雜的日志記錄以外,還有許多有趣的用途,例如:
- 典型示例:Caching等人(例如: 在JBoss Cache中的AOP上 ),事務(wù)管理,日志記錄,安全性實(shí)施,持久性,線程安全,錯(cuò)誤恢復(fù),方法的自動(dòng)實(shí)現(xiàn)(例如toString,equals,hashCode),遠(yuǎn)程處理
- 基于角色的編程 (例如OT / J ,使用BCEL)或數(shù)據(jù),上下文和交互體系結(jié)構(gòu)的實(shí)現(xiàn)
- 測(cè)試中
- 測(cè)試覆蓋率–注入代碼以記錄測(cè)試運(yùn)行期間是否已執(zhí)行某行
- 突變測(cè)試 ( μJava , Jumble )–向應(yīng)用程序注入“隨機(jī)”突變并驗(yàn)證測(cè)試是否失敗
- 模式測(cè)試 –通過(guò)AOP自動(dòng)驗(yàn)證代碼中正確實(shí)施了架構(gòu)/設(shè)計(jì)/最佳實(shí)踐建議
- 通過(guò)注入異常來(lái)模擬硬件/外部故障
- 幫助實(shí)現(xiàn)Java應(yīng)用程序的零周轉(zhuǎn)– JRebel對(duì)框架和服務(wù)器集成插件使用類(lèi)似于AOP的方法 –即其插件 使用Javassist進(jìn)行“二進(jìn)制修補(bǔ)”
- 解決問(wèn)題并避免使用AOP模式進(jìn)行猴子編碼,例如Worker Object Creation(通過(guò)Runnable和ThreadPool / task隊(duì)列將直接調(diào)用轉(zhuǎn)變?yōu)楫惒秸{(diào)用)和Wormhole(使調(diào)用方的上下文信息對(duì)被調(diào)用方可用,而不必傳遞它們)遍歷所有層作為參數(shù),并且沒(méi)有ThreadLocal)–在《 AspectJ in Action》一書(shū)中進(jìn)行了描述
- 處理遺留代碼–覆蓋對(duì)構(gòu)造函數(shù)的調(diào)用實(shí)例化的類(lèi)(可以使用此類(lèi)和類(lèi)似的類(lèi)來(lái)打破緊密耦合與可行的工作量), 確保向后兼容 o, 教導(dǎo)組件對(duì)環(huán)境變化做出正確反應(yīng)
- 保留API的向后兼容性,同時(shí)不阻止其演化能力,例如,在縮小/擴(kuò)展返回類(lèi)型( Bridge Method Injector –使用ASM)時(shí)添加向后兼容方法,或者通過(guò)重新添加舊方法并按照以下方式實(shí)現(xiàn)它們:新的API
- 將POJO轉(zhuǎn)換為JMX bean
摘要
我們已經(jīng)了解到,代碼注入對(duì)于故障排除是必不可少的,尤其是在處理封閉源代碼庫(kù)和復(fù)雜的部署環(huán)境時(shí)。 我們已經(jīng)看到了三種完全不同的代碼注入工具(動(dòng)態(tài)Java代理,Javassist,AspectJ)應(yīng)用于實(shí)際問(wèn)題,并討論了它們的優(yōu)缺點(diǎn),因?yàn)椴煌墓ぞ呖赡苓m用于不同的情況。 我們還提到了不應(yīng)過(guò)度使用代碼注入/ AOP,并查看了一些代碼注入/ AOP的高級(jí)應(yīng)用示例。
我希望您現(xiàn)在了解代碼注入如何為您提供幫助,并知道如何使用這三個(gè)工具。
源代碼
您可以從GitHub 獲取示例的完整文檔源代碼,不僅包括要注入的代碼,還包括目標(biāo)代碼和易于構(gòu)建的支持。 最簡(jiǎn)單的可能是:
git clone git://github.com/jakubholynet/JavaZone-Code-Injection.git cd JavaZone-Code-Injection/ cat README mvn -P javaproxy test mvn -P javassist test mvn -P aspectj test(Maven可能需要花費(fèi)幾分鐘來(lái)下載其依賴(lài)項(xiàng),插件和實(shí)際項(xiàng)目的依賴(lài)項(xiàng)。)
其他資源
- Spring對(duì)AOP的介紹
- dW: AOP @ Work:AOP的神話與現(xiàn)實(shí)
- AspectJ in Action的第1章 ,第2部分。 ed。
致謝
我要感謝所有為我提供這篇文章和演示文稿的人,包括我的大學(xué),JRebel同學(xué)和GluonJ的合著者。 千葉繁
參考: Holy Java博客上我們JCG合作伙伴 JakubHoly撰寫(xiě)的有關(guān) 使用AspectJ,Javassist和Java Proxy進(jìn)行代碼注入的實(shí)用介紹 。
相關(guān)文章:
- Spring和AspectJ的領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)
- 使用Spring AspectJ和Maven進(jìn)行面向方面的編程
- 使用Spring AOP進(jìn)行面向方面的編程
- 正確記錄應(yīng)用程序的10個(gè)技巧
翻譯自: https://www.javacodegeeks.com/2011/09/practical-introduction-into-code.html
總結(jié)
以上是生活随笔為你收集整理的使用AspectJ,Javassist和Java Proxy进行代码注入的实用介绍的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 芜湖房产备案网上查询(芜湖房产备案网)
- 下一篇: 克隆可序列化和不可序列化的Java对象