Arthas底層位元組增強工具bytekit

簡要說明

在幾次生產環境排查問題中使用到了Arthas,查找了部分資料找到一個關於Arthas底層用於位元組增強的工具。

主要實現了java位元組碼檔案層次的動態增強,可在不重新編譯原java檔案重新生成class的情況下,在應用伺服器執行時對class檔案進行修改增強;

找了一圈都沒有找到除了在Arthas之外的用的地方,大機率的也就是用於系統執行時診斷之類的,或系統執行時開關調整除錯,獲取更多的除錯資訊;

體驗過程

1、引入maven依賴,主要依賴了以下資訊

com。alibaba bytekit-core 0。0。7 net。bytebuddy byte-buddy-agent 1。11。6 org。benf cfr 0。151 org。slf4j slf4j-api 1。7。30

2、以下幾個過程的檔案透過網上搜來的,基本大差不差,按照使用順序分別是

3、未進行位元組增強的原始檔如下

public class Sample { private int exceptionCount = 0; public String hello(String str, boolean exception) { if (exception) { exceptionCount++; throw new RuntimeException(“test exception, str: ” + str); } return “hello ” + str; }}

4、位元組增強類:對位元組檔案進行處理,到了處理了什麼,該類定義了進入方法@AtEnter,結束方法@AtExit,以及方法丟擲異常@AtExceptionExit時的處理;

// Sample 類的攔截器class SampleInterceptor { // 攔截方法Entry點進行處理 @AtEnter(inline = false, suppress = RuntimeException。class, suppressHandler = PrintExceptionSuppressHandler。class) public static void atEnter(@Binding。This Object object, @Binding。Class Object clazz, @Binding。Args Object[] args, @Binding。MethodName String methodName, @Binding。MethodDesc String methodDesc) { System。out。println(“atEnter, args[0]: ” + args[0]); } // 攔截方法正常返回的語句,在返回前進行處理 @AtExit(inline = true) public static void atExit(@Binding。Return Object returnObject,@Binding。MethodName String methodName) { System。out。println(“atExit, returnObject: ” + returnObject); } // 攔截方法內部丟擲異常點 @AtExceptionExit(inline = true, onException = RuntimeException。class) public static void atExceptionExit(@Binding。Throwable RuntimeException ex, @Binding。Field(name = “exceptionCount”) int exceptionCount,@Binding。MethodName String methodName) { System。out。println(“atExceptionExit, ex: ” + ex。getMessage() + “, field exceptionCount: ” + exceptionCount); }}

5、異常處理器:即上述程式提到的異常處理方法

// 異常處理器class PrintExceptionSuppressHandler { @ExceptionHandler(inline = true) public static void onSuppress(@Binding。Throwable Throwable e, @Binding。Class Object clazz) { System。out。println(“exception handler: ” + clazz); e。printStackTrace(); }}

6、對ByteKit的封裝處理,主要是獲取到目標類,和增強類,使用增強類對目標類的位元組碼檔案進行處理,獲取到新的位元組碼檔案

class EnhanceUtil { public static byte[] enhanceClass(Class targetClass, String[] targetMethodNames, Class interceptorClass) throws Exception { // 初始化Instrumentation AgentUtils。install(); // 解析定義的 Interceptor類 和相關的註解 DefaultInterceptorClassParser interceptorClassParser = new DefaultInterceptorClassParser(); List processors = interceptorClassParser。parse(interceptorClass); // 載入位元組碼 ClassNode classNode = AsmUtils。loadClass(targetClass); List methodNameList = Arrays。asList(targetMethodNames); // 對載入到的位元組碼做增強處理 for (MethodNode methodNode : classNode。methods) { if (methodNameList。contains(methodNode。name)) { MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode); for (InterceptorProcessor interceptor : processors) { interceptor。process(methodProcessor); } } } // 獲取增強後的位元組碼 return AsmUtils。toBytes(classNode); } }

7、最後是實際的執行過程:

//測試入口類class SampleDemo { public static void main(String[] args) throws Exception { // 啟動Sample System。out。println(“before retransform 。。。”); try { Sample sample = new Sample(); sample。hello(“1”, false); sample。hello(“2”, true); } catch (Exception e) { e。printStackTrace(System。out); } System。out。println(); // 對Sample類的hello方法進行攔截處理,返回增強後的位元組碼 byte[] bytes = EnhanceUtil。enhanceClass(Sample。class, new String[]{“hello”}, SampleInterceptor。class); // 檢視反編譯結果 //System。out。println(Decompiler。decompile(bytes)); // 透過 reTransform 增強類 AgentUtils。reTransform(Sample。class, bytes); // 啟動Sample System。out。println(“after retransform 。。。”); try { Sample sample = new Sample(); sample。hello(“3”, false); sample。hello(“4”, true); } catch (Exception e) { e。printStackTrace(System。out); } }}

8、執行結果

before retransform 。。。java。lang。RuntimeException: test exception, str: 2 at com。sukeinfo。bk。demo。Sample。hello(Sample。java:9) at com。sukeinfo。bk。demo。SampleDemo。main(SampleDemo。java:15)after retransform 。。。atEnter, args[0]: 3atExit, returnObject: hello 3atEnter, args[0]: 4atExceptionExit, ex: test exception, str: 4, field exceptionCount: 1java。lang。RuntimeException: test exception, str: 4 at com。sukeinfo。bk。demo。Sample。hello(Sample。java:9) at com。sukeinfo。bk。demo。SampleDemo。main(SampleDemo。java:36)

InstrumentApi。invokeOrigin

0、再以上mvn的配置基礎上增加了

junit junit 4。13。2 test org。assertj assertj-core 3。23。1 test

1、原始的java檔案如下

public class InvokeOriginDemo { public void returnVoid() { } public Void returnVoidObject() { int i = 0; try { int parseInt = Integer。parseInt(“1000”); i += parseInt; } catch (Exception e) { System。err。println(i + “ ” + e); } return null; } public int returnInt(int i) { return 9998; } public int returnIntToObject(int i) { return 9998; } public int returnIntToInteger(int i) { return 9998; } public static int returnIntStatic(int i) { return 9998; } public long returnLong() { return 9998L; } public long returnLongToObject() { return 9998L; } public String[] returnStrArray() { String[] result = new String[] {“abc”, “xyz” , “ufo”}; return result; } public String[] returnStrArrayWithArgs(int i, String s, long l) { String[] result = new String[] {“abc” + i, “xyz” + s , “ufo” + l}; return result; } public String returnStr() { return new Date()。toString(); } public Object returnObject() { return InvokeOriginDemo。class; } public int recursive(int i) { if (i == 1) { return 1; } return i + recursive(i - 1); } // 測試修改args public String changeArgs(int i, long l, String str) { return str + i + l; }}

2、透過位元組增強的檔案如下:

@Instrument(Class = “com。sukeinfo。bk。demo2。InvokeOriginDemo”)public abstract class InvokeOriginDemo_APM { public void returnVoid() { Object o = InstrumentApi。invokeOrigin(); System。out。println(o); } public Void returnVoidObject() { Void v = InstrumentApi。invokeOrigin(); System。out。println(v); return v; } public int returnInt(int i) { System。out。println(“before”); int value = InstrumentApi。invokeOrigin(); System。out。println(“after”); return value + 123; } public int returnIntToObject(int i) { Object value = InstrumentApi。invokeOrigin(); return 9998 + (Integer) value; } public int returnIntToInteger(int i) { Integer ixx = InstrumentApi。invokeOrigin(); return ixx + 9998; } public static int returnIntStatic(int i) { int result = InstrumentApi。invokeOrigin(); return 9998 + result; } public long returnLong() { long result = InstrumentApi。invokeOrigin(); return 9998L + result; } public long returnLongToObject() { Long lll = InstrumentApi。invokeOrigin(); return 9998L + lll; } public String[] returnStrArray() { String[] result = InstrumentApi。invokeOrigin(); System。err。println(result); return result; } public String[] returnStrArrayWithArgs(int i, String s, long l) { System。out。println(i); String[] result = InstrumentApi。invokeOrigin(); result[0] = “fff”; return result; } public String returnStr() { System。err。println(“ssss”); Object result = InstrumentApi。invokeOrigin(); return “hello” + result; } public Object returnObject() { InstrumentApi。invokeOrigin(); return InvokeOriginDemo。class; } public int recursive(int i) { int result = InstrumentApi。invokeOrigin(); System。err。println(result); return result; } public String changeArgs(int i, long l, String str) { i = 1; l = 9999; str = “xxx”; String result = InstrumentApi。invokeOrigin(); System。err。println(result); return result; }}

3、以增強後的returnInt方法為例進行說明,在增強後的該類中進行如下處理,得到的class檔案反編譯的效果如下,即將原始碼中的執行結果拿到InstrumentApi。invokeOrigin()所在位置再進行處理:

//原始碼public int returnInt(int i) { return 9998;}//增強程式碼如下public int returnInt(int i) { System。out。println(“before”); int value = InstrumentApi。invokeOrigin(); System。out。println(“after”); return value + 123;}//增強後執行時進行替換class檔案效果public int returnInt(int i) { System。out。println(“before”); int n = i; InvokeOriginDemo invokeOriginDemo = this; int value = 9998; System。out。println(“after”); return value + 123;}

4、增強測試類

public class InvokeOriginTest { ClassNode apmClassNode; ClassNode originClassNode; ClassNode targetClassNode; @Rule public TestName testName = new TestName(); @BeforeClass public static void beforeClass() throws IOException { } @Before public void before() throws IOException { apmClassNode = AsmUtils。loadClass(InvokeOriginDemo_APM。class); originClassNode = AsmUtils。loadClass(InvokeOriginDemo。class); byte[] renameClass = AsmUtils。renameClass(AsmUtils。toBytes(apmClassNode), Type。getObjectType(originClassNode。name)。getClassName()); apmClassNode = AsmUtils。toClassNode(renameClass); targetClassNode = AsmUtils。copy(originClassNode); } private Object replace(String methodName) throws Exception { System。err。println(methodName); for (MethodNode methodNode : apmClassNode。methods) { if (methodNode。name。equals(methodName)) { methodNode = AsmUtils。removeLineNumbers(methodNode); // 從原來的類裡查詢對應的函式 MethodNode findMethod = AsmUtils。findMethod(originClassNode。methods, methodNode); if (findMethod != null) { MethodNode methodNode2 = InstrumentImpl。replaceInvokeOrigin(originClassNode。name, findMethod, methodNode); //System。err。println(Decompiler。toString(methodNode2)); AsmUtils。replaceMethod(targetClassNode, methodNode2); } else { } } } byte[] resutlBytes = AsmUtils。toBytes(targetClassNode); System。err。println(“=================”); System。err。println(Decompiler。decompile(resutlBytes)); // System。err。println(AsmUtils。toASMCode(resutlBytes)); VerifyUtils。asmVerify(resutlBytes); return VerifyUtils。instanceVerity(resutlBytes); } @Test public void test_returnVoid() throws Exception { String methodName = testName。getMethodName()。substring(“test_”。length()); Object object = replace(methodName); Assertions。assertThat(VerifyUtils。invoke(object, methodName))。isEqualTo(null); } @Test public void test_returnVoidObject() throws Exception { String methodName = testName。getMethodName()。substring(“test_”。length()); Object object = replace(methodName); Assertions。assertThat(VerifyUtils。invoke(object, methodName))。isEqualTo(null); } @Test public void test_returnInt() throws Exception { String methodName = testName。getMethodName()。substring(“test_”。length()); Object object = replace(methodName); Assertions。assertThat(VerifyUtils。invoke(object, methodName, 123))。isEqualTo(9998 + 123); } @Test public void test_returnIntToObject() throws Exception { String methodName = testName。getMethodName()。substring(“test_”。length()); Object object = replace(methodName); Assertions。assertThat(VerifyUtils。invoke(object, methodName, 123))。isEqualTo(9998 + 9998); } @Test public void test_returnIntToInteger() throws Exception { String methodName = testName。getMethodName()。substring(“test_”。length()); Object object = replace(methodName); Assertions。assertThat(VerifyUtils。invoke(object, methodName, 123))。isEqualTo(9998 + 9998); } @Test public void test_returnIntStatic() throws Exception { String methodName = testName。getMethodName()。substring(“test_”。length()); Object object = replace(methodName); Assertions。assertThat(VerifyUtils。invoke(object, methodName, 123))。isEqualTo(9998 + 9998); } @Test public void test_returnLong() throws Exception { String methodName = testName。getMethodName()。substring(“test_”。length()); Object object = replace(methodName); Assertions。assertThat(VerifyUtils。invoke(object, methodName))。isEqualTo(9998L + 9998); } @Test public void test_returnLongToObject() throws Exception { String methodName = testName。getMethodName()。substring(“test_”。length()); Object object = replace(methodName); Assertions。assertThat(VerifyUtils。invoke(object, methodName))。isEqualTo(9998L + 9998); } @Test public void test_returnStrArray() throws Exception { String methodName = testName。getMethodName()。substring(“test_”。length()); Object object = replace(methodName); Assertions。assertThat(VerifyUtils。invoke(object, methodName))。isEqualTo(new String[] { “abc”, “xyz”, “ufo” }); } @Test public void test_returnStrArrayWithArgs() throws Exception { String methodName = testName。getMethodName()。substring(“test_”。length()); Object object = replace(methodName); Assertions。assertThat(VerifyUtils。invoke(object, methodName, 123, “sss”, 777L)) 。isEqualTo(new Object[] { “fff”, “xyz” + “sss”, “ufo” + 777 }); } @Test public void test_returnStr() throws Exception { String methodName = testName。getMethodName()。substring(“test_”。length()); Object object = replace(methodName); Assertions。assertThat(VerifyUtils。invoke(object, methodName))。asString()。startsWith(“hello”); } @Test public void test_returnObject() throws Exception { String methodName = testName。getMethodName()。substring(“test_”。length()); Object object = replace(methodName); Assertions。assertThat(VerifyUtils。invoke(object, methodName))。isEqualTo(object。getClass()); } @Test public void test_recursive() throws Exception { String methodName = testName。getMethodName()。substring(“test_”。length()); Object object = replace(methodName); Assertions。assertThat(VerifyUtils。invoke(object, methodName, 100))。isEqualTo((100 + 1) * 100 / 2); } @Test public void test_changeArgs() throws Exception { String methodName = testName。getMethodName()。substring(“test_”。length()); Object object = replace(methodName); Assertions。assertThat(VerifyUtils。invoke(object, methodName, 100, 333, “fff”))。isEqualTo(“xxx19999”); }}

參考檔案

https://blog。csdn。net/lianggzone/article/details/120245570