字節(jié)碼插樁技術(shù),用于系統(tǒng)監(jiān)控設(shè)計(jì)和實(shí)現(xiàn)

轉(zhuǎn)載作者:小傅哥
本文會(huì)基于 AOP、字節(jié)碼框架(ASM、Javassist、Byte-Buddy),分別實(shí)現(xiàn)不同的監(jiān)控實(shí)現(xiàn)代碼。整個(gè)工程結(jié)構(gòu)如下:

MonitorDesign
├── cn-bugstack-middleware-aop
├── cn-bugstack-middleware-asm
├── cn-bugstack-middleware-bytebuddy
├── cn-bugstack-middleware-javassist
├── cn-bugstack-middleware-test
└── pom.xml

簡(jiǎn)單介紹:aop、asm、bytebuddy、javassist,分別是四種不同的實(shí)現(xiàn)方案。test 是一個(gè)基于 SpringBoot 的簡(jiǎn)單測(cè)試工程。

cn-bugstack-middleware-test
@RestController
public class UserController {

    private Logger logger = LoggerFactory.getLogger(UserController.class);

    /**
     * 測(cè)試:http://localhost:8081/api/queryUserInfo?userId=aaa
     */
    @RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
    public UserInfo queryUserInfo(@RequestParam String userId) {
        logger.info("查詢用戶信息,userId:{}", userId);
        return new UserInfo("蟲(chóng)蟲(chóng):" + userId, 19, "天津市東麗區(qū)萬(wàn)科賞溪苑14-0000");
    }

}

接下來(lái)的各類監(jiān)控代碼實(shí)現(xiàn),都會(huì)以監(jiān)控 UserController#queryUserInfo 的方法執(zhí)行信息為主,看看各類技術(shù)都是怎么操作的

使用 AOP 做個(gè)切面監(jiān)控

1. 工程結(jié)構(gòu)
cn-bugstack-middleware-aop
└── src
    ├── main
    │   └── java
    │       ├── cn.bugstack.middleware.monitor
    │       │   ├── annotation
    │       │   │   └── DoMonitor.java
    │       │   ├── config
    │       │   │   └── MonitorAutoConfigure.java
    │       │   └── DoJoinPoint.java
    │       └── resources
    │           └── META-INF 
    │               └── spring.factories
    └── test
        └── java
            └── cn.bugstack.middleware.monitor.test
                └── ApiTest.java

基于 AOP 實(shí)現(xiàn)的監(jiān)控系統(tǒng),核心邏輯的以上工程并不復(fù)雜,其核心點(diǎn)在于對(duì)切面的理解和運(yùn)用,以及一些配置項(xiàng)需要按照 SpringBoot 中的實(shí)現(xiàn)方式進(jìn)行開(kāi)發(fā)。

  • DoMonitor,是一個(gè)自定義注解。它作用就是在需要使用到的方法監(jiān)控接口上,添加此注解并配置必要的信息。
  • MonitorAutoConfigure,配置下是可以對(duì) SpringBoot yml 文件的使用,可以處理一些 Bean 的初始化操作。
  • DoJoinPoint,是整個(gè)中間件的核心部分,它負(fù)責(zé)對(duì)所有添加自定義注解的方法進(jìn)行攔截和邏輯處理。
2. 定義監(jiān)控注解

cn.bugstack.middleware.monitor.annotation.DoMonitor

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoMonitor {

   String key() default "";
   String desc() default "";

}
3. 定義切面攔截

cn.bugstack.middleware.monitor.DoJoinPoint

@Aspect
public class DoJoinPoint {

    @Pointcut("@annotation(cn.bugstack.middleware.monitor.annotation.DoMonitor)")
    public void aopPoint() {
    }

    @Around("aopPoint() && @annotation(doMonitor)")
    public Object doRouter(ProceedingJoinPoint jp, DoMonitor doMonitor) throws Throwable {
        long start = System.currentTimeMillis();
        Method method = getMethod(jp);
        try {
            return jp.proceed();
        } finally {
            System.out.println("監(jiān)控 - Begin By AOP");
            System.out.println("監(jiān)控索引:" + doMonitor.key());
            System.out.println("監(jiān)控描述:" + doMonitor.desc());
            System.out.println("方法名稱:" + method.getName());
            System.out.println("方法耗時(shí):" + (System.currentTimeMillis() - start) + "ms");
            System.out.println("監(jiān)控 - End\r\n");
        }
    }

    private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
        Signature sig = jp.getSignature();
        MethodSignature methodSignature = (MethodSignature) sig;
        return jp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
    }

}
  • 使用注解 @Aspect,定義切面類。這是一個(gè)非常常用的切面定義方式。
  • @Pointcut("@annotation(cn.bugstack.middleware.monitor.annotation.DoMonitor)"),定義切點(diǎn)。在 Pointcut 中提供了很多的切點(diǎn)尋找方式,有指定方法名稱的、有范圍篩選表達(dá)式的,也有我們現(xiàn)在通過(guò)自定義注解方式的。一般在中間件開(kāi)發(fā)中,自定義注解方式使用的比較多,因?yàn)樗梢愿屿`活的運(yùn)用到各個(gè)業(yè)務(wù)系統(tǒng)中。
  • @Around("aopPoint() && @annotation(doMonitor)"),可以理解為是對(duì)方法增強(qiáng)的織入動(dòng)作,有了這個(gè)注解的效果就是在你調(diào)用已經(jīng)加了自定義注解 @DoMonitor 的方法時(shí),會(huì)先進(jìn)入到此切點(diǎn)增強(qiáng)的方法。那么這個(gè)時(shí)候就你可以做一些對(duì)方法的操作動(dòng)作了,比如我們要做一些方法監(jiān)控和日志打印等。
  • 最后在 doRouter 方法體中獲取把方法執(zhí)行 jp.proceed(); 使用 try finally 包裝起來(lái),并打印相關(guān)的監(jiān)控信息。這些監(jiān)控信息的獲取最后都是可以通過(guò)異步消息的方式發(fā)送給服務(wù)端,再由服務(wù)器進(jìn)行處理監(jiān)控?cái)?shù)據(jù)和處理展示到監(jiān)控頁(yè)面。
4. 初始化切面類

cn.bugstack.middleware.monitor.config.MonitorAutoConfigure

@Configuration
public class MonitorAutoConfigure {

    @Bean
    @ConditionalOnMissingBean
    public DoJoinPoint point(){
        return new DoJoinPoint();
    }

}
  • @Configuration,可以算作是一個(gè)組件注解,在 SpringBoot 啟動(dòng)時(shí)可以進(jìn)行加載創(chuàng)建出 Bean 文件。因?yàn)?@Configuration 注解有一個(gè) @Component 注解
  • MonitorAutoConfigure 可以處理自定義在 yml 中的配置信息,也可以用于初始化 Bean 對(duì)象,比如在這里我們實(shí)例化了 DoJoinPoint 切面對(duì)象。
5. 運(yùn)行測(cè)試
5.1 引入 POM 配置
<!-- 監(jiān)控方式:AOP -->
<dependency>
    <groupId>cn.bugstack.middleware</groupId>
    <artifactId>cn-bugstack-middleware-aop</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
5.2 方法上配置監(jiān)控注冊(cè)
@DoMonitor(key = "cn.bugstack.middleware.UserController.queryUserInfo", desc = "查詢用戶信息")
@RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
public UserInfo queryUserInfo(@RequestParam String userId) {
    logger.info("查詢用戶信息,userId:{}", userId);
    return new UserInfo("蟲(chóng)蟲(chóng):" + userId, 19, "天津市東麗區(qū)萬(wàn)科賞溪苑14-0000");
}

在通過(guò) POM 引入自己的開(kāi)發(fā)的組件后,就可以通過(guò)自定義的注解,攔截方法獲取監(jiān)控信息。

5.3 測(cè)試結(jié)果
2021-07-04 23:21:10.710  INFO 19376 --- [nio-8081-exec-1] c.b.m.test.interfaces.UserController     : 查詢用戶信息,userId:aaa
監(jiān)控 - Begin By AOP
監(jiān)控索引:cn.bugstack.middleware.UserController.queryUserInfo
監(jiān)控描述:查詢用戶信息
方法名稱:queryUserInfo
方法耗時(shí):6ms
監(jiān)控 - End

接下來(lái)我們開(kāi)始介紹關(guān)于使用字節(jié)碼插樁非入侵的方式進(jìn)行系統(tǒng)監(jiān)控,關(guān)于字節(jié)碼插樁常用的有三個(gè)組件,包括:ASM、Javassit、Byte-Buddy,接下來(lái)我們分別介紹它們是如何使用的。

ASM

ASM 是一個(gè) Java 字節(jié)碼操控框架。它能被用來(lái)動(dòng)態(tài)生成類或者增強(qiáng)既有類的功能。ASM 可以直接產(chǎn)生二進(jìn)制 class 文件,也可以在類被加載入 Java 虛擬機(jī)之前動(dòng)態(tài)改變類行為。Java class 被存儲(chǔ)在嚴(yán)格格式定義的 .class 文件里,這些類文件擁有足夠的元數(shù)據(jù)來(lái)解析類中的所有元素:類名稱、方法、屬性以及 Java 字節(jié)碼(指令)。ASM 從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據(jù)用戶要求生成新類。
1. 先來(lái)個(gè)測(cè)試

cn.bugstack.middleware.monitor.test.ApiTest

private static byte[] generate() {
    ClassWriter classWriter = new ClassWriter(0);
    // 定義對(duì)象頭;版本號(hào)、修飾符、全類名、簽名、父類、實(shí)現(xiàn)的接口
    classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, "cn/bugstack/demo/asm/AsmHelloWorld", null, "java/lang/Object", null);
    // 添加方法;修飾符、方法名、描述符、簽名、異常
    MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
    // 執(zhí)行指令;獲取靜態(tài)屬性
    methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
    // 加載常量 load constant
    methodVisitor.visitLdcInsn("Hello World ASM!");
    // 調(diào)用方法
    methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    // 返回
    methodVisitor.visitInsn(Opcodes.RETURN);
    // 設(shè)置操作數(shù)棧的深度和局部變量的大小
    methodVisitor.visitMaxs(2, 1);
    // 方法結(jié)束
    methodVisitor.visitEnd();
    // 類完成
    classWriter.visitEnd();
    // 生成字節(jié)數(shù)組
    return classWriter.toByteArray();
}

以上這段代碼就是基于 ASM 編寫(xiě)的 HelloWorld,整個(gè)過(guò)程包括:定義一個(gè)類的生成 ClassWriter、設(shè)定版本、修飾符、全類名、簽名、父類、實(shí)現(xiàn)的接口,其實(shí)也就是那句;public class HelloWorld

監(jiān)控設(shè)計(jì)工程結(jié)構(gòu)
cn-bugstack-middleware-asm
└── src
    ├── main
    │   ├── java
    │   │   └── cn.bugstack.middleware.monitor
    │   │       ├── config
    │   │       │   ├── MethodInfo.java
    │   │       │   └── ProfilingFilter.java
    │   │       ├── probe
    │   │       │   ├── ProfilingAspect.java
    │   │       │   ├── ProfilingClassAdapter.java
    │   │       │   ├── ProfilingMethodVisitor.java
    │   │       │   └── ProfilingTransformer.java
    │   │       └── PreMain.java
    │   └── resources   
    │       └── META_INF
    │           └── MANIFEST.MF
    └── test
        └── java
            └── cn.bugstack.middleware.monitor.test
                └── ApiTest.java

以上工程結(jié)構(gòu)是使用 ASM 框架給系統(tǒng)方法做增強(qiáng)操作,也就是相當(dāng)于通過(guò)框架完成硬編碼寫(xiě)入方法前后的監(jiān)控信息。不過(guò)這個(gè)過(guò)程轉(zhuǎn)移到了 Java 程序啟動(dòng)時(shí)在 Javaagent#premain 進(jìn)行處理。

  • MethodInfo 是方法的定義,主要是描述類名、方法名、描述、入?yún)ⅰ⒊鰠⑿畔ⅰ?/li>
  • ProfilingFilter 是監(jiān)控的配置信息,主要是過(guò)濾一些不需要字節(jié)碼增強(qiáng)操作的方法,比如main、hashCode、javax/等
  • ProfilingAspect、ProfilingClassAdapter、ProfilingMethodVisitor、ProfilingTransformer,這四個(gè)類主要是完成字節(jié)碼插裝操作和輸出監(jiān)控結(jié)果的類。
  • PreMain 提供了 Javaagent 的入口,JVM 首先嘗試在代理類上調(diào)用 premain 方法。
  • MANIFEST.MF 是配置信息,主要是找到 Premain-Class Premain-Class: cn.bugstack.middleware.monitor.PreMain
3. 監(jiān)控類入口

cn.bugstack.middleware.monitor.PreMain

public class PreMain {

    //JVM 首先嘗試在代理類上調(diào)用以下方法
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new ProfilingTransformer());
    }

    //如果代理類沒(méi)有實(shí)現(xiàn)上面的方法,那么 JVM 將嘗試調(diào)用該方法
    public static void premain(String agentArgs) {
    }

}

這個(gè)是 Javaagent 技術(shù)的固定入口方法類,同時(shí)還需要把這個(gè)類的路徑配置到 MANIFEST.MF 中。

4. 字節(jié)碼方法處理

cn.bugstack.middleware.monitor.probe.ProfilingTransformer

public class ProfilingTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
            if (ProfilingFilter.isNotNeedInject(className)) {
                return classfileBuffer;
            }
            return getBytes(loader, className, classfileBuffer);
        } catch (Throwable e) {
            System.out.println(e.getMessage());
        }
        return classfileBuffer;
    }

    private byte[] getBytes(ClassLoader loader, String className, byte[] classfileBuffer) {
        ClassReader cr = new ClassReader(classfileBuffer);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        ClassVisitor cv = new ProfilingClassAdapter(cw, className);
        cr.accept(cv, ClassReader.EXPAND_FRAMES);
        return cw.toByteArray();
    }

}
5. 字節(jié)碼方法解析

cn.bugstack.middleware.monitor.probe.ProfilingMethodVisitor

public class ProfilingMethodVisitor extends AdviceAdapter {

    private List<String> parameterTypeList = new ArrayList<>();
    private int parameterTypeCount = 0;     // 參數(shù)個(gè)數(shù)
    private int startTimeIdentifier;        // 啟動(dòng)時(shí)間標(biāo)記
    private int parameterIdentifier;        // 入?yún)?nèi)容標(biāo)記
    private int methodId = -1;              // 方法全局唯一標(biāo)記
    private int currentLocal = 0;           // 當(dāng)前局部變量值
    private final boolean isStaticMethod;   // true;靜態(tài)方法,false;非靜態(tài)方法
    private final String className;

    protected ProfilingMethodVisitor(int access, String methodName, String desc, MethodVisitor mv, String className, String fullClassName, String simpleClassName) {
        super(ASM5, mv, access, methodName, desc);
        this.className = className;
        // 判斷是否為靜態(tài)方法,非靜態(tài)方法中局部變量第一個(gè)值是this,靜態(tài)方法是第一個(gè)入?yún)?shù)
        isStaticMethod = 0 != (access & ACC_STATIC);
        //(String var1,Object var2,String var3,int var4,long var5,int[] var6,Object[][] var7,Req var8)=="(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;IJ[I[[Ljava/lang/Object;Lorg/itstack/test/Req;)V"
        Matcher matcher = Pattern.compile("(L.*?;|\\[{0,2}L.*?;|[ZCBSIFJD]|\\[{0,2}[ZCBSIFJD]{1})").matcher(desc.substring(0, desc.lastIndexOf(')') + 1));
        while (matcher.find()) {
            parameterTypeList.add(matcher.group(1));
        }
        parameterTypeCount = parameterTypeList.size();
        methodId = ProfilingAspect.generateMethodId(new MethodInfo(fullClassName, simpleClassName, methodName, desc, parameterTypeList, desc.substring(desc.lastIndexOf(')') + 1)));
    }     

    //... 一些字節(jié)碼插樁操作 
}
  • 當(dāng)程序啟動(dòng)加載的時(shí)候,每個(gè)類的每一個(gè)方法都會(huì)被監(jiān)控到。類的名稱、方法的名稱、方法入?yún)⒊鰠⒌拿枋龅龋伎梢栽谶@里獲取。
  • 為了可以在后續(xù)監(jiān)控處理不至于每一次都去傳參(方法信息)浪費(fèi)消耗性能,一般這里都會(huì)給每個(gè)方法生產(chǎn)一個(gè)全局防重的 id ,通過(guò)這個(gè) id 就可以查詢到對(duì)應(yīng)的方法。
  • 另外從這里可以看到的方法的入?yún)⒑统鰠⒈幻枋龀梢欢沃付ǖ拇a,(II)Ljava/lang/String; ,為了我們后續(xù)對(duì)參數(shù)進(jìn)行解析,那么需要將這段字符串進(jìn)行拆解。
6. 運(yùn)行測(cè)試
6.1 配置 VM 參數(shù) Javaagent
-javaagent:E:\itstack\git\github.com\MonitorDesign\cn-bugstack-middleware-asm\target\cn-bugstack-middleware-asm.jar

IDEA 運(yùn)行時(shí)候配置到 VM options 中,jar包地址按照自己的路徑進(jìn)行配置。

6.2 測(cè)試結(jié)果
監(jiān)控 - Begin By ASM
方法:cn.bugstack.middleware.test.interfaces.UserController$$EnhancerBySpringCGLIB$$8f5a18ca.queryUserInfo
入?yún)ⅲ簄ull 入?yún)㈩愋停篬"Ljava/lang/String;"] 入數(shù)[值]:["aaa"]
出參:Lcn/bugstack/middleware/test/interfaces/dto/UserInfo; 出參[值]:{"address":"天津市東麗區(qū)萬(wàn)科賞溪苑14-0000","age":19,"code":"0000","info":"success","name":"蟲(chóng)蟲(chóng):aaa"}
耗時(shí):54(s)
監(jiān)控 - End

Javassist

Javassist是一個(gè)開(kāi)源的分析、編輯和創(chuàng)建Java字節(jié)碼的類庫(kù)。是由東京工業(yè)大學(xué)的數(shù)學(xué)和計(jì)算機(jī)科學(xué)系的 Shigeru Chiba (千葉 滋)所創(chuàng)建的。它已加入了開(kāi)放源代碼JBoss 應(yīng)用服務(wù)器項(xiàng)目,通過(guò)使用Javassist對(duì)字節(jié)碼操作為JBoss實(shí)現(xiàn)動(dòng)態(tài)"AOP"框架。
1. 先來(lái)個(gè)測(cè)試

cn.bugstack.middleware.monitor.test.ApiTest

public class ApiTest {

    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();

        CtClass ctClass = pool.makeClass("cn.bugstack.middleware.javassist.MathUtil");

        // 屬性字段
        CtField ctField = new CtField(CtClass.doubleType, "π", ctClass);
        ctField.setModifiers(Modifier.PRIVATE + Modifier.STATIC + Modifier.FINAL);
        ctClass.addField(ctField, "3.14");

        // 方法:求圓面積
        CtMethod calculateCircularArea = new CtMethod(CtClass.doubleType, "calculateCircularArea", new CtClass[]{CtClass.doubleType}, ctClass);
        calculateCircularArea.setModifiers(Modifier.PUBLIC);
        calculateCircularArea.setBody("{return π * $1 * $1;}");
        ctClass.addMethod(calculateCircularArea);

        // 方法;兩數(shù)之和
        CtMethod sumOfTwoNumbers = new CtMethod(pool.get(Double.class.getName()), "sumOfTwoNumbers", new CtClass[]{CtClass.doubleType, CtClass.doubleType}, ctClass);
        sumOfTwoNumbers.setModifiers(Modifier.PUBLIC);
        sumOfTwoNumbers.setBody("{return Double.valueOf($1 + $2);}");
        ctClass.addMethod(sumOfTwoNumbers);
        // 輸出類的內(nèi)容
        ctClass.writeFile();

        // 測(cè)試調(diào)用
        Class clazz = ctClass.toClass();
        Object obj = clazz.newInstance();

        Method method_calculateCircularArea = clazz.getDeclaredMethod("calculateCircularArea", double.class);
        Object obj_01 = method_calculateCircularArea.invoke(obj, 1.23);
        System.out.println("圓面積:" + obj_01);

        Method method_sumOfTwoNumbers = clazz.getDeclaredMethod("sumOfTwoNumbers", double.class, double.class);
        Object obj_02 = method_sumOfTwoNumbers.invoke(obj, 1, 2);
        System.out.println("兩數(shù)和:" + obj_02);
    }

}
  • 這是一個(gè)使用 Javassist 生成的求圓面積和抽象的類和方法并運(yùn)行結(jié)果的過(guò)程,可以看到 Javassist 主要是 ClassPool、CtClass、CtField、CtMethod 等方法的使用。
  • 測(cè)試結(jié)果主要包括會(huì)生成一個(gè)指定路徑下的類 cn.bugstack.middleware.javassist.MathUtil,同時(shí)還會(huì)在控制臺(tái)輸出結(jié)果。
生成的類
public class MathUtil {
  private static final double π = 3.14D;

  public double calculateCircularArea(double var1) {
      return 3.14D * var1 * var1;
  }

  public Double sumOfTwoNumbers(double var1, double var3) {
      return var1 + var3;
  }

  public MathUtil() {
  }
}
2. 監(jiān)控設(shè)計(jì)工程結(jié)構(gòu)
cn-bugstack-middleware-javassist
└── src
    ├── main
    │   ├── java
    │   │   └── cn.bugstack.middleware.monitor
    │   │       ├── config
    │   │       │   └── MethodDescription.java
    │   │       ├── probe
    │   │       │   ├── Monitor.java
    │   │       │   └── MyMonitorTransformer.java
    │   │       └── PreMain.java
    │   └── resources
    │       └── META_INF
    │           └── MANIFEST.MF
    └── test
        └── java
            └── cn.bugstack.middleware.monitor.test
                └── ApiTest.java

整個(gè)使用 javassist 實(shí)現(xiàn)的監(jiān)控框架來(lái)看,與 ASM 的結(jié)構(gòu)非常相似,但大部分操作字節(jié)碼的工作都交給了 javassist 框架來(lái)處理,所以整個(gè)代碼結(jié)構(gòu)看上去更簡(jiǎn)單了。

3. 監(jiān)控方法插樁

cn.bugstack.middleware.monitor.probe.MyMonitorTransformer

public class MyMonitorTransformer implements ClassFileTransformer {

    private static final Set<String> classNameSet = new HashSet<>();

    static {
        classNameSet.add("cn.bugstack.middleware.test.interfaces.UserController");
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        try {
            String currentClassName = className.replaceAll("/", ".");
            if (!classNameSet.contains(currentClassName)) { // 提升classNameSet中含有的類
                return null;
            }

            // 獲取類
            CtClass ctClass = ClassPool.getDefault().get(currentClassName);
            String clazzName = ctClass.getName();

            // 獲取方法
            CtMethod ctMethod = ctClass.getDeclaredMethod("queryUserInfo");
            String methodName = ctMethod.getName();

            // 方法信息:methodInfo.getDescriptor();
            MethodInfo methodInfo = ctMethod.getMethodInfo();

            // 方法:入?yún)⑿畔?            CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
            LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
            CtClass[] parameterTypes = ctMethod.getParameterTypes();

            boolean isStatic = (methodInfo.getAccessFlags() & AccessFlag.STATIC) != 0;  // 判斷是否為靜態(tài)方法
            int parameterSize = isStatic ? attr.tableLength() : attr.tableLength() - 1; // 靜態(tài)類型取值
            List<String> parameterNameList = new ArrayList<>(parameterSize);            // 入?yún)⒚Q
            List<String> parameterTypeList = new ArrayList<>(parameterSize);            // 入?yún)㈩愋?            StringBuilder parameters = new StringBuilder();                             // 參數(shù)組裝;$1、$2...,$$可以獲取全部,但是不能放到數(shù)組初始化

            for (int i = 0; i < parameterSize; i++) {
                parameterNameList.add(attr.variableName(i + (isStatic ? 0 : 1))); // 靜態(tài)類型去掉第一個(gè)this參數(shù)
                parameterTypeList.add(parameterTypes[i].getName());
                if (i + 1 == parameterSize) {
                    parameters.append("$").append(i + 1);
                } else {
                    parameters.append("$").append(i + 1).append(",");
                }
            }

            // 方法:出參信息
            CtClass returnType = ctMethod.getReturnType();
            String returnTypeName = returnType.getName();

            // 方法:生成方法唯一標(biāo)識(shí)ID
            int idx = Monitor.generateMethodId(clazzName, methodName, parameterNameList, parameterTypeList, returnTypeName);

            // 定義屬性
            ctMethod.addLocalVariable("startNanos", CtClass.longType);
            ctMethod.addLocalVariable("parameterValues", ClassPool.getDefault().get(Object[].class.getName()));

            // 方法前加強(qiáng)
            ctMethod.insertBefore("{ startNanos = System.nanoTime(); parameterValues = new Object[]{" + parameters.toString() + "}; }");

            // 方法后加強(qiáng)
            ctMethod.insertAfter("{ cn.bugstack.middleware.monitor.probe.Monitor.point(" + idx + ", startNanos, parameterValues, $_);}", false); // 如果返回類型非對(duì)象類型,$_ 需要進(jìn)行類型轉(zhuǎn)換

            // 方法;添加TryCatch
            ctMethod.addCatch("{ cn.bugstack.middleware.monitor.probe.Monitor.point(" + idx + ", $e); throw $e; }", ClassPool.getDefault().get("java.lang.Exception"));   // 添加異常捕獲

            return ctClass.toBytecode();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}
  • 與 ASM 實(shí)現(xiàn)相比,整體的監(jiān)控方法都是類似的,所以這里只展示下不同的地方。
  • 通過(guò) Javassist 的操作,主要是實(shí)現(xiàn)一個(gè) ClassFileTransformer 接口的 transform 方法,在這個(gè)方法中獲取字節(jié)碼并進(jìn)行相應(yīng)的處理。
  • 處理過(guò)程包括:獲取類、獲取方法、獲取入?yún)⑿畔ⅰ@取出參信息、給方法生成唯一ID、之后開(kāi)始進(jìn)行方法的前后增強(qiáng)操作,這個(gè)增強(qiáng)也就是在方法塊中添加監(jiān)控代碼。
  • 最后返回字節(jié)碼信息 return ctClass.toBytecode(); 現(xiàn)在你新加入的字節(jié)碼就已經(jīng)可以被程序加載處理了。
4. 運(yùn)行測(cè)試
4.1 配置 VM 參數(shù) Javaagent
-javaagent:E:\itstack\git\github.com\MonitorDesign\cn-bugstack-middleware-javassist\target\cn-bugstack-middleware-javassist.jar

作者源碼:https://github.com/fuzhengwei/MonitorDesign

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容