這篇文章旨在為您提供您可能(或我寧愿說“將要”)需要的知識,并說服您學習代碼注入的基礎確實值得您花很少的時間。 我將介紹三種不同的現實情況,在這些情況下我需要進行代碼注入,并使用不同的工具解決每個問題,最適合手頭的約束。
為什么您需要它
關于AOP的優勢(因此有代碼注入),已經有很多論述 ,因此,從故障排除的角度來看,我將只專注于一些要點。
最酷的事情是,它使您能夠修改第三方封閉源類 ,甚至實際上是JVM類。 我們大多數人使用的是遺留代碼和我們沒有源代碼的代碼,因此不可避免地我們偶爾會遇到這些第三方二進制文件的局限性或錯誤,因此非常需要更改其中的一些小東西或深入了解代碼的行為。 如果沒有代碼注入,則無法修改代碼或添加對代碼增加可觀察性的支持。 同樣,您通常需要在生產環境中處理問題或收集信息,在生產環境中,您不能使用調試器和類似工具,而您通常至少可以以某種方式管理應用程序的二進制文件和依賴項。 請考慮以下情況:
- 您正在將數據集合傳遞到閉源庫進行處理,并且庫中的一個方法對其中一個元素失敗,但是異常未提供有關它是哪個元素的信息。 您需要對其進行修改以記錄有問題的參數或將其包括在異常中。 (并且您不能使用調試器,因為它僅在生產應用程序服務器上發生。)
- 您需要收集應用程序中重要方法的性能統計信息,包括在典型生產負載下的某些封閉源組件。 (在生產環境中,您當然不能使用探查器,并且您希望產生最小的開銷。)
- 您使用JDBC批量發送大量數據到數據庫,而其中一個批量更新失敗。 您將需要一些不錯的方法來找出批次和包含的數據。
實際上,我已經遇到了這三種情況(在其他情況下),稍后您將看到可能的實現。
閱讀本文時,您應該牢記代碼注入的以下優點:
- 代碼注入使您能夠修改您沒有源代碼的二進制類
- 注入的代碼可用于在無法使用傳統開發工具(例如探查器和調試器)的環境中收集各種運行時信息。
- 不要重復自己:當您需要在多個地方使用相同的邏輯時,可以定義一次,然后將其注入所有這些地方。
- 使用代碼注入時,您無需修改??原始源文件,因此非常適合僅在有限時間內進行的(可能是大規模的)更改,尤其是借助可以輕松打開和關閉代碼注入的工具(例如,具有加載時編織功能的AspectJ)。 典型的情況是性能指標收集和故障排除期間增加的日志記錄
- 您可以在構建時靜態或靜態地注入代碼,或者在JVM加載目標類時動態注入代碼。
迷你詞匯
您可能會遇到以下與代碼注入和AOP有關的術語:
忠告
要注入的代碼。 通常,我們談論建議之前,之后和周圍,這些建議是在目標方法之前,之后或代替目標方法執行的。 除了將代碼注入方法之外,還可以進行其他更改,例如,向類添加字段或接口。
AOP(面向方面??的編程)
一個編程范例聲稱,“跨領域關注點”(在許多地方都需要的邏輯,沒有一個單獨的類在哪里實現)應該實施一次,然后注入這些地方。 檢查維基百科以獲得更好的描述。
方面
AOP中的模塊化單位大致對應于一個類–它可以包含不同的建議和切入點。
聯合點
程序中可能成為代碼注入目標的特定點,例如方法調用或方法條目。
切入點
粗略地說,切入點是一個表達式,它告訴代碼注入工具在哪里注入特定代碼段,即在哪個聯合點上應用特定建議。 它只能選擇一個這樣的點(例如,單個方法的執行),也可以選擇許多類似的點,例如,所有帶有自定義注釋(例如@MyBusinessMethod)的方法的執行。
織造
將代碼(建議)注入目標位置(聯合點)的過程。
工具
有很多非常不同的工具可以完成這項工作,因此我們將首先了解它們之間的差異,然后我們將熟悉代碼注入工具的不同演化分支的三個杰出代表。
代碼注入工具的基本分類
一,抽象水平
表達要注入的邏輯以及表達應在其中插入邏輯的切入點有多困難?
關于“建議”代碼:
- 直接字節碼操作(例如ASM)–要使用這些工具,您需要了解類的字節碼格式,因為它們從類中提取的很少,您可以直接使用操作碼,操作數堆棧和單個指令。 一個ASM示例:
methodVisitor.visitFieldInsn(Opcodes.GETSTATIC,“ java / lang / System”,“ out”,“ Ljava / io / PrintStream;”);
由于級別太低,因此難以使用,但功能最強大。 通常,它們用于實現更高級別的工具,實際上很少需要使用它們。
- 中級–字符串代碼,類文件結構的抽象(Javassist)
- Java建議(例如AspectJ)–要注入的代碼表示為語法檢查和靜態編譯的Java
關于將代碼注入到哪里的規范:
- 手動注入–您必須以某種方式掌握要注入代碼的位置(ASM,Javassist)
- 原始切入點–表達特定位置,特定類,類的所有公共方法或組中所有類的公共方法(Java EE攔截器)的地方,代碼的插入可能性非常有限。
- 模式匹配切入點表達式–強大的表達式,可基于多個條件使用通配符匹配關節點,對上下文的了解(例如“從包XY中的類調用”)等(AspectJ)
二。 當魔術發生時
可以在不同的時間點注入代碼:
- 在運行時手動–您的代碼必須明確要求增強代碼,例如,通過手動實例化包裝目標對象的自定義代理(可以說這不是真正的代碼注入)
- 在加載時–在JVM加載目標類時執行修改
- 在構建時–在打包和部署應用程序之前,您需要在構建過程中添加額外的步驟來修改已編譯的類。
這些注射方式中的每一種都可能更適合于不同情況。
三, 它能做什么
代碼注入工具可以做什么或不能做什么都存在很大差異,其中一些可能性是:
- 在方法之前/之后/而不是方法中添加代碼–僅成員級方法還是靜態方法?
- 將字段添加到班級
- 添加新方法
- 制作一個類以實現接口
- 修改方法體內的指令(例如方法調用)
- 修改泛型,注釋,訪問修飾符,更改常量值,…
- 刪除方法,字段等
選定的代碼注入工具
最著名的代碼注入工具是:
- 動態Java代理
- 字節碼操作庫ASM
- JBoss Javassist
- AspectJ
- Spring AOP /代理
- Java EE攔截器
Java Proxy,Javassist和AspectJ實用介紹
我選擇了三種非常不同的成熟和流行的代碼注入工具,并將它們呈現在我親身經歷的真實示例中。
無所不在的動態Java代理
Java.lang.reflect.Proxy使動態創建接口的代理成為可能,將所有調用轉發到目標對象。 它不是代碼注入工具,因為您不能在任何地方注入它,必須手動實例化并使用代理而不是原始對象,并且只能對接口執行此操作,但是如我們所見,它仍然非常有用。
優點:
- 它是JVM的一部分,因此隨處可見
- 您可以對不兼容的對象使用相同的代理(更確切地說是InvocationHandler) ,從而比平時更多地重用代碼
- 您可以節省精力,因為您可以輕松地將所有調用轉發到目標對象,而僅修改您感興趣的那些調用。 如果要手動實現代理,則需要實現相關接口的所有方法
缺點:
- 您只能為接口創建動態代理,如果代碼需要具體的類,則不能使用它
- 您必須實例化并手動應用它,沒有神奇的自動注入功能
- 有點太冗長
- 它的功能非常有限,它只能在方法之前/之后/周圍執行一些代碼
沒有代碼注入步驟-您必須手動應用代理。
例
我正在使用JDBC PreparedStatement的批處理更新來修改數據庫中的許多數據,并且由于違反完整性約束而導致其中一個批處理的處理失敗。 該異常沒有足夠的信息來找出導致失敗的數據,因此我為PreparedStatement創建了一個動態代理,該代理記住了傳遞給每個批處理更新的值,并且在失敗的情況下會自動打印該批處理數字和數據。 有了這些信息,我就可以修復數據,并保持解決方案就位,這樣,如果再次發生類似的問題,我將能夠找到原因并Swift解決。
該代碼的關鍵部分:
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());}}
...
筆記:
要創建代理,您首先需要實現一個InvocationHandler及其調用方法,只要在代理上調用任何接口的方法,就會調用該方法
您可以通過java.lang.reflect。*對象訪問有關該調用的信息,例如,通過method.invoke將調用委派給代理對象
我們還有一個實用方法,用于為Prepared語句創建代理實例:
LoggingStatementDecorator.java –代碼段2
public static PreparedStatement createProxy(PreparedStatement target) {return (PreparedStatement) Proxy.newProxyInstance(PreparedStatement.class.getClassLoader(),new Class[] { PreparedStatement.class },new LoggingStatementDecorator(target));
};
筆記:
- 您可以看到newProxyInstance調用使用一個類加載器,代理應實現的接口數組以及應將調用委派給其的調用處理程序(如果需要,處理程序本身必須管理對代理對象的引用)
然后按以下方式使用:
Main.java
...
PreparedStatement rawPrepStmt = connection.prepareStatement("...");
PreparedStatement loggingPrepStmt = LoggingStatementDecorator.createProxy(rawPrepStmt);
...
loggingPrepStmt.executeBatch();
...
筆記:
- 您會看到我們必須使用代理手動包裝原始對象,并在以后繼續使用代理
替代解決方案
可以通過不同的方式解決此問題,例如,通過創建一個實現PreparedStatement的非動態代理,并在記住批處理數據的同時將所有調用轉發到實際語句,但是對于具有許多方法的接口,這將是很多無聊的鍵入。 調用方還可以手動跟蹤已發送到準備好的語句的數據,但這會因無關的關注而使邏輯模糊。
使用動態Java代理,我們可以得到非常干凈且易于實現的解決方案。
獨立的Javassist
JBoss Javassist是一個中間代碼注入工具,它提供了比字節碼操作庫更高級別的抽象,并且提供了有限的但仍然非常有用的操作功能。 要注入的代碼以字符串表示,您必須手動進入將其注入的類方法。 它的主要優點是修改后的代碼對Javassist或其他任何東西都沒有新的運行時依賴項。 如果您在一家大公司工作,這可能是決定性因素,而在大公司中,由于法律和其他原因,很難部署其他開放源代碼庫(或幾乎任何其他庫),例如AspectJ。
優點:
- Javassist修改的代碼不需要任何新的運行時依賴項,注入會在構建時發生,并且注入的建議代碼本身不依賴于任何Javassist API
- 盡管比字節碼操作庫更高級,但注入的代碼是用Java語法編寫的,盡管包含在字符串中
- 可以完成您可能需要的大多數事情,例如“建議”方法調用和方法執行
- 您可以同時實現構建時注入(通過Java代碼或定制的Ant任務來執行執行/調用建議 )和加載時注入(通過實現自己的Java 5+代理 [thx to Anton])
缺點:
- 仍然有些太底層,因此難于使用–您必須處理一些方法的結構,并且注入的代碼未經語法檢查
- Javassist沒有執行注入的工具,因此您必須實現自己的注入代碼-包括不支持根據模式自動注入代碼
(有關沒有Javassist的大多數缺點的解決方案,請參見下面的GluonJ。)
使用Javassist,您可以創建一個類,該類使用Javassist API注入int目標代碼,并在編譯后將其作為構建過程的一部分運行,例如,就像我曾經通過自定義Ant任務所做的那樣。
例
我們需要在Java EE應用程序中添加一些簡單的性能監控,并且不允許我們部署任何未經批準的開源庫(至少在沒有經過耗時的審批過程的情況下)。 因此,我們使用Javassist將性能監視代碼注入到我們的重要方法中,以及將重要的外部方法調用到的地方。
代碼注入器:
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之類的對象,顯式添加局部變量等。
- Javassist在查找要修改的類方面非常靈活-它可以搜索類路徑,特定文件夾,JAR文件或包含JAR文件的文件夾
- 您將在編譯過程中編譯此類并運行其主要內容
類固醇的Javassist:GluonJ
GluonJ是一個基于Javassist的AOP工具。 它可以使用自定義語法或Java 5注釋,并且圍繞“修訂器”的概念構建。 Reviser是一個類(一個方面),它可以修改(即修改)特定的目標類并覆蓋其一個或多個方法(與繼承相反,修訂者的代碼實際上被強加于目標類內部的原始代碼)。
優點:
- 如果使用構建時編織,則沒有運行時依賴性(加載時編織需要GluonJ代理庫或gluonj.jar)
- 使用GlutonJ的注釋的簡單Java語法-盡管自定義語法也很容易理解和易于使用
- 使用GlutonJ的JAR工具,Ant任務或在加載時動態輕松地自動織入目標類
- 支持構建時和加載時編織
缺點:
- 一個方面只能修改一個類,而不能將同一段代碼注入多個類/方法
- 功率有限–僅在執行任何代碼時或僅在特定上下文中執行時(即從特定的類/方法中調用時),才提供字段/方法的添加和代碼的執行,而不是在目標方法周圍/
如果您不需要將同一段代碼注入多個方法中,那么GluonJ比Javassist更加容易和更好地選擇,并且如果它的簡單性對您來說不是問題,那么它也比AspectJ更好的選擇。簡單。
全能方面
AspectJ是功能完善的AOP工具,它幾乎可以完成您可能想要的任何事情,包括修改靜態方法,添加新字段,在類的已實現接口列表中添加接口等。
AspectJ建議的語法有兩種,一種是Java語法的超集,具有諸如Aspect和Pointcut的其他關鍵字,另一種稱為@AspectJ –是標準Java 5,具有諸如@ Aspect,@ Pointcut,@ Around的批注。 后者也許更容易學習和使用,但功能卻不那么強大,因為它不像自定義AspectJ語法那樣具有表現力。
使用AspectJ,您可以定義要用非常有力的表達建議的聯合點,但是學習它們并使其正確起來可能并不困難。 對于AspectJ開發,有一個有用的Eclipse插件– AspectJ開發工具 (AJDT)–但是上次嘗試時,它沒有我想要的那樣有用。
優點:
- 功能強大,幾乎可以完成您可能需要的所有操作
- 強大的切入點表達式,用于定義在何處注入建議以及何時激活建議(包括一些運行時檢查)–完全啟用DRY,即寫入一次并多次注入
- 編譯時和加載時代碼注入(編織)
缺點:
- 修改后的代碼取決于AspectJ運行時庫
- 切入點表達式非常強大,但是可能很難正確使用它們,盡管AJDT插件可以部分可視化它們的效果,但對“調試”它們的支持不多
- 盡管基本用法非常簡單(可能會花一些時間才能開始使用(使用@ Aspect,@ Around和一個簡單的切入點表達式,如我們在示例中所見))
例
曾幾何時,我為具有相關性的封閉式LMS J2EE應用程序編寫了一個插件,以致于無法在本地運行它。 在API調用期間,應用程序內部的某個方法失敗了,但該異常未包含足夠的信息來跟蹤問題的原因。 因此,我需要更改方法以在失敗時記錄其參數的值。
AspectJ代碼非常簡單:
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類,它只是AspectJ的標記
- @Around注釋指示AspectJ執行該方法,而不是與表達式匹配的方法,即代替TooQuiet3rdPartyClass的failingMethod。
- 周圍建議方法需要是公共的,返回一個對象,并采用一個特殊的AspectJ對象作為參數,該對象攜帶有關調用的信息– ProceedingJoinPoint –并且可以具有任意名稱(實際上,這是簽名的最小形式,它可以更復雜。)
- 我們使用ProceedingJoinPoint將調用委派給原始目標(TooQuiet3rdPartyClass的實例),并在發生異常的情況下獲取參數的值
- 我使用了@Around建議,盡管@AfterThrowing會更簡單,更合適,但這可以更好地顯示AspectJ的功能,并且可以與上述動態Java代理示例進行很好的比較
由于我無法控制應用程序的環境,因此無法啟用加載時編織,因此不得不在構建時使用AspectJ的Ant任務來編織代碼,重新打包受影響的JAR并將其重新部署到服務器。
替代解決方案
好吧,如果您不能使用調試器,那么您的選擇就非常有限。 我唯一想到的替代解決方案是反編譯該類(非法!),將日志記錄添加到該方法中(前提是反編譯成功),重新編譯它,然后將原始.class替換為修改后的.class。
黑暗的一面
代碼注入和面向方面的編程非常強大,有時對于故障排除和作為應用程序體系結構的常規部分來說都是必不可少的,例如我們可以看到,例如在Java EE的Enterprise Java Beans中,諸如事務管理和安全性檢查等業務問題是注入到POJO中(盡管實現實際上更可能使用代理)或在Spring中。
但是,由于可能會降低可理解性,因此需要付出一定的代價,因為運行時行為和結構與您根據源代碼所期望的不同(除非您知道還要檢查方面的源代碼,或者除非進行了注入)通過對目標類(例如Java EE的@Interceptors )的注釋進行顯式顯示。 因此,您必須仔細權衡代碼注入/ AOP的優缺點-盡管合理使用它們不會比接口,工廠等掩蓋程序流。 關于掩蓋代碼的爭論可能經常被高估了 。
如果您想看一下AOP的例子,請查看Glassbox的源代碼 ,它是JavaEE性能監視工具(為此,您可能需要一張地圖 ,以免丟失太多)。
花式使用代碼注入和AOP
在故障排除過程中,代碼注入的主要應用領域是日志記錄,通過提取并以某種方式傳達有關它的有趣運行時信息,可以更準確地了解應用程序正在做什么。 但是,AOP除了簡單或復雜的日志記錄以外,還有許多有趣的用途,例如:
- 典型示例:Caching等人(例如: 在JBoss Cache中的AOP上 ),事務管理,日志記錄,安全性實施,持久性,線程安全,錯誤恢復,方法的自動實現(例如toString,equals,hashCode),遠程處理
- 基于角色的編程 (例如OT / J ,使用BCEL)或數據,上下文和交互體系結構的實現
- 測試中
- 測試覆蓋率–注入代碼以記錄測試運行期間是否已執行某行
- 突變測試 ( μJava , Jumble )–向應用程序注入“隨機”突變并驗證測試是否失敗
- 模式測試 –通過AOP自動驗證代碼中正確實施了架構/設計/最佳實踐建議
- 通過注入異常來模擬硬件/外部故障
- 幫助實現Java應用程序的零周轉– JRebel對框架和服務器集成插件使用類似于AOP的方法 –即其插件 使用Javassist進行“二進制修補”
- 解決問題并避免使用AOP模式進行猴子編碼,例如Worker Object Creation(通過Runnable和ThreadPool / task隊列將直接調用轉變為異步調用)和Wormhole(使調用方的上下文信息對被調用方可用,而不必傳遞它們)遍歷所有層作為參數,并且沒有ThreadLocal)–在《 AspectJ in Action》一書中進行了描述
- 處理遺留代碼–覆蓋對構造函數的調用實例化的類(可以使用此類和類似的類來打破緊密耦合與可行的工作量), 確保向后兼容 o, 教導組件對環境變化做出正確反應
- 保留API的向后兼容性,同時不阻止其演化能力,例如,在縮小/擴展返回類型( Bridge Method Injector –使用ASM)時添加向后兼容方法,或者通過重新添加舊方法并按照以下方式實現它們:新的API
- 將POJO轉換為JMX bean
摘要
我們已經了解到,代碼注入對于故障排除是必不可少的,尤其是在處理封閉源代碼庫和復雜的部署環境時。 我們已經看到了三種完全不同的代碼注入工具(動態Java代理,Javassist,AspectJ)應用于實際問題,并討論了它們的優缺點,因為不同的工具可能適用于不同的情況。 我們還提到了不應過度使用代碼注入/ AOP,并查看了一些代碼注入/ AOP的高級應用示例。
我希望您現在了解代碼注入如何為您提供幫助,并知道如何使用這三個工具。
源代碼
您可以從GitHub 獲取示例的完整文檔源代碼,不僅包括要注入的代碼,還包括目標代碼和易于構建的支持。 最簡單的可能是:
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可能需要花費幾分鐘來下載其依賴項,插件和實際項目的依賴項。)
其他資源
- Spring對AOP的介紹
- dW: AOP @ Work:AOP的神話與現實
- AspectJ in Action的第1章 ,第2部分。 ed。
致謝
我要感謝所有為我提供這篇文章和演示文稿的人,包括我的大學,JRebel同學和GluonJ的合著者。 千葉繁
參考: Holy Java博客上我們JCG合作伙伴 JakubHoly撰寫的有關 使用AspectJ,Javassist和Java Proxy進行代碼注入的實用介紹 。
相關文章:
- Spring和AspectJ的領域驅動設計
- 使用Spring AspectJ和Maven進行面向方面的編程
- 使用Spring AOP進行面向方面的編程
- 正確記錄應用程序的10個技巧
翻譯自: https://www.javacodegeeks.com/2011/09/practical-introduction-into-code.html