不用Jar包的Agent? 幾行代碼實(shí)現(xiàn)運(yùn)行時(shí)增強(qiáng)?
提起 JavaAgent,很多人都說幾句,就像古龍武俠小說里的「孔雀翎」,威力很大,江湖上都是它的傳說。但真的見識(shí)過的人并沒幾個(gè)。
JavaAgent 雖說沒這么神秘,但也一直給人曲高和寡的感覺,除了一些中間件產(chǎn)品、大型的框架中使用外,在業(yè)務(wù)中一直很少出現(xiàn)。
原因可能有很多,一來是可能確實(shí)不需要,再者需要開發(fā)獨(dú)立的 Agent Jar 文件,在 Jar 內(nèi)對類的 transform 開發(fā)也并不容易。
我們知道,無論是啟動(dòng)時(shí)的 Java Agent,還是運(yùn)行時(shí)的動(dòng)態(tài) attach 到遠(yuǎn)程JVM, 都是為了拿到 Instrument,對 class 的字節(jié)碼進(jìn)行修改。這么底層的東西,當(dāng)然使用起來讓人不太容易下手。
不過就像機(jī)器語言不方便,人們發(fā)明了匯編語言,又發(fā)現(xiàn)了高級語言。對于字節(jié)碼的操作也類似,人們覺得直接操作字節(jié)碼有難度,而且需要深入理解 JVM 規(guī)范,具體什么位置多少字節(jié)代表啥,這不是一般人喜歡的,于是 ASM 框架出現(xiàn)了;但還是有規(guī)范的影子,不太「高級」,于是又出現(xiàn)了Javassist 這一類的「高級」工具。
我們今天要說的這個(gè)工具,和 Javassist 類似,都提供了更高層的API,來操作class,對普通程序員更友好,除此之外呢?
就像今天人們購物、讀書等,都更相信專業(yè)的平臺(tái)、或者專家的推薦,像XX嚴(yán)選,XX讀書會(huì)推薦。今天說的這個(gè)工具是Duke 的推薦,對,就是它, Java 的吉祥物,這個(gè)小胖子。今天的這個(gè)工具在 2015年被 Oracle 評選為「Duke's Choice award」。
除了Duke,框架也得到了眾多開發(fā)者的認(rèn)可,每年有七千多萬次的下載。
這個(gè)工具是:Bytebuddy。
從名字你就看的出來,它立志要做字節(jié)碼的好伙伴。所以在很多開源框架里也能看到它的身影。
既然已經(jīng)有了不少的工具, byteBuddy能帶來什么不一樣呢?
除了API 上的簡潔易操作,官方自己也大字強(qiáng)調(diào)了運(yùn)行時(shí)動(dòng)態(tài)的「代碼生成和字節(jié)碼操作」,不需要再借助 Java 編譯器。
來看看官網(wǎng)是怎么自我介紹的,后面再附上幾個(gè)代碼片段,就能很快 Get 到了。
官網(wǎng):https://bytebuddy.net/
Byte Buddy is a code generation and manipulation library for creating and modifying Java classes during the runtime of a Java application and without the help of a compiler. Other than the code generation utilities that ship with the Java Class Library, Byte Buddy allows the creation of arbitrary classes and is not limited to implementing interfaces for the creation of runtime proxies. Furthermore, Byte Buddy offers a convenient API for changing classes either manually, using a Java agent or during a build.
閱讀理解開始。重點(diǎn)你一定會(huì)看到了:
- 「code generation」
- 「creating and modifying Java classes」
作者貼心的加了一段小字來描述框架的優(yōu)勢。選重點(diǎn)的說就是:
- 不需要理解字節(jié)碼,也不需要理解class 文件格式
- API 非侵入,設(shè)計(jì)簡潔易懂
- 高度可定制,可以任意自定義
我自己認(rèn)為該把這點(diǎn)也加上,不寫 Java Agent 也可以 Attach 到 JVM 上,把 ByteBuddy 自己當(dāng)成一個(gè)Agent,運(yùn)行時(shí)直接install。Cool。
不寫JVM Agent 也能對類攔截和修改,我們來認(rèn)識(shí)下揭開字節(jié)碼修改黑魔法的家伙。
為了對 Class 進(jìn)行一些操作,我們一般都離不了 JVM Agent。不管是啟動(dòng)時(shí)直接連接,還是運(yùn)行時(shí)動(dòng)態(tài)的 Attach到對應(yīng)的JVM 上,都需要 Agent。也就是我們熟悉的premain 和 agentmain 的觸發(fā)入口,通過它們,我們才能拿到 Instrumentation,從而進(jìn)行 transform和 redeine。
但這個(gè)東西的使用,給人總是「陽春白雪」的感覺,讓人覺得是黑魔法一樣,一般不會(huì)輕易嘗試使用。
有了ByteBuddy,就不用再羨慕一些框架的「運(yùn)行時(shí)增強(qiáng)」,「動(dòng)態(tài)織入」等等,都可以實(shí)現(xiàn)。
如何上手呢?
只需要下載 Jar 文件或者 Maven 添加依賴之后就能狂奔了。
比如官方的這個(gè) HelloWorld
- Class<?> dynamicType = new ByteBuddy()
- .subclass(Object.class)
- .method(ElementMatchers.named("toString"))
- .intercept(FixedValue.value("Hello World!"))
- .make()
- .load(getClass().getClassLoader())
- .getLoaded();
- assertThat(dynamicType.newInstance().toString(), is("Hello World!"));
直接把 Object 的 toString 方法改寫了。
再比如我們可以在開發(fā) Java Agent的時(shí)候使用這個(gè)伙計(jì)
- public static void premain(String args, Instrumentation inst) {
- AgentBuilder agentBuilder = new AgentBuilder.Default();
- AgentBuilder.Transformer transformer = new AgentBuilder.Transformer() {
- public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
- TypeDescription typeDescription,
- ClassLoader classLoader, JavaModule javaModule) {
- String className = typeDescription.getCanonicalName();
- builder = builder.method(ElementMatchers.any())//匹配任意方法
- .intercept(MethodDelegation.to(new SimplePackageInstanceMethodInterceptor()));
- return builder;
- }
- };
- agentBuilder = agentBuilder.type(ElementMatchers.nameStartsWith("com.example.hello.sample")).transform(transformer);
- agentBuilder.installOn(inst);
- }
在類里進(jìn)行攔截匹配的時(shí)候,可以通過類名來限定,同時(shí)以不同的模式去匹配方法名等,這里的ElementMatchers可以用在類名與方法名等匹配場景中
- //ElementMatchers.named("abc") // 特定名稱的方法
- //ElementMatchers.nameStartsWith("hello") // 以什么開頭的
- //ElementMatchers.nameEndsWith("test") // 以什么結(jié)尾的
我們看到前面的代碼中 agentBuilder.installOn(inst);
通過 JavaAgent的Instrument 進(jìn)行類修改。
AgentBuilder 還提供了一個(gè)神奇的方法:
- agentBuilder.installOnByteBuddyAgent();
這樣無須提供 Jar 文件也一樣能實(shí)現(xiàn)運(yùn)行時(shí)增強(qiáng)。不過需要注意,這樣使用時(shí),一定要先執(zhí)行這行代碼,這也是其實(shí)現(xiàn)的秘密:
- ByteBuddyAgent.install();
因?yàn)?ByteBuddy 自己做為一個(gè) Jar 也 Attach ,然后再將其它后續(xù)的增強(qiáng)代碼加入,像不像「特洛伊木馬」 :)
另外, ByteBuddy 還支持類似于 AOP 的 Advice實(shí)現(xiàn),在攔截指定方法后可以實(shí)現(xiàn)OnMethodEnter 和 OnMethodExit 的控制,在這其中,可以完成繞過用戶代碼,執(zhí)行自定義內(nèi)容的邏輯。
咱們在開始的時(shí)候,還提到了代碼的生成。這在 ByteBuddy 看來也是易如反掌。
和上面的代碼一樣,先要拿到 AgentBuilder,之后在執(zhí)行 tranform的時(shí)候,直接指定方法名,以及對應(yīng)的參數(shù),訪問控制符等。
- DynamicType.Builder.MethodDefinition.ExceptionDefinition<?> hello =
- builder.defineMethod(methodName, types, Visibility.PUBLIC)
- .withParameters(m.getParameters().asTypeList());
再比如在運(yùn)行時(shí)給一個(gè)方法增加注解,
- builder.method(ElementMatchers.named("methodName")).intercept(SuperMethodCall.INSTANCE)
- .annotateMethod(AnnotationDescription.Builder.ofType(TestAnnotation.class)
- .define("testValue", 123).build());
是不是功能很強(qiáng)大?
更多的用法,可以參考官方的Github或者官網(wǎng)。
本文轉(zhuǎn)載自微信公眾號「Tomcat那些事兒」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系Tomcat那些事兒公眾號。