做者:小傅哥
博客:https://bugstack.cn - 系列專題文章編寫
java
沉澱、分享、成長,讓本身和他人都能有所收穫!
字節碼編程插樁這種技術常與 Javaagent
技術結合用在系統的非入侵監控中,這樣就能夠替代在方法中進行硬編碼操做。好比,你須要監控一個方法,包括;方法信息、執行耗時、出入參數、執行鏈路以及異常等。那麼就很是適合使用這樣的技術手段進行處理。編程
爲了能讓這部分最核心的內容體現出來,本文會只使用 Javassist
技術對一段方法字節碼進行插樁操做,最終輸出這段方法的執行信息,以下;數組
方法 - 測試方法用於後續進行字節碼加強操做框架
public Integer strToInt(String str01, String str02) { return Integer.parseInt(str01); }
監控 - 對一段方法進行字節碼加強後,輸出監控信息性能
監控 - Begin 方法:org.itstack.demo.javassist.ApiTest.strToInt 入參:["str01","str02"] 入參[類型]:["java.lang.String","java.lang.String"] 入數[值]:["1","2"] 出參:java.lang.Integer 出參[值]:1 耗時:59(s) 監控 - End
有了這樣的監控方案,基本咱們能夠輸出方法執行過程當中的所有信息。再經過後期的完善將監控信息展現到界面,實時報警。既提高了系統的監控質量,也方便了研發排查並定位問題。測試
好!那麼接下來咱們開始一步步使用 javassist
進行字節碼插樁,已達到咱們的監控效果。this
itstack-demo-bytecode-1-04
,能夠關注公衆號:bugstack蟲洞棧
,回覆源碼下載獲取。你會得到一個下載連接列表,打開后里面的第17個「由於我有好多開源代碼」
,記得給個Star
!ClassPool pool = ClassPool.getDefault(); // 獲取類 CtClass ctClass = pool.get(org.itstack.demo.javassist.ApiTest.class.getName()); ctClass.replaceClassName("ApiTest", "ApiTest02"); String clazzName = ctClass.getName();
經過類名獲取類的信息,同時這裏能夠把類名進行替換。它也包括類裏面一些其餘獲取屬性的操做,好比;ctClass.getSimpleName()
、ctClass.getAnnotations()
等。編碼
CtMethod ctMethod = ctClass.getDeclaredMethod("strToInt"); String methodName = ctMethod.getName();
經過 getDeclaredMethod 獲取方法的 CtMethod
的內容。以後就能夠獲取方法的名稱等信息。spa
MethodInfo methodInfo = ctMethod.getMethodInfo();
MethodInfo 中包括了方法的信息;名稱、類型等內容。code
boolean isStatic = (methodInfo.getAccessFlags() & AccessFlag.STATIC) != 0;
經過 methodInfo.getAccessFlags()
獲取方法的標識,以後經過 與運算,AccessFlag.STATIC
,判斷方法是否爲靜態方法。由於靜態方法會影響後續的參數名稱獲取,靜態方法第一個參數是 this
,須要排除。
CodeAttribute codeAttribute = methodInfo.getCodeAttribute(); LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag); CtClass[] parameterTypes = ctMethod.getParameterTypes();
CtClass returnType = ctMethod.getReturnType(); String returnTypeName = returnType.getName();
對於方法的出參信息,只須要獲取出參類型。
System.out.println("類名:" + clazzName); System.out.println("方法:" + methodName); System.out.println("類型:" + (isStatic ? "靜態方法" : "非靜態方法")); System.out.println("描述:" + methodInfo.getDescriptor()); System.out.println("入參[名稱]:" + attr.variableName(1) + "," + attr.variableName(2)); System.out.println("入參[類型]:" + parameterTypes[0].getName() + "," + parameterTypes[1].getName()); System.out.println("出參[類型]:" + returnTypeName);
輸出結果
類名:org.itstack.demo.javassist.ApiTest 方法:strToInt 類型:非靜態方法 描述:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Integer; 入參[名稱]:str01,str02 入參[類型]:java.lang.String,java.lang.String 出參[類型]:java.lang.Integer
以上,所輸出信息,都在爲監控方法在作準備。從上面能夠記錄方法的基本描述以及入參個數等。尤爲是入參個數,由於在後續還須要使用 $1
,來獲取沒有給入參的值。
一段需會被字節碼插樁改變的原始方法;
public class ApiTest { public Integer strToInt(String str01, String str02) { return Integer.parseInt(str01); } }
在監控的適合,不可能每一次調用都把全部方法信息彙總輸出出來。這樣作不僅是性能問題,而是這些都是固定不變的信息,沒有必要讓每一次方法執行都輸出。
好!那麼在方法編譯時候,給每個方法都生成一個惟一ID
,用ID
關聯上方法的固定信息。也就能夠把監控數據經過ID
傳遞到外面。
// 方法:生成方法惟一標識ID int idx = Monitor.generateMethodId(clazzName, methodName, parameterNameList, parameterTypeList, returnTypeName);
生成ID的過程
public static final int MAX_NUM = 1024 * 32; private final static AtomicInteger index = new AtomicInteger(0); private final static AtomicReferenceArray<MethodDescription> methodTagArr = new AtomicReferenceArray<>(MAX_NUM); public static int generateMethodId(String clazzName, String methodName, List<String> parameterNameList, List<String> parameterTypeList, String returnType) { MethodDescription methodDescription = new MethodDescription(); methodDescription.setClazzName(clazzName); methodDescription.setMethodName(methodName); methodDescription.setParameterNameList(parameterNameList); methodDescription.setParameterTypeList(parameterTypeList); methodDescription.setReturnType(returnType); int methodId = index.getAndIncrement(); if (methodId > MAX_NUM) return -1; methodTagArr.set(methodId, methodDescription); return methodId; }
// 定義屬性 ctMethod.addLocalVariable("startNanos", CtClass.longType); // 方法前增強 ctMethod.insertBefore("{ startNanos = System.nanoTime(); }");
long
類型的屬性,startNanos
。並經過 insertBefore
插入到方法內容的開始處。最終 class
類方法
public class ApiTest { public Integer strToInt(String str01, String str02) { long startNanos = System.nanoTime(); return Integer.parseInt(str01); } }
// 定義屬性 ctMethod.addLocalVariable("parameterValues", pool.get(Object[].class.getName())); // 方法前增強 ctMethod.insertBefore("{ parameterValues = new Object[]{" + parameters.toString() + "}; }");
Object[]
,用於記錄入參信息。最終 class
類方法
public Integer strToInt(String str01, String str02) { Object[] var10000 = new Object[]{str01, str02}; long startNanos = System.nanoTime(); return Integer.parseInt(str01); }
insertBefore
進行插入,這裏是爲了更加清晰的向你展現字節碼插樁的過程。如今咱們就有了進入方法的時間和參數集合,方便後續輸出。由於咱們須要將監控信息,輸出給外部。那麼咱們這裏會定義一個靜態方法,讓字節碼加強後的方法去調用,輸出監控信息。
public static void point(final int methodId, final long startNanos, Object[] parameterValues, Object returnValues) { MethodDescription method = methodTagArr.get(methodId); System.out.println("監控 - Begin"); System.out.println("方法:" + method.getClazzName() + "." + method.getMethodName()); System.out.println("入參:" + JSON.toJSONString(method.getParameterNameList()) + " 入參[類型]:" + JSON.toJSONString(method.getParameterTypeList()) + " 入數[值]:" + JSON.toJSONString(parameterValues)); System.out.println("出參:" + method.getReturnType() + " 出參[值]:" + JSON.toJSONString(returnValues)); System.out.println("耗時:" + (System.nanoTime() - startNanos) / 1000000 + "(s)"); System.out.println("監控 - End\r\n"); } public static void point(final int methodId, Throwable throwable) { MethodDescription method = methodTagArr.get(methodId); System.out.println("監控 - Begin"); System.out.println("方法:" + method.getClazzName() + "." + method.getMethodName()); System.out.println("異常:" + throwable.getMessage()); System.out.println("監控 - End\r\n"); }
MQ
將監控信息發送給服務端記錄起來並作展現。// 方法後增強 ctMethod.insertAfter("{ org.itstack.demo.javassist.Monitor.point(" + idx + ", startNanos, parameterValues, $_);}", false); // 若是返回類型非對象類型,$_ 須要進行類型轉換
idx
、startNanos
、parameterValues
、$_
出參值 最終 class
類方法
public Integer strToInt(String str01, String str02) { Object[] parameterValues = new Object[]{str01, str02}; long startNanos = System.nanoTime(); Integer var7 = Integer.parseInt(str01); Monitor.point(0, startNanos, parameterValues, var7); return var7; }
以上插樁內容,若是隻是正常調用仍是沒問題的。可是若是方法拋出異常,那麼這個時候就不能作到收集監控信息了。因此還須要給方法添加上 TryCatch
。
// 方法;添加TryCatch ctMethod.addCatch("{ org.itstack.demo.javassist.Monitor.point(" + idx + ", $e); throw $e; }", ClassPool.getDefault().get("java.lang.Exception")); // 添加異常捕獲
addCatch
將方法包裝在 TryCatch
裏面。catch
中調用外部方法,將異常信息輸出。$e
,用於獲取拋出異常的內容。最終 class
類方法
public Integer strToInt(String str01, String str02) { try { Object[] parameterValues = new Object[]{str01, str02}; long startNanos = System.nanoTime(); Integer var7 = Integer.parseInt(str01); Monitor.point(0, startNanos, parameterValues, var7); return var7; } catch (Exception var9) { Monitor.point(0, var9); throw var9; } }
收錄方法執行的信息
,包括它的正常執行以及異常狀況。接下來就是執行咱們的調用測試被修改後的方法字節碼。經過不一樣的入參,來驗證監控結果;
// 測試調用 byte[] bytes = ctClass.toBytecode(); Class<?> clazzNew = new GenerateClazzMethod().defineClass("org.itstack.demo.javassist.ApiTest", bytes, 0, bytes.length); // 反射獲取 main 方法 Method method = clazzNew.getMethod("strToInt", String.class, String.class); Object obj_01 = method.invoke(clazzNew.newInstance(), "1", "2"); System.out.println("正確入參:" + obj_01); Object obj_02 = method.invoke(clazzNew.newInstance(), "a", "b"); System.out.println("異常入參:" + obj_02);
ClassLoader
加載字節碼,以後生成新的類。測試結果
監控 - Begin 方法:org.itstack.demo.javassist.ApiTest.strToInt 入參:["str01","str02"] 入參[類型]:["java.lang.String","java.lang.String"] 入數[值]:["1","2"] 出參:java.lang.Integer 出參[值]:1 耗時:63(s) 監控 - End 正確入參:1 監控 - Begin 方法:org.itstack.demo.javassist.ApiTest.strToInt 異常:For input string: "a" 監控 - End
Javassist
字節碼操做框架能夠很是方便的去進行字節碼加強,也不須要考慮純字節碼編程下的指令碼控制。但若是考慮性能以及更加細緻的改變,仍是須要使用到 ASM
。這裏包括一些字節碼操做的知識點,以下;
methodInfo.getDescriptor()
,能夠輸出方法描述信息。(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Integer;
,其實就是方法的出入參和返回值。$1 $2 ...
用於獲取不一樣位置的參數。$$
能夠獲取所有入參,可是不太適合用在數值傳遞中。this
參數。AccessFlag.STATIC。addCatch
最開始執行就包裹原有方法內的內容,最後執行就包括全部內容。它依賴於順序操做,其餘的方法也是這樣;insertBefore
、insertAfter
。