一、從認識ByteBuddy開始
在之前的博客當中我們了解了Java Agent的一些基本概念和如何編寫一個簡單的Java Agent,但是在之前的博客中所使用的Agent編寫方法還是相對原始和繁瑣的。在原先的邏輯中我們是使用Instrument直接進行二進制碼操作和修改,這種方式要求使用者對Java class文件格式的相關知識能夠了然于胸,簡單來說就是需要做到人肉翻譯二進制文件這樣一個非人操作。為了進一步簡化編寫Java Agent的復雜度,這里我們要介紹下面這樣一款字節碼處理利器——ByteBuddy。
ByteBuddy是一個能夠在Java應用程序運行時用于創建和修改Java類的代碼生成和操作類庫,而這種處理能力是不需要編譯器參與的。從官網的介紹中可以發現,ByteBuddy是基于另一款字節碼操作神器ASM創造出來的,但是相比ASM的高使用門檻(仍然需要對Java字節碼有一定的了解),ByteBuddy使用起來會顯得更為簡單便捷。由于ByteBuddy提供了一系列完善且便捷的API,使用者可以在不需要了解Java字節碼和class文件格式的情況下很方便地進行字節碼操作(通過使用Java Agent或者在程序構建時完成對應的操作)。
二、編寫一個簡單的Java Agent——方法耗時統計
從上面的描述中我們可以了解到,ByteBuddy的誕生并非單純為了創建Java Agent,我們只是借助了ByteBuddy提供的API來生成更易維護的Java Agent,下面我們通過一個簡單的例子來了解一下如何使用ByteBuddy來編寫一個Java Agent。
下面我們要編寫的Java Agent主要是用于進行方法執行的耗時統計,參考以往使用AOP方式的思路,我們需要進行以下處理:
- 指定需要攔截處理的對象(可以是類、方法或者被注解的元素);
- 明確如何處理攔截的對象;
在Java Agent當中所有關于字節碼的操作都需要通過Instrumentation來進行,為了完成上面的兩個操作和關于Instrumentation的操作,ByteBuddy提供了AgentBuilder類來提供所需的API接口。我們借助下面的例子看一下AgentBuilder提供的API接口。
DemoAgent(示例Agent)
public class DemoAgent {
/**
* 在主線程啟動之前進行處理
*
* @param agentArgs 代理請求參數
* @param instrumentation 插樁
*/
public static void premain(String agentArgs, Instrumentation instrumentation) {
handleInstrument(instrumentation);
}
/**
* 進行插樁處理
*
* @param instrumentation 待處理樁
*/
private static void handleInstrument(Instrumentation instrumentation) {
new AgentBuilder.Default()
.type(ElementMatchers.nameEndsWith("App"))
.transform((builder, type, classLoader, module) -> builder.method(ElementMatchers.any()).intercept(MethodDelegation.to(TimeInterceptor.class)))
.installOn(instrumentation);
}
}
TimeInterceptor(方法執行耗時統計攔截器)
/**
* 時間統計攔截器,虛擬機維度的aop
*
* @author brucebat
* @version 1.0
* @since Created at 2021/12/30 10:31 上午
*/
public class TimeInterceptor {
/**
* 進行方法攔截, 注意這里可以對所有修飾符的修飾的方法(包含private的方法)進行攔截
*
* @param method 待處理方法
* @param callable 原方法執行
* @return 執行結果
*/
@RuntimeType
public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception {
long start = System.currentTimeMillis();
System.out.println("agent test: before method invoke! Method name: " + method.getName());
try {
return callable.call();
} catch (Exception e) {
// 進行異常信息上報
System.out.println("方法執行發生異常" + e.getMessage());
throw e;
} finally {
System.out.println("agent test: after method invoke! Method name: " + method.getName());
System.out.println(method + ": took " + (System.currentTimeMillis() - start) + " millisecond");
}
}
}
在上面的示例中我們使用了AgentBuilder的默認實現類(這里不難發現創建者模式的身影,由于不是關注的重點這里暫時不提),通過AgentBuilder的默認實現類完成了以下三件事件:
- 通過
Identified.Narrowable type(ElementMatcher<? super TypeDescription> typeMatcher);方法,指定了當前Agent需要攔截處理的對象,在本例中需要處理的對象為所有名稱以App結尾的類型; - 通過
Extendable transform(Transformer transformer);方法明確了需要如何處理被攔截的對象,這里使用了lambda方式來簡寫了對于Transformer#transform方法的實現。在實現的過程中通過builder.method()進一步明確需要處理的方法,在本例中會處理符合上一個攔截條件的所有方法,接著通過intercept()方法和MethodDelegation來注入關于被攔截方法的另一種實現方法,本例中通過TimeInterceptor來完成對于方法的執行耗時統計。看到這里是否會感覺和代理模式(或者說我們常用的AOP)有些類似,尤其是TimeInterceptor當中的處理邏輯,只是在這一過程中并沒有使用反射機制,這也是使用ByteBuddy的一個優勢; - 最后,在完成了對于攔截對象的指定和對象處理邏輯的編寫后,通過
ResettableClassFileTransformer installOn(Instrumentation instrumentation);完成Instrumentation裝載ClassFileTransformer(即上面關于文件修改的邏輯)的邏輯;
從上面的流程可以看出,本質上AgentBuilder幫助我們完成了關于ClassFileTransformer的實現和裝載邏輯。和原先直接編寫一個ClassFileTransformer然后修改其中的二進制文件數據相比,使用AgentBuilder來會讓我們對于整個的處理邏輯更加明確和專注,在編寫的過程我們只需要關注所需要修改的對象和修改的邏輯,其余只需要通過API就可以完成所有的操作。
三、總結
本文更多在于介紹ByteBuddy的概要和使用ByteBuddy創建Java Agent的使用流程,對于ByteBuddy具體的原理這里不做過多的說明,在后續的篇章中會進行具體的介紹。