通過(guò)使用Byte?Buddy便捷創(chuàng)建Java?Agent
Java agent 是在另外一個(gè) Java 應(yīng)用(“目標(biāo)”應(yīng)用)啟動(dòng)之前要執(zhí)行的 Java 程序,這樣 agent 就有機(jī)會(huì)修改目標(biāo)應(yīng)用或者應(yīng)用所運(yùn)行的環(huán)境。在本文中,我們將會(huì)從基礎(chǔ)內(nèi)容開(kāi)始,逐漸增強(qiáng)其功能,借助字節(jié)碼操作工具 Byte Buddy,使其成為高級(jí)的 agent 實(shí)現(xiàn)。
在最基本的用例中,Java agent 會(huì)用來(lái)設(shè)置應(yīng)用屬性或者配置特定的環(huán)境狀態(tài),agent 能夠作為可重用和可插入的組件。如下的樣例描述了這樣的一個(gè) agent,它設(shè)置了一個(gè)系統(tǒng)屬性,在實(shí)際的程序中就可以使用該屬性了:
public class Agent { public static void premain(String arg) { System.setProperty("my-property", “foo”); } }
如上面的代碼所述,Java agent 的定義與其他的 Java 程序類(lèi)似,只不過(guò)它使用premain
方法替代 main 方法作為入口點(diǎn)。顧名思義,這個(gè)方法能夠在目標(biāo)應(yīng)用的 main 方法之前執(zhí)行。相對(duì)于其他的 Java 程序,編寫(xiě) agent 并沒(méi)有特定的規(guī)則。有一個(gè)很小的區(qū)別在于,Java agent 接受一個(gè)可選的參數(shù),而不是包含零個(gè)或更多參數(shù)的數(shù)組。
如果要使用這個(gè) agent,必須要將 agent 類(lèi)和資源打包到 jar 中,并且在 jar 的 manifest 中要將Agent-Class
屬性設(shè)置為包含premain
方法的 agent 類(lèi)。(agent 必須要打包到 jar 文件中,它不能通過(guò)拆解的格式進(jìn)行指定。)接下來(lái),我們需要啟動(dòng)應(yīng)用程序,并且在命令行中通過(guò) javaagent 參數(shù)來(lái)引用 jar 文件的位置:
java -javaagent:myAgent.jar -jar myProgram.jar
我們還可以在位置路徑上設(shè)置可選的 agent 參數(shù)。在下面的命令中會(huì)啟動(dòng)一個(gè) Java 程序并且添加給定的 agent,將值 myOptions 作為參數(shù)提供給premain
方法:
java -javaagent:myAgent.jar=myOptions -jar myProgram.jar
通過(guò)重復(fù)使用javaagent
命令,能夠添加多個(gè) agent。
但是,Java agent 的功能并不局限于修改應(yīng)用程序環(huán)境的狀態(tài),Java agent 能夠訪問(wèn) Java instrumentation API,這樣的話(huà),agent 就能修改目標(biāo)應(yīng)用程序的代碼。Java 虛擬機(jī)中這個(gè)鮮為人知的特性提供了一個(gè)強(qiáng)大的工具,有助于實(shí)現(xiàn)面向切面的編程。
如果要對(duì) Java 程序進(jìn)行這種修改,我們需要在 agent 的premain
方法上添加類(lèi)型為Instrumentation
的第二個(gè)參數(shù)。Instrumentation 參數(shù)可以用來(lái)執(zhí)行一系列的任務(wù),比如確定對(duì)象以字節(jié)為單位的精確大小以及通過(guò)注冊(cè)ClassFileTransformers
實(shí)際修改類(lèi)的實(shí)現(xiàn)。ClassFileTransformers
注冊(cè)之后,當(dāng)類(lèi)加載器(class loader)加載類(lèi)的時(shí)候都會(huì)調(diào)用它。當(dāng)它被調(diào)用時(shí),在類(lèi)文件所代表的類(lèi)加載之前,類(lèi)文件 transformer 有機(jī)會(huì)改變或完全替換這個(gè)類(lèi)文件。按照這種方式,在類(lèi)使用之前,我們能夠增強(qiáng)或修改類(lèi)的行為,如下面的樣例所示:
public class Agent { public static void premain(String argument, Instrumentation inst) { inst.addTransformer(new ClassFileTransformer() { @Override public byte[] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, // 如果類(lèi)之前沒(méi)有加載的話(huà),值為 null ProtectionDomain protectionDomain, byte[] classFileBuffer) { // 返回改變后的類(lèi)文件。 } }); } }
通過(guò)使用Instrumentation
實(shí)例注冊(cè)上述的ClassFileTransformer
之后,每個(gè)類(lèi)加載的時(shí)候,都會(huì)調(diào)用這個(gè) transformer。為了實(shí)現(xiàn)這一點(diǎn),transformer 會(huì)接受一個(gè)二進(jìn)制和類(lèi)加載器的引用,分別代表了類(lèi)文件以及試圖加載類(lèi)的類(lèi)加載器。
Java agent 也可以在 Java 應(yīng)用的運(yùn)行期注冊(cè),如果是在這種場(chǎng)景下,instrumentation API 允許重新定義已加載的類(lèi),這個(gè)特性被稱(chēng)之為“HotSwap”。不過(guò),重新定義類(lèi)僅限于替換方法體。在重新定義類(lèi)的時(shí)候,不能新增或移除類(lèi)成員,并且類(lèi)型和簽名也不能進(jìn)行修改。當(dāng)類(lèi)第一次加載的時(shí)候,并沒(méi)有這種限制,如果是在這樣的場(chǎng)景下,那classBeingRedefined
會(huì)被設(shè)置為 null。
Java 字節(jié)碼與類(lèi)文件格式
類(lèi)文件代表了 Java 類(lèi)編譯之后的狀態(tài)。類(lèi)文件中會(huì)包含字節(jié)碼,這些字節(jié)碼代表了 Java 源碼中最初的程序指令。Java 字節(jié)碼可以視為 Java 虛擬機(jī)的語(yǔ)言。實(shí)際上,JVM 并不會(huì)將 Java 視為編程語(yǔ)言,它只能處理字節(jié)碼。因?yàn)樗捎枚M(jìn)制的表現(xiàn)形式,所以相對(duì)于程序的源碼,它占用的空間更少。除此之外,將程序以字節(jié)碼的形式進(jìn)行表現(xiàn)能夠更容易地編譯 Java 以外的其他語(yǔ)言,如 Scala 或 Clojure,從而讓它們運(yùn)行在 JVM 上。如果沒(méi)有字節(jié)碼作為中間語(yǔ)言的話(huà),那么其他的程序在運(yùn)行之前,可能還需要將其轉(zhuǎn)換為 Java 源碼。
但是,在代碼處理的時(shí)候,這種抽象卻帶來(lái)了一定的成本。如果要將ClassFileTransformer
應(yīng)用到某個(gè)類(lèi)上,那我們不能將該類(lèi)按照 Java 源碼的形式進(jìn)行處理,甚至不能假設(shè)被轉(zhuǎn)換的代碼最初是由 Java 編寫(xiě)而成的。更糟糕的是,探查類(lèi)成員或注解的反射 API 也是禁止使用的,這是因?yàn)轭?lèi)加載之前,我們無(wú)法訪問(wèn)這些 API,而在轉(zhuǎn)換進(jìn)程完成之前,是無(wú)法進(jìn)行加載的。
所幸的是,Java 字節(jié)碼相對(duì)來(lái)講是一個(gè)比較簡(jiǎn)單的抽象形式,它包含了很少量的操作,稍微花點(diǎn)功夫我們就能大致將其掌握起來(lái)。Java 虛擬機(jī)執(zhí)行程序的時(shí)候,會(huì)以基于棧的方式來(lái)處理值。字節(jié)碼指令一般會(huì)告知虛擬機(jī),需要從操作數(shù)棧(operand stack)上彈出值,執(zhí)行一些操作,然后再將結(jié)果壓到棧中。
讓我們考慮一個(gè)簡(jiǎn)單的樣例:將數(shù)字 1 和 2 進(jìn)行相加操作。JVM 首先會(huì)將這兩個(gè)數(shù)字壓到棧中,這是通過(guò) _iconst_1_ 和 _iconst_2_ 這兩個(gè)字節(jié)指令實(shí)現(xiàn)的。_iconst_1_ 是個(gè)單字節(jié)的便捷運(yùn)算符(operator),它會(huì)將數(shù)字 1 壓到棧中。與之類(lèi)似,_iconst_2_ 會(huì)將數(shù)字 2 壓到棧中。然后,會(huì)執(zhí)行 _iadd_ 指令,它會(huì)將棧中最新的兩個(gè)值彈出,將它們求和計(jì)算的結(jié)果重新壓到棧中。在類(lèi)文件中,每個(gè)指令并不是以其易于記憶的名稱(chēng)進(jìn)行存儲(chǔ)的,而是以一個(gè)字節(jié)的形式進(jìn)行存儲(chǔ),這個(gè)字節(jié)能夠唯一地標(biāo)記特定的指令,這也是 _bytecode_ 這個(gè)術(shù)語(yǔ)的來(lái)歷。上文所述的字節(jié)碼指令及其對(duì)操作數(shù)棧的影響,通過(guò)下面的圖片進(jìn)行了可視化。
對(duì)于人類(lèi)用戶(hù)來(lái)講,會(huì)更喜歡源碼而不是字節(jié)碼,不過(guò)幸運(yùn)的是 Java 社區(qū)創(chuàng)建了多個(gè)庫(kù),能夠解析類(lèi)文件并將緊湊的字節(jié)碼暴露為具有名稱(chēng)的指令流。例如,流行的 ASM 庫(kù)提供了一個(gè)簡(jiǎn)單的 visitor API,它能夠?qū)㈩?lèi)文件剖析為成員和方法指令,其操作方式類(lèi)似于閱讀 XML 文件時(shí)的 SAX 解析器。如果使用 ASM 的話(huà),那上述樣例中的字節(jié)碼可以按照如下的代碼來(lái)進(jìn)行實(shí)現(xiàn)(在這里,ASM 方式的指令是visitIns
,能夠提供修正的方法實(shí)現(xiàn)):
MethodVisitor methodVisitor = ... methodVisitor.visitIns(Opcodes.ICONST_1); methodVisitor.visitIns(Opcodes.ICONST_2); methodVisitor.visitIns(Opcodes.IADD);
需要注意的是,字節(jié)碼規(guī)范只不過(guò)是一種比喻的說(shuō)法(metaphor),因?yàn)?Java 虛擬機(jī)允許將程序轉(zhuǎn)換為優(yōu)化后的機(jī)器碼(machine code),只要程序的輸出能夠保證是正確的即可。因?yàn)樽止?jié)碼的簡(jiǎn)潔性,所以在已有的類(lèi)中取代和修改指令是很簡(jiǎn)單直接的。因此,使用 ASM 及其底層的 Java 字節(jié)碼基礎(chǔ)就足以實(shí)現(xiàn)類(lèi)轉(zhuǎn)換的 Java agent,這需要注冊(cè)一個(gè)ClassFileTransformer
,它會(huì)使用這個(gè)庫(kù)來(lái)處理其參數(shù)。
克服字節(jié)碼的不足
對(duì)于實(shí)際的應(yīng)用來(lái)講,解析原始的類(lèi)文件依然意味著有很多的手動(dòng)工作。Java 程序員通常感興趣的是類(lèi)型層級(jí)結(jié)構(gòu)中的類(lèi)。例如,某個(gè) Java agent 可能需要修改所有實(shí)現(xiàn)給定接口的類(lèi)。如果要確定某個(gè)類(lèi)的超類(lèi),那只靠解析ClassFileTransformer
所給定的類(lèi)文件就不夠了,類(lèi)文件中只包含了直接超類(lèi)和接口的名字。為了解析可能的超類(lèi)型關(guān)聯(lián)關(guān)系,程序員依然需要定位這些類(lèi)型的類(lèi)文件。
在項(xiàng)目中直接使用 ASM 的另外一個(gè)困難在于,團(tuán)隊(duì)中需要有開(kāi)發(fā)人員學(xué)習(xí) Java 字節(jié)碼的基礎(chǔ)知識(shí)。在實(shí)踐中,這往往會(huì)導(dǎo)致很多的開(kāi)發(fā)人員不敢再去修改字節(jié)碼操作相關(guān)的代碼。如果這樣的話(huà),實(shí)現(xiàn) Java agent 很容易為項(xiàng)目的長(zhǎng)期維護(hù)帶來(lái)風(fēng)險(xiǎn)。
為了克服這些問(wèn)題,我們最好使用較高層級(jí)的抽象來(lái)實(shí)現(xiàn) Java agent,而不是直接操作 Java 字節(jié)碼。Byte Buddy 是開(kāi)源的、基于 Apache 2.0 許可證的庫(kù),它致力于解決字節(jié)碼操作和 instrumentation API 的復(fù)雜性。Byte Buddy 所聲稱(chēng)的目標(biāo)是將顯式的字節(jié)碼操作隱藏在一個(gè)類(lèi)型安全的領(lǐng)域特定語(yǔ)言背后。通過(guò)使用 Byte Buddy,任何熟悉 Java 編程語(yǔ)言的人都有望非常容易地進(jìn)行字節(jié)碼操作。
Byte Buddy 簡(jiǎn)介
Byte Buddy 的目的并不僅僅是為了生成 Java agent。它提供了一個(gè) API 用于生成任意的 Java 類(lèi),基于這個(gè)生成類(lèi)的 API,Byte Buddy 提供了額外的 API 來(lái)生成 Java agent。
作為 Byte Buddy 的簡(jiǎn)介,如下的樣例展現(xiàn)了如何生成一個(gè)簡(jiǎn)單的類(lèi),這個(gè)類(lèi)是 Object 的子類(lèi),并且重寫(xiě)了 toString 方法,用來(lái)返回“Hello World!”。與原始的 ASM 類(lèi)似,“intercept”會(huì)告訴 Byte Buddy 為攔截到的指令提供方法實(shí)現(xiàn):
Class<?> dynamicType = new ByteBuddy() .subclass(Object.class) .method(ElementMatchers.named("toString")) .intercept(FixedValue.value("Hello World!")) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded();
從上面的代碼中,我們可以看到 Byte Buddy 要實(shí)現(xiàn)一個(gè)方法分為兩步。首先,編程人員需要指定一個(gè)ElementMatcher
,它負(fù)責(zé)識(shí)別一個(gè)或多個(gè)需要實(shí)現(xiàn)的方法。Byte Buddy 提供了功能豐富的預(yù)定義攔截器(interceptor),它們暴露在ElementMatchers
類(lèi)中。在上述的例子中,toString
方法完全精確匹配了名稱(chēng),但是,我們也可以匹配更為復(fù)雜的代碼結(jié)構(gòu),如類(lèi)型或注解。
當(dāng) Byte Buddy 生成類(lèi)的時(shí)候,它會(huì)分析所生成類(lèi)型的類(lèi)層級(jí)結(jié)構(gòu)。在上述的例子中,Byte Buddy 能夠確定所生成的類(lèi)要繼承其超類(lèi) Object 的名為 toString 的方法,指定的匹配器會(huì)要求 Byte Buddy 重寫(xiě)該方法,這是通過(guò)隨后的
<ahref="http://bytebuddy.net/javadoc/0.7.1/net/bytebuddy/implementation/Implementation.html">Implementation</a>
實(shí)例實(shí)現(xiàn)的,在我們的樣例中,這個(gè)實(shí)例也就是
<ahref="http://bytebuddy.net/javadoc/0.7.1/net/bytebuddy/implementation/FixedValue.html">FixedValue</a>
當(dāng)創(chuàng)建子類(lèi)的時(shí)候,Byte Buddy 始終會(huì)攔截(intercept)
一個(gè)匹配的方法,在生成的類(lèi)中重寫(xiě)該方法。但是,我們?cè)诒疚纳院髮?huì)看到 Byte Buddy 還能夠重新定義已有的類(lèi),而不必通過(guò)子類(lèi)的方式來(lái)實(shí)現(xiàn)。在這種情況下,Byte Buddy 會(huì)將已有的代碼替換為生成的代碼,而將原有的代碼復(fù)制到另外一個(gè)合成的(synthetic)方法中。
在我們上面的代碼樣例中,匹配的方法進(jìn)行了重寫(xiě),在實(shí)現(xiàn)里面,返回了固定的值“Hello World!”。intercept
方法接受 Implementation 類(lèi)型的參數(shù),Byte Buddy 自帶了多個(gè)預(yù)先定義的實(shí)現(xiàn),如上文所使用的FixedValue
類(lèi)。但是,如果需要的話(huà),可以使用前文所述的 ASM API 將某個(gè)方法實(shí)現(xiàn)為自定義的字節(jié)碼,Byte Buddy 本身也是基于 ASM API 實(shí)現(xiàn)的。
定義完類(lèi)的屬性之后,就能通過(guò) make 方法來(lái)進(jìn)行生成。在樣例應(yīng)用中,因?yàn)橛脩?hù)沒(méi)有指定類(lèi)名,所以生成的類(lèi)會(huì)給定一個(gè)任意的名稱(chēng)。最終,生成的類(lèi)將會(huì)使用ClassLoadingStrategy
來(lái)進(jìn)行加載。通過(guò)使用上述的默認(rèn)WRAPPER
策略,類(lèi)將會(huì)使用一個(gè)新的類(lèi)加載器進(jìn)行加載,這個(gè)類(lèi)加載器會(huì)使用環(huán)境類(lèi)加載器作為父加載器。
類(lèi)加載之后,使用 Java 反射 API 就可以訪問(wèn)它了。如果沒(méi)有指定其他構(gòu)造器的話(huà),Byte Buddy 將會(huì)生成類(lèi)似于父類(lèi)的構(gòu)造器,因此生成的類(lèi)可以使用默認(rèn)的構(gòu)造器。這樣,我們就可以檢驗(yàn)生成的類(lèi)重寫(xiě)了toString
方法,如下面的代碼所示:
assertThat(dynamicType.newInstance().toString(), is("Hello World!"));
當(dāng)然,這個(gè)生成的類(lèi)并沒(méi)有太大的用處。對(duì)于實(shí)際的應(yīng)用來(lái)講,大多數(shù)方法的返回值是在運(yùn)行時(shí)計(jì)算的,這個(gè)計(jì)算過(guò)程要依賴(lài)于方法的參數(shù)和對(duì)象的狀態(tài)。
通過(guò)委托實(shí)現(xiàn) Instrumentation
要實(shí)現(xiàn)某個(gè)方法,有一種更為靈活的方式,那就是使用 Byte Buddy 的 MethodDelegation。通過(guò)使用方法委托,在生成重寫(xiě)的實(shí)現(xiàn)時(shí),我們就有可能調(diào)用給定類(lèi)和實(shí)例的其他方法。按照這種方式,我們可以使用如下的委托器(delegator)重新編寫(xiě)上述的樣例:
class ToStringInterceptor { static String intercept() { return “Hello World!”; } }
借助上面的 POJO 攔截器,我們就可以將之前的 FixedValue 實(shí)現(xiàn)替換為
MethodDelegation.to(ToStringInterceptor.class):
Class<?> dynamicType = new ByteBuddy() .subclass(Object.class) .method(ElementMatchers.named("toString")) .intercept(MethodDelegation.to(ToStringInterceptor.class)) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded();
使用上述的委托器,Byte Buddy 會(huì)在 to 方法所給定的攔截目標(biāo)中,確定 _ 最優(yōu)的調(diào)用方法 _。就ToStringInterceptor.class
來(lái)講,選擇過(guò)程只是非常簡(jiǎn)單地解析這個(gè)類(lèi)型的唯一靜態(tài)方法而已。在本例中,只會(huì)考慮一個(gè)靜態(tài)方法,因?yàn)槲械哪繕?biāo)中指定的是一個(gè) _ 類(lèi) _。與之不同的是,我們還可以將其委托給某個(gè)類(lèi)的 _ 實(shí)例 _,如果是這樣的話(huà),Byte Buddy 將會(huì)考慮所有的虛方法(virtual method)。如果類(lèi)或?qū)嵗嫌卸鄠€(gè)這樣的方法,那么 Byte Buddy 首先會(huì)排除掉所有與指定 instrumentation 不兼容的方法。在剩余的方法中,庫(kù)將會(huì)選擇最佳的匹配者,通常來(lái)講這會(huì)是參數(shù)最多的方法。我們還可以顯式地指定目標(biāo)方法,這需要縮小合法方法的范圍,將ElementMatcher
傳遞到MethodDelegation
中,就會(huì)進(jìn)行方法的過(guò)濾。例如,通過(guò)添加如下的filter
,Byte Buddy 只會(huì)將名為“intercept”的方法視為委托目標(biāo):
MethodDelegation.to(ToStringInterceptor.class) .filter(ElementMatchers.named(“intercept”))
執(zhí)行上面的攔截之后,被攔截到的方法依然會(huì)打印出“Hello World!”,但是這次的結(jié)果是動(dòng)態(tài)計(jì)算的,這樣的話(huà),我們就可以在攔截器方法上設(shè)置斷點(diǎn),所生成的類(lèi)每次調(diào)用toString
時(shí),都會(huì)觸發(fā)攔截器的方法。
當(dāng)我們?yōu)閿r截器方法設(shè)置參數(shù)時(shí),就能釋放出MethodDelegation
的全部威力。這里的參數(shù)通常是帶有注解的,用來(lái)要求 Byte Buddy 在調(diào)用攔截器方法時(shí),注入某個(gè)特定的值。例如,通過(guò)使用@Origin
注解,Byte Buddy 提供了添加 instrument 功能的方法的實(shí)例,將其作為 Java 反射 API 中類(lèi)的實(shí)例:
class ContextualToStringInterceptor { static String intercept(@Origin Method m) { return “Hello World from ” + m.getName() + “!”; } }
當(dāng)攔截toString
方法時(shí),對(duì) instrument 方法的調(diào)用將會(huì)返回“Hello world from toString!”。
除了@Origin
注解以外,Byte Buddy 提供了一組功能豐富的注解。例如,通過(guò)在類(lèi)型為 Callable
的參數(shù)上使用@Super
注解,Byte Buddy 會(huì)創(chuàng)建并注入一個(gè)代理實(shí)例,它能夠調(diào)用被 instrument 方法的原始代碼。如果對(duì)于特定的用戶(hù)場(chǎng)景,所提供的注解不能滿(mǎn)足需求或者不太適合的話(huà),我們甚至能夠注冊(cè)自定義的注解,讓這些注解注入用戶(hù)特定的值。
實(shí)現(xiàn)方法級(jí)別的安全性
可以看到,我們?cè)谶\(yùn)行時(shí)可以借助簡(jiǎn)單的 Java 代碼,使用 MethodDelegation 來(lái)動(dòng)態(tài)重寫(xiě)某個(gè)方法。這只是一個(gè)簡(jiǎn)單的樣例,但是這項(xiàng)技術(shù)可以用到更加實(shí)際的應(yīng)用之中。在本文剩余的內(nèi)容中,我們將會(huì)開(kāi)發(fā)一個(gè)樣例,它會(huì)使用代碼生成技術(shù)實(shí)現(xiàn)一個(gè)注解驅(qū)動(dòng)的庫(kù),用來(lái)限制方法級(jí)別的安全性。在我們的第一個(gè)迭代中,這個(gè)庫(kù)會(huì)通過(guò)生成子類(lèi)的方式來(lái)限制安全性。然后,我們將會(huì)采取相同的方式來(lái)實(shí)現(xiàn) Java agent,完成相同的功能。
樣例庫(kù)會(huì)使用如下的注解,允許用戶(hù)指定某個(gè)方法需要考慮安全因素:
@interface Secured { String user(); }
例如,假設(shè)應(yīng)用需要使用如下的Service
類(lèi)來(lái)執(zhí)行敏感操作,并且只有用戶(hù)被認(rèn)證為管理員才能執(zhí)行該方法。這是通過(guò)為執(zhí)行這個(gè)操作的方法聲明 Secured 注解來(lái)指定的:
class Service { @Secured(user = “ADMIN”) void doSensitiveAction() { // 運(yùn)行敏感代碼... } }
我們當(dāng)然可以將安全檢查直接編寫(xiě)到方法中。在實(shí)際中,硬編碼橫切關(guān)注點(diǎn)往往會(huì)導(dǎo)致復(fù)制 - 粘貼的邏輯,使其難以維護(hù)。另外,一旦應(yīng)用需要涉及額外的需求時(shí),如日志、收集調(diào)用指標(biāo)或結(jié)果緩存,直接添加這樣的代碼擴(kuò)展性不會(huì)很好。通過(guò)將這樣的功能抽取到 agent 中,方法就能很純粹地關(guān)注其業(yè)務(wù)邏輯,使得代碼庫(kù)能夠更易于閱讀、測(cè)試和維護(hù)。
為了讓我們規(guī)劃的庫(kù)保持盡可能得簡(jiǎn)單,按照注解的協(xié)議聲明,如果當(dāng)前用戶(hù)不具備注解的用戶(hù)屬性時(shí),將會(huì)拋出IllegalStateException
異常。通過(guò)使用 Byte Buddy,這種行為可以用一個(gè)簡(jiǎn)單的攔截器來(lái)實(shí)現(xiàn),如下面樣例中的SecurityInterceptor
所示,它會(huì)通過(guò)其靜態(tài)的 user 域,跟蹤當(dāng)前用戶(hù)已經(jīng)進(jìn)行了登錄:
class SecurityInterceptor { static String user = “ANONYMOUS” static void intercept(@Origin Method method) { if (!method.getAnnotation(Secured.class).user().equals(user)) { throw new IllegalStateException(“Wrong user”); } } }
通過(guò)上面的代碼,我們可以看到,即便給定用戶(hù)授予了訪問(wèn)權(quán)限,攔截器也沒(méi)有調(diào)用原始的方法。為了解決這個(gè)問(wèn)題,Byte Buddy 有很多預(yù)定義的方法可以實(shí)現(xiàn)功能的鏈接。借助MethodDelegation
類(lèi)的andThen
方法,上述的安全檢查可以放到原始方法的調(diào)用之前,如下面的代碼所示。如果用戶(hù)沒(méi)有進(jìn)行認(rèn)證的話(huà),安全檢查將會(huì)拋出異常并阻止后續(xù)的執(zhí)行,因此原始方法將不會(huì)執(zhí)行。
將這些功能集合在一起,我們就能生成Service
的一個(gè)子類(lèi),所有帶有注解方法的都能恰當(dāng)?shù)剡M(jìn)行安全保護(hù)。因?yàn)樗傻念?lèi)是 Service 的子類(lèi),所以它能夠替代所有類(lèi)型為Service
的變量,并不需要任何的類(lèi)型轉(zhuǎn)換,如果沒(méi)有恰當(dāng)認(rèn)證的話(huà),調(diào)用doSensitiveAction
方法就會(huì)拋出異常:
new ByteBuddy() .subclass(Service.class) .method(ElementMatchers.isAnnotatedBy(Secured.class)) .intercept(MethodDelegation.to(SecurityInterceptor.class) .andThen(SuperMethodCall.INSTANCE))) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded() .newInstance() .doSensitiveAction();
不過(guò)壞消息是,因?yàn)閷?shí)現(xiàn) instrumentation
功能的子類(lèi)是在運(yùn)行時(shí)創(chuàng)建的,所以除了使用 Java 反射以外,沒(méi)有其他辦法創(chuàng)建這樣的實(shí)例。因此,所有 instrumentation 類(lèi)的實(shí)例都應(yīng)該通過(guò)一個(gè)工廠來(lái)創(chuàng)建,這個(gè)工廠會(huì)封裝創(chuàng)建 instrumentation 子類(lèi)的復(fù)雜性。這樣造成的結(jié)果就是,子類(lèi) instrumentation 通常會(huì)用于框架之中,這些框架本身就需要通過(guò)工廠來(lái)創(chuàng)建實(shí)例,例如,像依賴(lài)管理的框架 Spring
或?qū)ο?- 關(guān)系映射的框架 Hibernate
,而對(duì)于其他類(lèi)型的應(yīng)用來(lái)講,子類(lèi) instrumentation 實(shí)現(xiàn)起來(lái)通常過(guò)于復(fù)雜。
實(shí)現(xiàn)安全功能的 Java agent
通過(guò)使用 Java agent,上述安全框架的一個(gè)替代實(shí)現(xiàn)將會(huì)修改Service
類(lèi)的原始字節(jié)碼,而不是重寫(xiě)它。這樣做的話(huà),我們就沒(méi)有必要?jiǎng)?chuàng)建托管的實(shí)例了,只需簡(jiǎn)單地調(diào)用
new Service().doSensitiveAction()
即可,如果對(duì)應(yīng)的用戶(hù)沒(méi)有進(jìn)行認(rèn)證的話(huà),就會(huì)拋出異常。為了支持這種方式,Byte Buddy 提供一種稱(chēng)之為 _rebase 某個(gè)類(lèi) _ 的理念。當(dāng) rebase 某個(gè)類(lèi)的時(shí)候,不會(huì)創(chuàng)建子類(lèi),所采用的策略是實(shí)現(xiàn) instrumentation 功能的代碼將會(huì)合并到被 instrument 的類(lèi)中,從而改變其行為。在添加 instrumentation 功能之后,在被 instrument 的類(lèi)中,其所有方法的原始代碼均可進(jìn)行訪問(wèn),因此像SuperMethodCall
這樣的 instrumentation,工作方式與創(chuàng)建子類(lèi)是完全一樣的。
創(chuàng)建子類(lèi)與 rebase 的行為是非常類(lèi)似的,所以?xún)煞N操作的 API 執(zhí)行方式是一致的,都會(huì)使用相同的DynamicType.Builder
接口來(lái)描述某個(gè)類(lèi)型。兩種形式的 instrumentation 都可以通過(guò)ByteBuddy
類(lèi)來(lái)進(jìn)行訪問(wèn)。為了使 Java agent 的定義更加便利,Byte Buddy 還提供了AgentBuilder
類(lèi),它希望能夠以一種簡(jiǎn)潔的方式應(yīng)對(duì)一些通用的用戶(hù)場(chǎng)景。為了定義 Java agent 實(shí)現(xiàn)方法級(jí)別的安全性,將如下的類(lèi)定義為 agent 的入口點(diǎn)就足以完成該功能了:
class SecurityAgent { public static void premain(String arg, Instrumentation inst) { new AgentBuilder.Default() .type(ElementMatchers.any()) .transform((builder, type) -> builder .method(ElementMatchers.isAnnotatedBy(Secured.class) .intercept(MethodDelegation.to(SecurityInterceptor.class) .andThen(SuperMethodCall.INSTANCE)))) .installOn(inst); } }
如果將這個(gè) agent 打包為 jar 文件并在命令行中進(jìn)行指定,那么所有帶有Secured
注解的方法將會(huì)進(jìn)行“轉(zhuǎn)換”或重定義,從而實(shí)現(xiàn)安全保護(hù)。如果不激活這個(gè) Java agent 的話(huà),應(yīng)用在運(yùn)行時(shí)就不包含額外的安全檢查。當(dāng)然,這意味著如果對(duì)帶有注解的代碼進(jìn)行單元測(cè)試的話(huà),這些方法的調(diào)用并不需要特殊的搭建過(guò)程來(lái)模擬安全上下文。Java 運(yùn)行時(shí)會(huì)忽略掉無(wú)法在 classpath 中找到的注解類(lèi)型,因此在運(yùn)行帶有注解的方法時(shí),我們甚至完全可以在應(yīng)用中移除掉安全庫(kù)。
另外一項(xiàng)優(yōu)勢(shì)在于,Java agent 能夠很容易地進(jìn)行疊加。如果在命令行中指定多個(gè) Java agent 的話(huà),每個(gè) agent 都有機(jī)會(huì)對(duì)類(lèi)進(jìn)行修改,其順序就是在命令行中所指定的順序。例如,我們可以采取這種方式將安全、日志以及監(jiān)控框架聯(lián)合在一起,而不需要在這些應(yīng)用間增添任何形式的集成層。因此,使用 Java agent 實(shí)現(xiàn)橫切的關(guān)注點(diǎn)提供了一種更為模塊化的代碼編寫(xiě)方式,而不必針對(duì)某個(gè)管理實(shí)例的中心框架來(lái)集成所有的代碼。
_Byte Buddy 的源碼可以免費(fèi)地在 GitHub 上獲取到。入門(mén)手冊(cè)可以在 http://bytebuddy.net上找到。Byte Buddy 當(dāng)前的可用版本是 0.7.4,所有樣例均是基于該版本的。因?yàn)槠涓镄滦砸约皩?duì) Java 生態(tài)系統(tǒng)的貢獻(xiàn),該庫(kù)曾經(jīng)在 2015 年獲得過(guò) Oracle 的 Duke’s Choice 獎(jiǎng)項(xiàng)。
關(guān)于作者
Rafael Winterhalter是一位軟件咨詢(xún)師,在挪威的奧斯陸工作。他是靜態(tài)類(lèi)型的支持者,對(duì) JVM 有極大的熱情,尤其關(guān)注于代碼 instrumentation、并發(fā)和函數(shù)式編程。Rafael 日常會(huì)撰寫(xiě)關(guān)于軟件開(kāi)發(fā)的博客,經(jīng)常出席相關(guān)的會(huì)議,并被認(rèn)定為 JavaOne Rock Star。在工作以外的編碼過(guò)程中,他為多個(gè)開(kāi)源項(xiàng)目做出過(guò)貢獻(xiàn),經(jīng)常會(huì)花精力在 Byte Buddy 上,這是一個(gè)為 Java 虛擬機(jī)簡(jiǎn)化運(yùn)行時(shí)代碼生成的庫(kù)。因?yàn)樗呢暙I(xiàn),Rafael 得到過(guò) Duke’s Choice 獎(jiǎng)項(xiàng)。
查看英文原文: Easily Create Java Agents with Byte Buddy
以上就是通過(guò)使用Byte Buddy便捷創(chuàng)建Java Agent的詳細(xì)內(nèi)容,更多關(guān)于Byte Buddy創(chuàng)建Java Agent的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
簡(jiǎn)單了解Java刪除字符replaceFirst原理及實(shí)例
這篇文章主要介紹了簡(jiǎn)單了解Java刪除字符replaceFirst原理及實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05Java簡(jiǎn)單數(shù)據(jù)加密方法DES實(shí)現(xiàn)過(guò)程解析
這篇文章主要介紹了Java簡(jiǎn)單數(shù)據(jù)加密方法DES實(shí)現(xiàn)過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12使用Jenkins來(lái)構(gòu)建GIT+Maven項(xiàng)目的方法步驟
這篇文章主要介紹了使用Jenkins來(lái)構(gòu)建GIT+Maven項(xiàng)目,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01在Spring?Boot使用Undertow服務(wù)的方法
Undertow是RedHAT紅帽公司開(kāi)源的產(chǎn)品,采用JAVA開(kāi)發(fā),是一款靈活,高性能的web服務(wù)器,提供了NIO的阻塞/非阻塞API,也是Wildfly的默認(rèn)Web容器,這篇文章給大家介紹了在Spring?Boot使用Undertow服務(wù)的方法,感興趣的朋友跟隨小編一起看看吧2023-05-05Eclipse查看開(kāi)發(fā)包jar里源代碼的方法
這篇文章主要介紹了Eclipse查看開(kāi)發(fā)包jar里源代碼的方法的相關(guān)資料,需要的朋友可以參考下2017-07-07Mybatis實(shí)現(xiàn)數(shù)據(jù)的增刪改查實(shí)例(CRUD)
本篇文章主要介紹了Mybatis實(shí)現(xiàn)數(shù)據(jù)的增刪改查實(shí)例(CRUD),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-05-05