天天看點

基于 javaagent + javassist 一步步實作調用鍊系統 (2)

上一章中, 基本上将 JDBC, Servlet 的資訊采集以及調用鍊的實作思路給梳理清楚了. 現在我們就可以開始編寫我們的調用鍊系統了.  

對于整個後端應用, 我們并不需要采集所有類的執行資訊, 我們隻需要對 Servlet, Controller , Service 以及 JDBC 這四個層級的類進行調用資訊采集就可以了. 

對于每一個請求, 我們的調用鍊系統都會生成一個全局唯一的 ID (TraceId), 通過它來區分每一次完成的請求. 對于同一個 TraceId, 調用鍊系統會通過 spanId 來進行區分, 每一個方法的調用都會産生一個唯一的 spanId. 通過 spanId 來區分方法的調用順序以及調用層級. 

首先貼出 maven 工程的 pom 依賴:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>call-chain</artifactId>
    <version>1.0.SNAPSHOT</version>

    <dependencies>
        <!-- 引入 javassist 用于編輯位元組碼 -->
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.25.0-GA</version>
        </dependency>
        <!-- 引入 servlet-api 用于代理 HttpServlet 時使用 -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
	    <!-- 引入 alibaba 開源的 TransmittableThreadLocal 用于追蹤跨線程的方法調用軌迹 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>transmittable-thread-local</artifactId>
            <version>2.11.4</version>
        </dependency>
        <!-- 引入 Helper 包 -->
        <dependency>
            <groupId>com.codetool</groupId>
            <artifactId>common</artifactId>
            <version>1.0.RELEASE</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
	        <!-- 使用 assembly-plugin 來進行打包,它可以将依賴的 jar 包的源碼一起打進去 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>2.5.5</version>
                <configuration>
                    <archive>
			            <!-- 使用 classpath 下的 MAINFEST.MF -->
                        <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>

	        <!-- 設定程式編譯和運作的 jdk 版本 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>utf8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
           

示範一個經典的 controller 調用 service, service 調用 repository 的案例:

public class Controller {
    Service s = new Service();

    void c1() {
        s.s1();
        s.s2();
    }  

    public static void main(String[] args) {
        Controller c = new Controller();
        c.c1();
    }
}

public class Service {
    Repository r = new Repository();

    void s1() {
        r.r1();
    }

    void s2() {
        r.r2();
    }
}

public class Repository {
    void r1() {

    }

    void r2() {

    }
}
           

上面是一個監督的 controller 調用 service, service 調用 repository 的簡單例子, 我們的調用鍊會将上面的整個調用鍊路給梳理出來并且記錄到日志中. 首先, 它們的調用關系如下:

基于 javaagent + javassist 一步步實作調用鍊系統 (2)

在第一篇文章中, 是使用 Stack 來記錄調用層級關系的, 但是那個代碼隻能記錄調用層級, 而不能記錄具體的調用順序. 得到的結果隻能得出如上圖那樣的結果, 而我們需要實作的功能是這樣的, 能夠記錄每一個方法的調用層級, 同一個層級下的兩個方法存在不同的值來表示它們在同一個層級中的調用順序

基于 javaagent + javassist 一步步實作調用鍊系統 (2)

接下來, 使用代碼實作這個功能, 這裡我使用了一個棧和指針來實作這樣的功能. 整個流程是這樣的:

基于 javaagent + javassist 一步步實作調用鍊系統 (2)

首先, c.c1 方法開始執行. 建立出第一個 span, 它的值是 0, 并且指針指向 0 這個 span. 

c.c1 調用 s.s1 方法. 此時調用鍊發現已經存在根節點了. 于是在節點 0 的下面建立出節點1. 并且讓目前節點指向 0 -> 1 (也就是所謂的 0.1).

s.s1 調用 r.r1 方法, 按照上面的邏輯, 繼續在 0 -> 1 下面建立節點 1. 并且讓指針指向 0 -> 1 -> 1 (0.1.1)

基于 javaagent + javassist 一步步實作調用鍊系統 (2)

當方法指向完畢以後, 我們隻需要方向移動指針的位置就可以了. 比如 r.r1 指向完畢以後, 調用鍊會到了 s.s1. 此時指針就有 0 -> 1 -> 1 變成了 0 -> 1. 現在開始指向後面的部分. 然後方法繼續運作. 當 s.s1 方法執行完畢後. 執行 s.s2 方法, 此時. span 的變化如下

基于 javaagent + javassist 一步步實作調用鍊系統 (2)

當調用 s.s2 時, 此時 0 -> 1 已經存在了, 于是我們隻能建立 0 -> 2, 然後指針指向 0 -> 2, 接着調用 r.r2. 由于 0 -> 2 下面沒有子節點. 是以建立一個 0 -> 2 -> 1. 然後等待執行完畢後. 開始反向調整指針.

基于 javaagent + javassist 一步步實作調用鍊系統 (2)

此時, 整個流程就已經走完了. 整個原理就是這樣. 接下來我們使用代碼來進行實作:

package org.example.call;

import java.util.Stack;

/**
 * @author tameti
 */
public class TreeSpan implements Cloneable {
    // 記錄目前整個數中的根節點
    private Span rootSpan;

    // 記錄目前節點軌迹
    private Span pointSpan;

    public TreeSpan() {
        this(null);
    }

    public TreeSpan(String span) {
        if (span == null) {
            rootSpan = new Span(0, null);
        } else {
            String[] spans = span.split("\\.");
            Span tmpSpan = null;
            for (String s : spans) {
                int v = Integer.parseInt(s);
                tmpSpan = new Span(v, tmpSpan);
            }
            rootSpan = tmpSpan;
        }
        pointSpan = rootSpan;
    }

    public Span createEntrySpan() {
        Stack<Span> childSpans = pointSpan.childSpans;
        int value = 1;
        // 如果目前節點下不存在子節點, 那麼我們就直接建立一個 0 作為第一個子節點
        // 如果已經存在子節點了, 那麼我們就取這個棧中最後一個元素的值, 然後 +1 作為新的節點的值
        if (!childSpans.isEmpty()) {
            Span peek = childSpans.peek();
            value = peek.value + 1;
        }

        // 接下來開始建立 Span 節點, 它的父親節點是我們的軌迹節點
        Span newSpan = new Span(value, pointSpan);
        pointSpan.childSpans.push(newSpan);

        // 最後修改軌迹的節點的指針
        pointSpan = newSpan;
        return newSpan;
    }


    public void exitSpan() {
        // 1. 拿到軌迹節點的父親節點
        // 2. 然後修改軌迹節點的指針
        pointSpan = pointSpan.parentSpan;
    }

    public Span getCurrentSpan() {
        return pointSpan;
    }

    @Override
    protected TreeSpan clone() {
        return new TreeSpan(pointSpan.toString());
    }

    @Deprecated
    public static class Span {
        private int value;
        private Span parentSpan;
        private Stack<Span> childSpans = new Stack<>();

        public Span(int value, Span parentSpan) {
            this.value = value;
            this.parentSpan = parentSpan;
        }

        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append(value);
            Span currentSpan = parentSpan;
            while (currentSpan != null) {
                sb.append(".").append(currentSpan.value);
                currentSpan = currentSpan.parentSpan;
            }
            return sb.reverse().toString();
        }
    }
}
           

此時, 我們可以通過 createEntrySpan 來得到下一個 span 的值, 通過 exitSpan 來調整指針的變化. 當方法執行前, 執行 createEntrySpan 來建立 span, 方法執行完畢後, 通過 exitSpan 來反向移動這個指針. 這裡我們使用 toString 來輸出這個對象對應的字元串值, 比如 0 -> 1 輸出後得到 0.1

public static void main(String[] args) {
    TreeSpan span = new TreeSpan();
    span.createEntrySpan(); // 0.1
    span.createEntrySpan(); // 0.1.1
    span.exitSpan();        // 0.1
    span.createEntrySpan(); // 0.1.2
    System.out.println(span.getCurrentSpan().toString());   // 0.1.2
}
           

在應用通過 http 或者 rpc 調用另外一個服務接口時, 是存在 span 值傳遞的情況的. 比如 服務A 在 service 層中調用了 服務 B 的某某業務方法. 于是我們需要把 span 傳遞過去, span 傳遞過去後, 我們需要進行一個解析使用. 如下:

public static void main(String[] args) {
    TreeSpan span = new TreeSpan("0.1.3.2");
    span.createEntrySpan();
    System.out.println(span.getCurrentSpan().toString());   // 0.1.3.2.1
}
           

當支援這兩種功能以後, 這個 TreeSpan 就可以配置 ThreadLocal 來進行單系統的調用鍊監控了. 這裡 TreeSpan 實作的并不是很好.  TreeSpan 中的 Span 節點通過 Stack<Span> 來維護了很多的子節點, 然而這個棧的作用其實并沒有發揮出來, 于是我把 TreeSpan 給廢棄了, 替換後的代碼如下:

package org.example.call;

/**
 * 吸取了 TreeSpan 的精華, 去其糟粕. 底層是基于兩個指針實作的, 1 個指針維護調用鍊的父節點, 另一個
 * 指針維護調用鍊的最大子節點. 并且重寫了 clone() 方法, 進而在跨 Thread 的情況下實作值得深拷貝
 *
 * @author tameti
 */
public class CallSpan implements Cloneable {
    // 記錄目前節點軌迹
    private Span currentSpan;

    public CallSpan() {
        this(null);
    }

    public CallSpan(String span) {
        if (span == null) {
            currentSpan = new Span(0, null);
        } else {
            String[] spans = span.split("\\.");
            Span tmpSpan = null;
            for (String s : spans) {
                int v = Integer.parseInt(s);
                tmpSpan = new Span(v, tmpSpan);
            }
            currentSpan = tmpSpan;
        }
    }

    public Span createEntrySpan() {
        Span childSpan = currentSpan.childSpan;
        int value = 1;
        // 如果目前節點下不存在子節點, 那麼我們就直接建立一個 0 作為第一個子節點
        // 如果已經存在子節點了, 那麼我們就取這個棧中最後一個元素的值, 然後 +1 作為新的節點的值
        if (childSpan != null) {
            value = childSpan.value + 1;
        }

        // 接下來開始建立 Span 節點, 它的父親節點是我們的軌迹節點
        Span newSpan = new Span(value, currentSpan);
        currentSpan.childSpan = newSpan;
        // 最後修改軌迹的節點的指針
        currentSpan = newSpan;
        return newSpan;
    }

    public void exitSpan() {
        if (currentSpan != null) {
            // 1. 拿到軌迹節點的父親節點
            // 2. 然後修改軌迹節點的指針
            currentSpan = currentSpan.parentSpan;
        }
    }

    public Span getCurrentSpan() {
        return currentSpan;
    }


    @Override
    protected CallSpan clone() {
        return new CallSpan(currentSpan.toString());
    }

    public static class Span {
        private int value;
        private Span parentSpan;
        private Span childSpan;

        private Span(int value, Span parentSpan) {
            this.value = value;
            this.parentSpan = parentSpan;
        }

        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append(value);
            Span currentSpan = parentSpan;
            while (currentSpan != null) {
                sb.append(".").append(currentSpan.value);
                currentSpan = currentSpan.parentSpan;
            }
            return sb.reverse().toString();
        }
    }
}
           

二者實作的功能是一樣的. 但 CallSpan 占用的空間明顯要低于 TreeSpan. 現在我們要将 CallSpan 與 alibaba 的 TransmittableThreadLocal 進行一個整合. 進而實作跨線程的調用鍊共享. 由于異步方法執行速度快慢不同, 在跨線程時, 不能使用預設的資料淺拷貝. 因為主線程方法已經執行完畢了, 異步線程方法可能還沒開始執行. 如果此時二者共用同一個調用鍊, 主線程方法執行 CallSpan.exitSpan() 後, 異步線程才開始執行 CallSpan.createEntrySpan(). 這樣得到的順序明顯是錯誤的. 為了解決這樣的問題, 我們需要實作跨線程的資料深拷貝. 代碼如下:

package org.example.call;

import com.alibaba.ttl.TransmittableThreadLocal;

/**
 * 基于 alibaba 的 TransmittableThreadLocal 實作的線程級别的容器類.
 * 當跨越線程時, 會針對 CallSpan 類型的資料做深拷貝操作, 其他類型的
 * 資料做淺拷貝操作
 * 
 * @author tameti
 */
public class CallSpanThreadLocal<T> extends TransmittableThreadLocal<T> {

    @Override
    @SuppressWarnings("unchecked")
    public T copy(T parentSpan) {
        // 隻針對 CallSpan 對深拷貝, 針對其他的值做淺拷貝
        if (parentSpan instanceof CallSpan) {
            return (T) ((CallSpan) parentSpan).clone();
        }
        return parentSpan;
    }

    @Override
    @SuppressWarnings("unchecked")
    protected T childValue(T parentSpan) {
        // 隻針對 CallSpan 對深拷貝, 針對其他的值做淺拷貝
        if (parentSpan instanceof CallSpan) {
            return (T) ((CallSpan) parentSpan).clone();
        }
        return parentSpan;
    }
}
           

然後基于 CallspanThreadLocal 編寫一個調用鍊上下文對象. 

package org.example.call;

import com.codetool.common.SnowFlakeHelper;

/**
 * 調用鍊上下文對象
 *
 * @author tameti
 */
public class CallContext {
    private static CallSpanThreadLocal<CallSpan> context = new CallSpanThreadLocal<>();
    private static CallSpanThreadLocal<String> traceContext = new CallSpanThreadLocal<>();

    public static CallSpan.Span createEntrySpan(String span) {
        CallSpan callSpan = context.get();
        if (callSpan == null) {
            callSpan = new CallSpan(span);
            context.set(callSpan);
            return callSpan.getCurrentSpan();
        }
        return callSpan.createEntrySpan();
    }

    public static void exitSpan() {
        CallSpan callSpan = context.get();
        if (callSpan != null) {
            callSpan.exitSpan();
        }
    }

    public static CallSpan.Span getCurrentSpan() {
        CallSpan callSpan = context.get();
        if (callSpan == null) {
            return null;
        }
        return callSpan.getCurrentSpan();
    }

    public static void setTrace(String trace) {
        if (trace == null) {
            trace = SnowFlakeHelper.getInstance(100).nextId() + "";
        }
        traceContext.set(trace);
    }

    public static String getTrace() {
        return traceContext.get();
    }

    public static void main(String[] args) {
        CallContext.createEntrySpan(null);
        CallContext.exitSpan();
    }
}
           

由于 trace 在一個系統中隻會進行一次指派, 是以我把 trace 和 CallSpan 分離管理了. 但是他們都是通過同一個類的接口來進行調用.  現在整個調用鍊的通用代碼如下, 方法開始執行前, 調用:

// 是否存在傳遞過來的 spanValue
CallSpan.Span span = CallContext.createEntrySpan(spanValue);
CallContext.setTrace(traceValue);
           

方法執行完畢後調用:

CallContext.exitSpan();
           

接下來我們隻需要在 javaagent 裡面把這兩行代碼插進去就可以了.  好的, 整個調用鍊就寫完了....

就這, 就這. 就這? 就結束了?

基于 javaagent + javassist 一步步實作調用鍊系統 (2)

這裡已經完成了最基本的調用鍊追蹤, 接下來要配合資訊收集. 從最外層開始收集, 知道最内層. 也就是 servlet -> web -> service -> jdbc. 寫了很多之後發現, 其實有很多通用的部分, 于是基于面向對象的設計原理, 首先抽象出一個接口, 然後巴拉巴拉巴拉..., 最終結構圖如下:

基于 javaagent + javassist 一步步實作調用鍊系統 (2)

首先有一個最大的接口 FilterChain, 這個名字取得有點随意. 裡面存在兩個方法

package org.example.filter;

import javassist.CtClass;

/**
 * 這個接口的作用是濾我們需要代理的目标類
 *
 * @author tameti
 */
public interface FilterChain {

    /**
     * 是否是需要代理的目标類
     *
     * @param className 類的全路徑名稱
     * @param ctClass   類的位元組碼對象
     * @return
     */
    boolean isTargetClass(String className, CtClass ctClass);

    /**
     * 具體的代理邏輯方法入口
     *
     * @param loader    目前類的類加載器
     * @param ctClass   類的位元組碼對象
     * @param className 類的全路徑名稱
     * @return 傳回處理後的位元組數組
     */
    byte[] processingAgentClass(ClassLoader loader, CtClass ctClass, String className) throws Exception;

}
           

通過 isTargetClass 來判斷是否為我們需要處理的類, 如果傳回 true, 則調用 processingAgentClass 方法進行具體的增強處理.

然後是 AbstractFilterChain

package org.example.filter;

import com.codetool.common.FileHelper;
import com.codetool.common.MethodHelper;
import javassist.CtMethod;
import org.example.call.CallContext;
import org.example.call.pojo.BaseCall;

import java.io.File;
import java.util.Map;

/**
 * 這個類對攔截的方法的具體執行邏輯做了一系列的抽象.
 *
 * @author tameti
 */
public abstract class AbstractFilterChain implements FilterChain {

    /**
     * 增強的方法執行之前執行這個方法, 它會接收一個空的 Map 用來記錄方法的運作資訊.
     * 然後接收原來的方法裡面的所有參數, 并且封裝為一個 Object[] 數組.最終傳回一個對象
     * 用于替換原來方法執行時産生的 result
     *
     * @param context 上下文對象
     * @return 傳回一個用于替換原來的傳回結果的對象, 如果為 null, 則不替換原來的傳回對象
     */
    public void before(BaseCall context) {

    }

    /**
     * 增強的方法執行之後執行這個方法, 它會接收 before 這個方法傳回的 Map 作為第一個參數, 然後接收
     * 原來的方法的所有參數, 将它封裝為一個 Object[] 數組, 最後一個參數接收原來的方法的傳回值. 最終
     * 這個方法也可以傳回一個 Object 對象用于替換傳回值.
     *
     * @param context 上下文對象
     * @return 傳回一個用于替換原來的傳回結果的對象, 如果為 null, 則不替換原來的傳回對象
     */
    public void after(BaseCall context) {

    }

    /**
     * 增強的方法出現異常後會執行這個方法, 它會接收 before 方法傳回的 Map 作為第一個參數, 然後接收原來的
     * 方法中的所有具體的實參, 并且封裝為一個 Object[] 數組, 最後一個參數是這個方法運作時抛出的異常資訊.
     * 最終這個方法也可以傳回一個 Object 對象用于替換傳回值.
     *
     * @param context 上下文對象
     * @return 傳回一個用于替換原來的傳回結果的對象, 如果為 null, 則不替換原來的傳回對象
     */
    public void throwing(BaseCall context) {

    }

    /**
     * 增強的方法最終會執行的這個方法, 執行順序不同, 和 org.example.filter.AbstractFilterChain.after(Map<\String,Object>, Object[], Object) 差不多
     *
     * @param context 上下文對象
     * @return 傳回一個用于替換原來的傳回結果的對象, 如果為 null, 則不替換原來的傳回對象
     */
    public void finale(BaseCall context) {
        CallContext.exitSpan();
        String data = context.getData();
        FileHelper.append(data, new File("D:\\tmp\\data\\" + context.trace + ".call"));
    }

    /**
     * 渲染參數清單
     *
     * @param method 位元組碼方法
     */
    protected String renderParamNames(CtMethod method) {
        String[] variables = MethodHelper.getVariables(method);
        if (variables.length > 0) {
            StringBuilder sb = new StringBuilder("{");
            for (int i = 0; i < variables.length; i++) {
                sb.append("\"").append(variables[i]).append("\"");
                if (i != variables.length - 1) {
                    sb.append(",");
                }
            }
            sb.append("}");
            return "new String[] " + sb.toString();
        }
        return null;
    }

    /**
     * 判斷方法是否為需要處理的方法
     *
     * @param m
     * @return
     */
    protected boolean processed(CtMethod m) {
        int modifiers = m.getModifiers();
        if (!java.lang.reflect.Modifier.isPublic(modifiers)) {
            return false;
        }

        if (java.lang.reflect.Modifier.isStatic(modifiers)) {
            return false;
        }

        return !java.lang.reflect.Modifier.isNative(modifiers);
    }

}
           

我們會将一些需要采集的資訊封裝到 BaseCall 這個對象中 (如果不确定需要采集的資訊的模闆的話,可以直接傳入一個 Map 進去) , 然後傳遞下去. 定義 before, after, throwing, finale 這幾個方法來表示方法執行前的具體含義. 

關于 BaseCall 的定義如下:

package org.example.call.pojo;

import com.codetool.common.SnowFlakeHelper;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

public class BaseCall implements Serializable {
    // 主鍵 ID
    public Long id = SnowFlakeHelper.getInstance(1000).nextId();

    // 調用鍊的類型, SERVLET | WEB | SERVICE | JDBC
    public String type;

    // 調用鍊的唯一辨別, 同樣的 trace 表示是同一次調用會話
    public String trace;

    // 方法位于的調用層級
    public String span;

    // 方法抛出的異常資訊
    public String error;

    // 方法執行的開始時間
    public long startTime;

    // 方法執行的結束時間
    public long endTime;

    // 方法運作耗時
    public long useTime;

    // 方法執行的線程
    public String thread;

    // 運作傳回的結果
    public String result;

    // 方法所在的類的名稱
    public String className;

    // 方法的名稱
    public String methodName;

    // 上下文對象, 用于存儲一些擴充的資料. 比如: 參數名稱, 參數值...
    public Map<String, Object> context = new HashMap<>();

    // 必須要由子類進行實作
    public String getData() {
        throw new IllegalArgumentException("方法必須子類實作");
    }
}
           

Servlet 日志收集

存放 servlet 日志資訊的表

CREATE TABLE `call_servlet`(
	`id` 		    bigint primary key auto_increment comment '主鍵ID',
	`trace` 	    varchar(50) 	not null comment '調用鍊辨別, 同樣的 調用鍊辨別 表示同一次會話',
	`span` 		    varchar(3000)   not null comment '層級 ID, 表示這個方法的運作位于這個調用鍊中的位置. 如果是 0 則表示是調用鍊發起端',
	`session`	    varchar(50) 	not null comment '會話辨別',
	`address`       varchar(100) 	not null comment '發起這次請求的用戶端位址資訊',
	`url`		    varchar(3000)   not null comment '請求連結, 盡量控制在 3000 字以内',
	`method`	    varchar(15)		not null comment '請求類型, 例: GET, POST, PUT, DELETE, TRACE, OPTIONS',
	`params`	    varchar(3000)   not null comment '請求攜帶參數資訊',
	`header`	    varchar(3000)	not null comment '請求頭資訊',
	`cookies`	    varchar(3000)   not null comment '請求的 cookie 資訊',
	`thread`	    varchar(50)		not null comment '處理的線程的資訊',
	`status`	    int				not null comment '請求響應狀态',
	`start_time`    bigint 		    not null comment '請求發起開始時間',
	`end_time`	    bigint 		    not null comment '請求結束的時間',
	`use_time`	    bigint 		    not null comment '請求消耗時間',
	`error`		    varchar(3000)	not null comment '異常描述資訊'
) ENGINE=INNODB DEFAULT CHARSET='UTF8' COMMENT 'servlet 層的 APM 監控';

           

基于 BaseCall 下的實作類 CallServlet

package org.example.call.pojo;

public class CallServlet extends BaseCall {
    public String session;
    public String address;
    public String url;
    public String method;
    public String params;
    public String header;
    public String cookies;
    public Integer status;

    // 必須要由子類進行實作
    public String getData() {
        return "INSERT INTO `call_servlet`(" + "`id`, `trace`, `span`, `session`, " +
                "`address`, `url`, `method`, `params`," +
                "`header`, `cookies`, `thread`, `status`," +
                "`start_time`, `end_time`, `use_time`, `error`) VALUES(" +
                id + ", \"" + trace + "\", \"" + span + "\", \"" + session + "\", " +
                "\"" + address + "\", \"" + url + "\", \"" + method + "\", '" + params + "', " +
                "'" + header + "', '" + cookies + "', \"" + thread + "\", " + status + ", " +
                startTime + "," + endTime + "," + useTime + ", \"" + error + "\");";
    }
}
           

方法代理模闆類: 後續 web 和 service 都會使用到這個類. 當對類中的某些方法進行插樁時, 會從原來的方法中拷貝出一個新的方法, 名字叫做: 方法名稱$agent, 然後修改原來方法的方法體, 大緻原理如下:

基于 javaagent + javassist 一步步實作調用鍊系統 (2)

原始的 m1方法中的執行邏輯隻是簡單的列印 run... , 現在我要基于 m1 拷貝出一個 m1$agent 方法, 然後修改 m1 中的方法體, 将方法體修改為我們的邏輯, 在中間插入原來需要執行的方法的拷貝. 這樣在調用 m1 方法的時候, 既可以完成插樁, 也不會影響原來的方法的運作邏輯. 

package org.example.context;

import org.example.call.pojo.BaseCall;

/**
 * javassist 方法增強的模闆引擎, 裡面将方法代理的正常邏輯整理了出來
 *
 * @auhtor tameti
 */
public class BaseTemplate {
    public BaseCall context;


    /**
     * {
     *     Object result = null;
     *     org.example.call.pojo.BaseCall context = new org.example.call.pojo.BaseCall();
     *     context.type = "Controller";
     *     context.startTime = 111110;
     *     context.className = "org.example.controller.DemoController";
     *     context.thread = "pool-1-exec-0";
     *
     *     try {
     *
     *
     * @param sb
     */
    protected void renderStart(StringBuilder sb) {
        String callType = context.context.get("CallType").toString();

        sb.append("{").append('\n');
        sb.append("    Object result = null;").append('\n');
        sb.append("    org.example.call.pojo.BaseCall context = new ").append(callType).append("();\n");        
        sb.append("    context.type = \"").append(context.type).append("\";\n");
        sb.append("    context.startTime =  System.currentTimeMillis();\n");
        sb.append("    context.className = \"").append(context.className).append("\";\n");
        sb.append("    context.thread = Thread.currentThread().getName();\n");
        sb.append("    context.methodName = \"").append(context.methodName).append("\";\n");

        if (context.context.get("names") != null) {
            sb.append("    context.context.put(\"names\",").append(context.context.get("names")).append(");\n");
            sb.append("    context.context.put(\"values\", $args);\n");
        }
        sb.append("    try {\n");
    }

    /**
     *     ${instance}.before(context);
     *     result = ${method}$agent($$);
     *     context.context.put("result", result);
     *     context.result = com.codetool.common.JsonHelper.stringify(result);
     *     ${instance}.after(context);
     *
     * @param sb
     */
    protected void renderCenter(StringBuilder sb) {
        String instance = context.context.get("instance").toString();
        sb.append("        ").append(instance).append(".before(context);\n");
        sb.append("        result = ").append(context.methodName).append("$agent($$);\n");
        sb.append("        context.context.put(\"result\", result);\n");
        sb.append("        context.result = com.codetool.common.JsonHelper.stringify(result);\n");
        sb.append("        ").append(instance).append(".after(context);\n");
    }


    /**
     *
     * } catch (Throwable e) {
     *     context.error = e.getMessage();
     *     ${instance}.throwing(context);
     * } finally {
     *     context.endTime = System.currentTimeMillis();
     *     context.useTime = context.endTime - context.startTime;
     *     ${instance}.finale(context);
     *     return ($r) context.context.get("result");
     * }
     *
     * @param sb
     */
    protected void renderEnd(StringBuilder sb) {
        String instance = context.context.get("instance").toString();
        sb.append("    } catch (Throwable e) {\n");
        sb.append("        context.error = e.getMessage();\n");
        sb.append("        ").append(instance).append(".throwing(context);\n");
        sb.append("    } finally {\n");
        sb.append("        context.endTime = System.currentTimeMillis();\n");
        sb.append("        context.useTime = context.endTime - context.startTime;\n");
        sb.append("        ").append(instance).append(".finale(context);\n");
        sb.append("    }\n");
    }

    protected void renderReturn(StringBuilder sb) {
        sb.append("    return ($r) context.context.get(\"result\");\n");
    }



    protected void renderFinally(StringBuilder sb) {
        sb.append("}\n");
    }

    public String render() {
        StringBuilder sb = new StringBuilder();
        renderStart(sb);
        renderCenter(sb);
        renderEnd(sb);
        renderReturn(sb);
        renderFinally(sb);
        return sb.toString();
    }
}
           

BaseTemplate  存在一個 VoidTemplate 實作, 用來處理方法不存在傳回值的情況

package org.example.context;

public class VoidTemplate extends BaseTemplate {

    @Override
    protected void renderCenter(StringBuilder sb) {
        String instance = context.context.get("instance").toString();
        sb.append("        ").append(instance).append(".before(context);\n");
        sb.append("        ").append(context.methodName).append("$agent($$);\n");
        sb.append("        ").append(instance).append(".after(context);\n");
    }

    @Override
    protected void renderReturn(StringBuilder sb) {
        // 啥也不做...
    }
}
           

提供一個使用這兩個類的入口類

package org.example.context;

import java.util.HashMap;
import java.util.Map;

public class TemplateFactory {
    private static Map<Boolean, BaseTemplate> TEMPLATE_MAP = new HashMap<>();
    static {
        TEMPLATE_MAP.put(true, new BaseTemplate());
        TEMPLATE_MAP.put(false, new VoidTemplate());
    }

    public static BaseTemplate getTemplate(boolean mode) {
        return TEMPLATE_MAP.get(mode);
    }
}
           

截取 HttpServlet 日志資訊的類. HttpServletFilterChain

package org.example.filter.support;

import com.codetool.common.JsonHelper;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import org.example.call.CallContext;
import org.example.call.CallSpan;
import org.example.call.pojo.BaseCall;
import org.example.call.pojo.CallServlet;
import org.example.context.BaseTemplate;
import org.example.context.TemplateFactory;
import org.example.filter.AbstractFilterChain;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * 針對 HttpServlet 的請求攔截
 *
 * @author tameti
 */
public class HttpServletFilterChain extends AbstractFilterChain {

    /**
     * 暴露執行個體出去
     */
    public static HttpServletFilterChain INSTANCE = new HttpServletFilterChain();

    /**
     * web 入口
     */
    public static String servletClassName = "javax.servlet.http.HttpServlet";

    /**
     * 擷取請求頭資訊
     *
     * @param request
     * @return
     */
    private Map<String, String> getHeader(HttpServletRequest request) {
        Enumeration<String> headers = request.getHeaderNames();
        Map<String, String> map = new HashMap<>();
        while (headers.hasMoreElements()) {
            String h = headers.nextElement();
            String v = request.getHeader(h);
            map.put(h, v);
        }
        return map;
    }

    /**
     * 擷取響應頭資訊
     *
     * @param response
     * @return
     */
    private Map<String, String> getHeader(HttpServletResponse response) {
        Collection<String> headers = response.getHeaderNames();
        Map<String, String> map = new HashMap<>();
        headers.forEach(e -> map.put(e, response.getHeader(e)));
        return map;
    }

    @Override
    public void before(BaseCall context) {
        Object[] paramValues = (Object[]) context.context.get("values");
        HttpServletRequest request = (HttpServletRequest) paramValues[0];

        // 擷取 span 和 trace
        String spanValue = request.getHeader("span");
        String traceValue = request.getHeader("trace");
        CallSpan.Span span = CallContext.createEntrySpan(spanValue);
        CallContext.setTrace(traceValue);


        String session = request.getSession().getId();
        String address = request.getRemoteAddr();
        String url = request.getRequestURI();

        // 請求頭資訊
        Map<String, String> header = getHeader(request);

        // 請求的方法類型
        String method = request.getMethod();
        CallServlet servlet = (CallServlet) context;
        servlet.trace = CallContext.getTrace();
        servlet.span = span.toString();
        servlet.session = session;
        servlet.address = address;
        servlet.url = url;
        servlet.method = method;
        servlet.params = JsonHelper.stringify(request.getParameterMap());
        servlet.header = JsonHelper.stringify(header);
        servlet.cookies = JsonHelper.stringify(request.getCookies());
        servlet.thread = Thread.currentThread().getName();
    }


    @Override
    public void finale(BaseCall context) {
        CallServlet servlet = (CallServlet) context;
        Object[] paramValues = (Object[]) context.context.get("values");
        HttpServletResponse response = (HttpServletResponse) paramValues[1];
        servlet.status = response.getStatus();

        super.finale(servlet);
    }

    @Override
    public boolean isTargetClass(String className, CtClass ctClass) {
        return servletClassName.equals(className);
    }


    @Override
    public byte[] processingAgentClass(ClassLoader loader, CtClass ctClass, String className) throws Exception {
        String methodName = "service";
        CtMethod service = ctClass.getMethod(methodName, "(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V");

        BaseCall context = new BaseCall();
        context.type = "SERVLET";
        context.className = servletClassName;
        context.context.put("CallType", "org.example.call.pojo.CallServlet");
        context.methodName = methodName;
        context.context.put("instance", "org.example.filter.support.HttpServletFilterChain.INSTANCE");
        context.context.put("names", "new String[] {\"req\", \"resp\"}");
        CtMethod service$agent = CtNewMethod.copy(service, context.methodName + "$agent", ctClass, null);
        ctClass.addMethod(service$agent);

        BaseTemplate template = TemplateFactory.getTemplate(service.getReturnType() != CtClass.voidType);
        template.context = context;
        String templateValue = template.render();
        System.out.println(templateValue);
        service.setBody(templateValue);
        return ctClass.toBytecode();
    }


}
           

這裡我們就完成了針對 Servlet 的攔截, 接下來在 premain 中使用它.

package org.example;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.LoaderClassPath;
import org.apache.commons.lang3.StringUtils;
import org.example.context.VMInfo;
import org.example.filter.FilterChain;
import org.example.filter.support.HttpServletFilterChain;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class AgentApplication implements ClassFileTransformer {
    private final List<FilterChain> chains = new ArrayList<>();
    private static final byte[] NO_TRANSFORM = null;

    private AgentApplication() {
        chains.add(new HttpServletFilterChain());
    }


    public static void premain(String args, Instrumentation instrumentation) {
        boolean showVm = false;
        if (StringUtils.isNoneBlank(args) && args.equals("debug")) {
            showVm = true;
        }

        // 開始 JVM 監控, 這一部分是上一章介紹 javagent 配合列印 gc 日志使用的. 可以不用要 
        if (showVm) {
            // 開啟一個線程
            // 每隔 1分鐘 執行一次
            Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> {
                VMInfo.memoryInfo();
                VMInfo.gcInfo();
                System.out.println();
            }, 0, 60, TimeUnit.SECONDS);
        }

        System.out.println("          _____                   _______                   _____                    _____                _____                    _____                    _____");
        System.out.println("         /\\    \\                 /::\\    \\                 /\\    \\                  /\\    \\              /\\    \\                  /\\    \\                  /\\    \\ ");
        System.out.println("        /::\\____\\               /::::\\    \\               /::\\____\\                /::\\    \\            /::\\    \\                /::\\    \\                /::\\    \\ ");
        System.out.println("       /::::|   |              /::::::\\    \\             /::::|   |                \\:::\\    \\           \\:::\\    \\              /::::\\    \\              /::::\\    \\ ");
        System.out.println("      /:::::|   |             /::::::::\\    \\           /:::::|   |                 \\:::\\    \\           \\:::\\    \\            /::::::\\    \\            /::::::\\    \\ ");
        System.out.println("     /::::::|   |            /:::/~~\\:::\\    \\         /::::::|   |                  \\:::\\    \\           \\:::\\    \\          /:::/\\:::\\    \\          /:::/\\:::\\    \\ ");
        System.out.println("    /:::/|::|   |           /:::/    \\:::\\    \\       /:::/|::|   |                   \\:::\\    \\           \\:::\\    \\        /:::/__\\:::\\    \\        /:::/__\\:::\\    \\ ");
        System.out.println("   /:::/ |::|   |          /:::/    / \\:::\\    \\     /:::/ |::|   |                   /::::\\    \\          /::::\\    \\      /::::\\   \\:::\\    \\      /::::\\   \\:::\\    \\ ");
        System.out.println("  /:::/  |::|___|______   /:::/____/   \\:::\\____\\   /:::/  |::|   | _____    ____    /::::::\\    \\        /::::::\\    \\    /::::::\\   \\:::\\    \\    /::::::\\   \\:::\\    \\ ");
        System.out.println(" /:::/   |::::::::\\    \\ |:::|    |     |:::|    | /:::/   |::|   |/\\    \\  /\\   \\  /:::/\\:::\\    \\      /:::/\\:::\\    \\  /:::/\\:::\\   \\:::\\    \\  /:::/\\:::\\   \\:::\\____\\ ");
        System.out.println("/:::/    |:::::::::\\____\\|:::|____|     |:::|    |/:: /    |::|   /::\\____\\/::\\   \\/:::/  \\:::\\____\\    /:::/  \\:::\\____\\/:::/__\\:::\\   \\:::\\____\\/:::/  \\:::\\   \\:::|    |");
        System.out.println("\\::/    / ~~~~~/:::/    / \\:::\\    \\   /:::/    / \\::/    /|::|  /:::/    /\\:::\\  /:::/    \\::/    /   /:::/    \\::/    /\\:::\\   \\:::\\   \\::/    /\\::/   |::::\\  /:::|____|");
        System.out.println(" \\/____/      /:::/    /   \\:::\\    \\ /:::/    /   \\/____/ |::| /:::/    /  \\:::\\/:::/    / \\/____/   /:::/    / \\/____/  \\:::\\   \\:::\\   \\/____/  \\/____|:::::\\/:::/    /");
        System.out.println("             /:::/    /     \\:::\\    /:::/    /            |::|/:::/    /    \\::::::/    /           /:::/    /            \\:::\\   \\:::\\    \\            |:::::::::/    /");
        System.out.println("            /:::/    /       \\:::\\__/:::/    /             |::::::/    /      \\::::/____/           /:::/    /              \\:::\\   \\:::\\____\\           |::|\\::::/    /");
        System.out.println("           /:::/    /         \\::::::::/    /              |:::::/    /        \\:::\\    \\           \\::/    /                \\:::\\   \\::/    /           |::| \\::/____/");
        System.out.println("          /:::/    /           \\::::::/    /               |::::/    /          \\:::\\    \\           \\/____/                  \\:::\\   \\/____/            |::|  ~|");
        System.out.println("         /:::/    /             \\::::/    /                /:::/    /            \\:::\\    \\                                    \\:::\\    \\                |::|   |");
        System.out.println("        /:::/    /               \\::/____/                /:::/    /              \\:::\\____\\                                    \\:::\\____\\               \\::|   |");
        System.out.println("        \\::/    /                 ~~                      \\::/    /                \\::/    /                                     \\::/    /                \\:|   |");
        System.out.println("         \\/____/                                           \\/____/                  \\/____/                                       \\/____/                  \\|___|");
        System.out.println();
        instrumentation.addTransformer(new AgentApplication());
    }


    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
            // 如果目前類名稱是 null, 則直接傳回 null
            if (className == null) {
                return NO_TRANSFORM;
            }
            String finalClassName = className.replace("/", ".");

            ClassPool pool = new ClassPool(true);
            if (loader != null) {
                pool.insertClassPath(new LoaderClassPath(loader));
            } else {
                pool.insertClassPath(new LoaderClassPath(ClassLoader.getSystemClassLoader()));
            }

            for (FilterChain chain : chains) {
                CtClass sourceClass = pool.getCtClass(finalClassName);
                if (chain.isTargetClass(finalClassName, sourceClass)) {
                    System.err.println("嘗試對類: " + className + " 進行增強");
                    return chain.processingAgentClass(loader, sourceClass, className);
                }
            }

        } catch (Exception e) {
            // System.out.println("無法對類 " + className + " 進行增強, 具體的錯誤原因是: " + e.toString());
        }

        return NO_TRANSFORM;
    }


}
           

現在, 對項目進行打包, 然後啟動一個 springboot 并且攜帶javaagent參數, 測試一個 web 請求, 此時 web 請求處理完畢後. 我的 D:/tmp/data 目錄下會建立出一個以 traceId 作為檔案名的 .call 字尾檔案, 裡面的内容是一條 sql 語句. 裡面記錄了關于 servlet 采集的資訊

基于 javaagent + javassist 一步步實作調用鍊系統 (2)
基于 javaagent + javassist 一步步實作調用鍊系統 (2)

為了避免影響業務系統的效率, 調用鍊系統隻是負責生成sql語句, 然後存在另外一個系統來處理這些 sql 語句.  補充一點: 在給 traceId 命名時附帶一些其他資訊可以更加友善我們進行統計, 比如 alibaba 的鷹眼的全局唯一的ID

基于 javaagent + javassist 一步步實作調用鍊系統 (2)

Controller 層采集

controller 層的采集和 Servlet 層類似, 不同之處在于, 怎樣确定這個位元組碼類是 Controller 類, 這裡我是通過判斷這個類是否攜帶了 RestController 或 Controller 注解來判斷這個類是否屬于 controller 類.

定義一個采集資訊的實體類 CallWeb, 它繼承自 BaseCall. 還有資料表:

CREATE TABLE `call_web`(
	`id` 				    bigint primary key auto_increment comment '主鍵ID',
	`trace` 		        varchar(50) 	not null comment '調用鍊辨別, 同樣的 調用鍊辨別 表示同一次會話',
	`span` 			        varchar(3000)   not null comment '層級 ID, 表示這個方法的運作位于這個調用鍊中的位置. 如果是 0 則表示是調用鍊發起端',
	`result`		        varchar(3000) 	not null comment '請求響應的結果, 可以是JSON, 也可以是頁面',
	`class_name` 		    varchar(100) 	not null comment '處理的 web 類名稱',
	`method_name`		    varchar(3000)   not null comment '具體負責處理的方法的名稱',
	`thread`		        varchar(50)		not null comment '處理的線程的資訊',
	`start_time`	        bigint 		    not null comment '請求發起開始時間',
	`end_time`		        bigint 		    not null comment '請求結束的時間',
	`use_time`		        bigint 		    not null comment '請求消耗時間',
	`error`		            varchar(3000)	not null comment '異常描述資訊'
) ENGINE=INNODB DEFAULT CHARSET='UTF8' COMMENT 'controller 層的 APM 監控';
           
package org.example.call.pojo;


public class CallWeb extends BaseCall {

    @Override
    public String getData() {
        return "INSERT INTO `call_web`(" + "`id`, `trace`, `span`, `result`, " +
                "`class_name`, `method_name`, `thread`, `start_time`, " +
                "`end_time`, `use_time`, `error`) VALUES(" +
                id + ", \"" + trace + "\", \"" + span + "\", '" + result + "', " +
                "\"" + className + "\", \"" + methodName + "\", \"" + thread + "\", " +
                startTime + "," + endTime + "," + useTime + ",\"" + error + "\");";
    }

}
           

定義具體的 Controller 插樁類

package org.example.filter.support;

import com.codetool.common.AnnotationHelper;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import org.example.call.CallContext;
import org.example.call.CallSpan;
import org.example.call.pojo.BaseCall;
import org.example.context.BaseTemplate;
import org.example.context.TemplateFactory;
import org.example.filter.AbstractFilterChain;

public class SpringControllerFilterChain extends AbstractFilterChain {
    public static SpringControllerFilterChain INSTANCE = new SpringControllerFilterChain();
    
    // 如果類上包含這兩個注解中的任意一個則進行插樁
    private static final String[] controllerAnnotations = {
            "@org.springframework.web.bind.annotation.RestController",
            "@org.springframework.stereotype.Controller"
    };

    // 如果方法上包含 6 個注解中的任意一個則進行插樁
    private static final String[] mappingAnnotations = {
            "@org.springframework.web.bind.annotation.RequestMapping",
            "@org.springframework.web.bind.annotation.GetMapping",
            "@org.springframework.web.bind.annotation.PostMapping",
            "@org.springframework.web.bind.annotation.PutMapping",
            "@org.springframework.web.bind.annotation.DeleteMapping",
            "@org.springframework.web.bind.annotation.PatchMapping"
    };


    @Override
    public void before(BaseCall context) {
        CallSpan.Span span = CallContext.createEntrySpan(null);
        context.trace = CallContext.getTrace();
        context.span = span.toString();
    }


    @Override
    public boolean isTargetClass(String className, CtClass ctClass) {
        // 不處理 BasicErrorController 類
        if (className.equals("org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController")) {
            return false;
        }

        // 不處理注解
        if (!ctClass.isAnnotation()) {
            try {
                for (String controllerAnnotation : controllerAnnotations) {
                    String annotationValue = AnnotationHelper.getAnnotationValue(ctClass.getAnnotations(), controllerAnnotation);
                    if (annotationValue != null) {
                        return true;
                    }
                }
            } catch (ClassNotFoundException e) {
                // System.err.println(e.getMessage());
            }
        }
        return false;
    }


    @Override
    public byte[] processingAgentClass(ClassLoader loader, CtClass ctClass, String className) throws Exception {
        CtMethod[] methods = ctClass.getMethods();

        for (CtMethod method : methods) {
            if (!processed(method)) {
                continue;
            }

            boolean status = false;

            // 必須包含指定的注解
            for (String annotation : mappingAnnotations) {
                // 反複與運算, 隻要包含一個 Mapping 注解. 那麼這個方法就是我們需要處理的方法
                status = status || AnnotationHelper.getAnnotationValue(method.getAnnotations(), annotation) != null;
            }

            // 如果不包含指定的注解, 那麼就不處理這個方法
            if (!status) {
                continue;
            }

            String methodName = method.getName();

            BaseCall context = new BaseCall();
            context.type = "CONTROLLER";
            context.className = className;
            context.methodName = methodName;
            context.context.put("CallType", "org.example.call.pojo.CallWeb");
            context.context.put("instance", "org.example.filter.support.SpringControllerFilterChain.INSTANCE");
            context.context.put("names", renderParamNames(method));

            ctClass.addMethod(CtNewMethod.copy(method, methodName + "$agent", ctClass, null));
            BaseTemplate baseTemplate = TemplateFactory.getTemplate(method.getReturnType() != CtClass.voidType);
            baseTemplate.context = context;
            method.setBody(baseTemplate.render());
        }

        return ctClass.toBytecode();
    }

}
           

然後在 AgentApplication 中添加這個 Controller 插樁類

package org.example;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.LoaderClassPath;
import org.apache.commons.lang3.StringUtils;
import org.example.context.VMInfo;
import org.example.filter.FilterChain;
import org.example.filter.support.HttpServletFilterChain;
import org.example.filter.support.SpringControllerFilterChain;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class AgentApplication implements ClassFileTransformer {
    private final List<FilterChain> chains = new ArrayList<>();
    private static final byte[] NO_TRANSFORM = null;

    private AgentApplication() {
        chains.add(new HttpServletFilterChain());
        chains.add(new SpringControllerFilterChain());
    }


    public static void premain(String args, Instrumentation instrumentation) {
        boolean showVm = false;
        if (StringUtils.isNoneBlank(args) && args.equals("debug")) {
            showVm = true;
        }

        // 開始 JVM 監控
        if (showVm) {
            // 開啟一個線程
            // 每隔 1分鐘 執行一次
            Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> {
                VMInfo.memoryInfo();
                VMInfo.gcInfo();
                System.out.println();
            }, 0, 60, TimeUnit.SECONDS);
        }

        System.out.println("          _____                   _______                   _____                    _____                _____                    _____                    _____");
        System.out.println("         /\\    \\                 /::\\    \\                 /\\    \\                  /\\    \\              /\\    \\                  /\\    \\                  /\\    \\ ");
        System.out.println("        /::\\____\\               /::::\\    \\               /::\\____\\                /::\\    \\            /::\\    \\                /::\\    \\                /::\\    \\ ");
        System.out.println("       /::::|   |              /::::::\\    \\             /::::|   |                \\:::\\    \\           \\:::\\    \\              /::::\\    \\              /::::\\    \\ ");
        System.out.println("      /:::::|   |             /::::::::\\    \\           /:::::|   |                 \\:::\\    \\           \\:::\\    \\            /::::::\\    \\            /::::::\\    \\ ");
        System.out.println("     /::::::|   |            /:::/~~\\:::\\    \\         /::::::|   |                  \\:::\\    \\           \\:::\\    \\          /:::/\\:::\\    \\          /:::/\\:::\\    \\ ");
        System.out.println("    /:::/|::|   |           /:::/    \\:::\\    \\       /:::/|::|   |                   \\:::\\    \\           \\:::\\    \\        /:::/__\\:::\\    \\        /:::/__\\:::\\    \\ ");
        System.out.println("   /:::/ |::|   |          /:::/    / \\:::\\    \\     /:::/ |::|   |                   /::::\\    \\          /::::\\    \\      /::::\\   \\:::\\    \\      /::::\\   \\:::\\    \\ ");
        System.out.println("  /:::/  |::|___|______   /:::/____/   \\:::\\____\\   /:::/  |::|   | _____    ____    /::::::\\    \\        /::::::\\    \\    /::::::\\   \\:::\\    \\    /::::::\\   \\:::\\    \\ ");
        System.out.println(" /:::/   |::::::::\\    \\ |:::|    |     |:::|    | /:::/   |::|   |/\\    \\  /\\   \\  /:::/\\:::\\    \\      /:::/\\:::\\    \\  /:::/\\:::\\   \\:::\\    \\  /:::/\\:::\\   \\:::\\____\\ ");
        System.out.println("/:::/    |:::::::::\\____\\|:::|____|     |:::|    |/:: /    |::|   /::\\____\\/::\\   \\/:::/  \\:::\\____\\    /:::/  \\:::\\____\\/:::/__\\:::\\   \\:::\\____\\/:::/  \\:::\\   \\:::|    |");
        System.out.println("\\::/    / ~~~~~/:::/    / \\:::\\    \\   /:::/    / \\::/    /|::|  /:::/    /\\:::\\  /:::/    \\::/    /   /:::/    \\::/    /\\:::\\   \\:::\\   \\::/    /\\::/   |::::\\  /:::|____|");
        System.out.println(" \\/____/      /:::/    /   \\:::\\    \\ /:::/    /   \\/____/ |::| /:::/    /  \\:::\\/:::/    / \\/____/   /:::/    / \\/____/  \\:::\\   \\:::\\   \\/____/  \\/____|:::::\\/:::/    /");
        System.out.println("             /:::/    /     \\:::\\    /:::/    /            |::|/:::/    /    \\::::::/    /           /:::/    /            \\:::\\   \\:::\\    \\            |:::::::::/    /");
        System.out.println("            /:::/    /       \\:::\\__/:::/    /             |::::::/    /      \\::::/____/           /:::/    /              \\:::\\   \\:::\\____\\           |::|\\::::/    /");
        System.out.println("           /:::/    /         \\::::::::/    /              |:::::/    /        \\:::\\    \\           \\::/    /                \\:::\\   \\::/    /           |::| \\::/____/");
        System.out.println("          /:::/    /           \\::::::/    /               |::::/    /          \\:::\\    \\           \\/____/                  \\:::\\   \\/____/            |::|  ~|");
        System.out.println("         /:::/    /             \\::::/    /                /:::/    /            \\:::\\    \\                                    \\:::\\    \\                |::|   |");
        System.out.println("        /:::/    /               \\::/____/                /:::/    /              \\:::\\____\\                                    \\:::\\____\\               \\::|   |");
        System.out.println("        \\::/    /                 ~~                      \\::/    /                \\::/    /                                     \\::/    /                \\:|   |");
        System.out.println("         \\/____/                                           \\/____/                  \\/____/                                       \\/____/                  \\|___|");
        System.out.println();
        instrumentation.addTransformer(new AgentApplication());
    }


    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
            if (className == null) {
                return NO_TRANSFORM;
            }
            String finalClassName = className.replace("/", ".");

            ClassPool pool = new ClassPool(true);
            if (loader != null) {
                pool.insertClassPath(new LoaderClassPath(loader));
            } else {
                pool.insertClassPath(new LoaderClassPath(ClassLoader.getSystemClassLoader()));
            }

            for (FilterChain chain : chains) {
                CtClass sourceClass = pool.getCtClass(finalClassName);
                if (chain.isTargetClass(finalClassName, sourceClass)) {
                    System.err.println("嘗試對類: " + className + " 進行增強");
                    try {
                        return chain.processingAgentClass(loader, sourceClass, finalClassName);
                    } catch (Exception e) {
                        System.out.println("無法對類 " + className + " 進行增強, 具體的錯誤原因是: " + e.toString());
                    }
                }
            }

        } catch (Exception e) {
            // TODO ...
        }

        return NO_TRANSFORM;
    }


}
           

現在, 對項目進行打包, 然後啟動一個 springboot 并且攜帶javaagent參數, 測試一個 web 請求, 此時 web 請求處理完畢後. 看一下 ${trace}.call 的内容. 

INSERT INTO `call_web`(`id`, `trace`, `span`, `result`, `class_name`, `method_name`, `thread`, `start_time`, `end_time`, `use_time`, `error`) VALUES(1422454555353710592, "1422454555219492864", "0.1", '{"code":200,"data":[{"city":"江西省吉安市","areas":[{"id":84,"key":"江西省吉安市青原區","status":0},{"id":94,"key":"江西省吉安市萬安縣","status":0},{"id":90,"key":"江西省吉安市峽江縣","status":0},{"id":88,"key":"江西省吉安市永豐縣","status":0},{"id":86,"key":"江西省吉安市吉安縣","status":0},{"id":89,"key":"江西省吉安市新幹縣","status":0},{"id":92,"key":"江西省吉安市安福縣","status":0},{"id":93,"key":"江西省吉安市泰和縣","status":0},{"id":85,"key":"江西省吉安市井岡山市","status":0},{"id":95,"key":"江西省吉安市永新縣","status":0},{"id":83,"key":"江西省吉安市吉州區","status":0},{"id":87,"key":"江西省吉安市吉水縣","status":0},{"id":91,"key":"江西省吉安市遂川縣","status":0},{"id":82,"key":"江西省吉安市","status":0}]},{"city":"江西省九江市","areas":[{"id":105,"key":"江西省九江市柴桑區","status":0},{"id":109,"key":"江西省九江市廬山市","status":0},{"id":102,"key":"江西省九江市永修縣","status":1},{"id":97,"key":"江西省九江市武甯縣","status":1},{"id":98,"key":"江西省九江市濂溪區","status":0},{"id":104,"key":"江西省九江市湖口縣","status":0},{"id":103,"key":"江西省九江市彭澤縣","status":0},{"id":107,"key":"江西省九江市瑞昌市","status":0},{"id":106,"key":"江西省九江市都昌縣","status":1},{"id":100,"key":"江西省九江市共青城市","status":1},{"id":108,"key":"江西省九江市德安縣","status":0},{"id":96,"key":"江西省九江市","status":0},{"id":99,"key":"江西省九江市浔陽區","status":1},{"id":101,"key":"江西省九江市修水縣","status":1}]},{"city":"江西省南昌市","areas":[{"id":13,"key":"江西省南昌市","status":0},{"id":16,"key":"江西省南昌市青山湖區","status":0},{"id":15,"key":"江西省南昌市西湖區","status":0},{"id":21,"key":"江西省南昌市建立區","status":0},{"id":17,"key":"江西省南昌市青雲譜區","status":1},{"id":18,"key":"江西省南昌市安義縣","status":1},{"id":19,"key":"江西省南昌市進賢縣","status":0},{"id":14,"key":"江西省南昌市東湖區","status":0},{"id":20,"key":"江西省南昌市南昌縣","status":0}]},{"city":"江西省上饒市","areas":[{"id":29,"key":"江西省上饒市弋陽縣","status":0},{"id":24,"key":"江西省上饒市廣豐區","status":0},{"id":33,"key":"江西省上饒市婺源縣","status":0},{"id":35,"key":"江西省上饒市上饒經濟技術開發區","status":0},{"id":23,"key":"江西省上饒市廣信區","status":1},{"id":30,"key":"江西省上饒市橫峰縣","status":0},{"id":25,"key":"江西省上饒市信州區","status":0},{"id":31,"key":"江西省上饒市鄱陽縣","status":0},{"id":32,"key":"江西省上饒市萬年縣","status":0},{"id":27,"key":"江西省上饒市","status":0},{"id":26,"key":"江西省上饒市鉛山縣","status":0},{"id":34,"key":"江西省上饒市德興市","status":0},{"id":28,"key":"江西省上饒市玉山縣","status":1},{"id":22,"key":"江西省上饒市餘幹縣","status":0}]},{"city":"江西省宜春市","areas":[{"id":45,"key":"江西省宜春市上高縣","status":0},{"id":50,"key":"江西省宜春市樟樹市","status":0},{"id":47,"key":"江西省宜春市靖安縣","status":0},{"id":46,"key":"江西省宜春市宜豐縣","status":0},{"id":52,"key":"江西省宜春市宜經濟技術開發區","status":0},{"id":51,"key":"江西省宜春市高安市","status":0},{"id":41,"key":"江西省宜春市","status":0},{"id":44,"key":"江西省宜春市萬載縣","status":0},{"id":48,"key":"江西省宜春市銅鼓縣","status":0},{"id":42,"key":"江西省宜春市袁州區","status":0},{"id":49,"key":"江西省宜春市豐城市","status":0},{"id":43,"key":"江西省宜春市奉新縣","status":0}]},{"city":"江西省新餘市","areas":[{"id":40,"key":"江西省新餘市高新技術産業開發區","status":0},{"id":36,"key":"江西省新餘市","status":0},{"id":37,"key":"江西省新餘市渝水區","status":0},{"id":39,"key":"江西省新餘市仙女湖風景名勝區","status":0},{"id":38,"key":"江西省新餘市分宜縣","status":0}]},{"city":"江西省撫州市","areas":[{"id":2,"key":"江西省撫州市宜黃縣","status":0},{"id":3,"key":"江西省撫州市資溪縣","status":0},{"id":5,"key":"江西省撫州市金溪縣","status":0},{"id":8,"key":"江西省撫州市南城縣","status":0},{"id":12,"key":"江西省撫州市廣昌縣","status":0},{"id":10,"key":"江西省撫州市黎川縣","status":0},{"id":4,"key":"江西省撫州市樂安縣","status":0},{"id":7,"key":"江西省撫州市東鄉區","status":0},{"id":9,"key":"江西省撫州市南豐縣","status":0},{"id":6,"key":"江西省撫州市臨川區","status":0},{"id":1,"key":"江西省撫州市","status":0},{"id":11,"key":"江西省撫州市崇仁縣","status":0}]},{"city":"江西省本級市","areas":[{"id":120,"key":"江西省本級市","status":0}]},{"city":"江西省景德鎮市","areas":[{"id":113,"key":"江西省景德鎮市浮梁縣","status":0},{"id":111,"key":"江西省景德鎮市昌江區","status":0},{"id":112,"key":"江西省景德鎮市珠山區","status":0},{"id":115,"key":"江西省景德鎮市景德鎮高新技","status":0},{"id":114,"key":"江西省景德鎮市樂平市","status":1},{"id":110,"key":"江西省景德鎮市","status":0}]},{"city":"江西省萍鄉市","areas":[{"id":57,"key":"江西省萍鄉市上栗縣","status":0},{"id":55,"key":"江西省萍鄉市湘東區","status":1},{"id":53,"key":"江西省萍鄉市","status":0},{"id":59,"key":"江西省萍鄉市經濟技術開發區","status":0},{"id":58,"key":"江西省萍鄉市蘆溪縣","status":0},{"id":56,"key":"江西省萍鄉市蓮花縣","status":0},{"id":54,"key":"江西省萍鄉市安源區","status":0}]},{"city":"江西省鷹潭市","areas":[{"id":118,"key":"江西省鷹潭市月湖區","status":0},{"id":117,"key":"江西省鷹潭市餘江區","status":0},{"id":119,"key":"江西省鷹潭市貴溪市","status":0},{"id":116,"key":"江西省鷹潭市","status":0}]},{"city":"江西省贛州市","areas":[{"id":72,"key":"江西省贛州市全南縣","status":0},{"id":81,"key":"江西省贛州市贛州市蓉江新區","status":0},{"id":68,"key":"江西省贛州市安遠縣","status":0},{"id":69,"key":"江西省贛州市龍南經濟技術開發區","status":1},{"id":80,"key":"江西省贛州市贛州經濟技術開發區","status":0},{"id":66,"key":"江西省贛州市上猶縣","status":0},{"id":76,"key":"江西省贛州市會昌縣","status":0},{"id":70,"key":"江西省贛州市龍南縣","status":0},{"id":61,"key":"江西省贛州市章貢區","status":0},{"id":71,"key":"江西省贛州市定南縣","status":0},{"id":60,"key":"江西省贛州市","status":1},{"id":64,"key":"江西省贛州市信豐縣","status":0},{"id":74,"key":"江西省贛州市于都縣","status":0},{"id":63,"key":"江西省贛州市贛縣區","status":0},{"id":79,"key":"江西省贛州市瑞金市","status":0},{"id":65,"key":"江西省贛州市大餘縣","status":0},{"id":67,"key":"江西省贛州市崇義縣","status":0},{"id":77,"key":"江西省贛州市尋烏縣","status":1},{"id":78,"key":"江西省贛州市石城縣","status":0},{"id":62,"key":"江西省贛州市南康區","status":0},{"id":75,"key":"江西省贛州市興國縣","status":0},{"id":73,"key":"江西省贛州市甯都縣","status":0}]}],"message":"請求成功"}', "com.code.runner.web.HttpController", "getCitys", "http-nio-8080-exec-1", 1627974568580,1627974568600,20,"null");
INSERT INTO `call_servlet`(`id`, `trace`, `span`, `session`, `address`, `url`, `method`, `params`,`header`, `cookies`, `thread`, `status`,`start_time`, `end_time`, `use_time`, `error`) VALUES(1422454555202715648, "1422454555219492864", "0", "68C9DB158B73F3FEA556BCF3C8569BD2", "0:0:0:0:0:0:0:1", "/api/citys", "GET", '{}', '{"sec-fetch-mode":"navigate","sec-fetch-site":"none","accept-language":"zh-CN,zh;q=0.9","cookie":"Idea-39b9dd95=bd61abab-a6f5-4866-aab2-901cf43d0220; JSESSIONID=0753AD8409CC3FF0122346B87FF93EF9","sec-fetch-user":"?1","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9","sec-ch-ua":"\"Chromium\";v=\"92\", \" Not A;Brand\";v=\"99\", \"Google Chrome\";v=\"92\"","sec-ch-ua-mobile":"?0","host":"localhost:8080","upgrade-insecure-requests":"1","connection":"keep-alive","cache-control":"max-age=0","accept-encoding":"gzip, deflate, br","user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36","sec-fetch-dest":"document"}', '[{"name":"Idea-39b9dd95","value":"bd61abab-a6f5-4866-aab2-901cf43d0220","version":0,"maxAge":-1,"secure":false,"httpOnly":false},{"name":"JSESSIONID","value":"0753AD8409CC3FF0122346B87FF93EF9","version":0,"maxAge":-1,"secure":false,"httpOnly":false}]', "http-nio-8080-exec-1", 200, 1627974568544,1627974568713,169, "null");
           

Service 層采集

相比較 controller 層的采集, service 層的采集就相當簡單了, 直接貼代碼:

CREATE TABLE `call_service`(
	`id` 				    bigint primary key auto_increment comment '主鍵ID',
	`trace` 		        varchar(50) 	not null comment '調用鍊辨別, 同樣的 調用鍊辨別 表示同一次會話',
	`span` 			        varchar(3000)   not null comment '層級 ID, 表示這個方法的運作位于這個調用鍊中的位置. 如果是 0 則表示是調用鍊發起端',
	`result`		        varchar(3000) 	not null comment '方法傳回的結果',
	`class_name` 		    varchar(100) 	not null comment '處理的類名稱',
	`method_name`		    varchar(3000)   not null comment '具體負責處理的方法的名稱',
	`thread`		        varchar(50)		not null comment '處理的線程的資訊',
	`start_time`	        bigint 		    not null comment '請求發起開始時間',
	`end_time`		        bigint 		    not null comment '請求結束的時間',
	`use_time`		        bigint 		    not null comment '請求消耗時間',
	`error`		            varchar(3000)	not null comment '異常描述資訊'
) ENGINE=INNODB DEFAULT CHARSET='UTF8' COMMENT 'service 層的 APM 監控';
           
package org.example.call.pojo;

public class CallService extends BaseCall {
    @Override
    public String getData() {
        return "INSERT INTO `call_service`(" + "`id`, `trace`, `span`, `result`, " +
                "`class_name`, `method_name`, `thread`, " +
                "`start_time`, `end_time`, `use_time`, `error`) VALUES(" +
                id + ", \"" + trace + "\", \"" + span + "\", '" + result + "', " +
                "\"" + className + "\", \"" + methodName + "\", \"" + thread + "\", " +
                startTime + "," + endTime + "," + useTime + ",\"" + error + "\");";
    }
}
           
package org.example.filter.support;

import com.codetool.common.AnnotationHelper;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import org.example.call.CallContext;
import org.example.call.CallSpan;
import org.example.call.pojo.BaseCall;
import org.example.context.BaseTemplate;
import org.example.context.TemplateFactory;
import org.example.filter.AbstractFilterChain;

import java.util.HashMap;
import java.util.Map;

public class SpringServiceFilterChain extends AbstractFilterChain {
    public static final SpringServiceFilterChain INSTANCE = new SpringServiceFilterChain();
    private static final String serviceAnnotation = "@org.springframework.stereotype.Service";

    @Override
    public boolean isTargetClass(String className, CtClass ctClass) {
        try {
            Object[] annotations = ctClass.getAnnotations();
            String annotationValue = AnnotationHelper.getAnnotationValue(annotations, serviceAnnotation);
            return annotationValue != null;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }


    @Override
    public void before(BaseCall context) {
        CallSpan.Span span = CallContext.createEntrySpan(null);
        context.span = span.toString();
        context.trace = CallContext.getTrace();
    }

    @Override
    public void finale(BaseCall context) {
        super.finale(context);
    }

    @Override
    public byte[] processingAgentClass(ClassLoader loader, CtClass ctClass, String className) throws Exception {
        CtMethod[] methods = ctClass.getDeclaredMethods();
        for (CtMethod method : methods) {
            if (!processed(method)) {
                continue;
            }

            String methodName = method.getName();

            BaseCall context = new BaseCall();
            context.type = "SERVICE";
            context.className = className;
            context.methodName = methodName;
            context.context.put("CallType", "org.example.call.pojo.CallService");
            context.context.put("instance", "org.example.filter.support.SpringServiceFilterChain.INSTANCE");
            context.context.put("names", renderParamNames(method));


            ctClass.addMethod(CtNewMethod.copy(method, methodName + "$agent", ctClass, null));
            BaseTemplate baseTemplate = TemplateFactory.getTemplate(method.getReturnType() != CtClass.voidType);
            baseTemplate.context = context;
            method.setBody(baseTemplate.render());
        }
        return ctClass.toBytecode();
    }
}
           
package org.example;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.LoaderClassPath;
import org.apache.commons.lang3.StringUtils;
import org.example.context.VMInfo;
import org.example.filter.FilterChain;
import org.example.filter.support.HttpServletFilterChain;
import org.example.filter.support.SpringControllerFilterChain;
import org.example.filter.support.SpringServiceFilterChain;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class AgentApplication implements ClassFileTransformer {
    private final List<FilterChain> chains = new ArrayList<>();
    private static final byte[] NO_TRANSFORM = null;

    private AgentApplication() {
        chains.add(new HttpServletFilterChain());
        chains.add(new SpringControllerFilterChain());
        chains.add(new SpringServiceFilterChain());
    }


    public static void premain(String args, Instrumentation instrumentation) {
        boolean showVm = false;
        if (StringUtils.isNoneBlank(args) && args.equals("debug")) {
            showVm = true;
        }

        // 開始 JVM 監控
        if (showVm) {
            // 開啟一個線程
            // 每隔 1分鐘 執行一次
            Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> {
                VMInfo.memoryInfo();
                VMInfo.gcInfo();
                System.out.println();
            }, 0, 60, TimeUnit.SECONDS);
        }

        System.out.println("          _____                   _______                   _____                    _____                _____                    _____                    _____");
        System.out.println("         /\\    \\                 /::\\    \\                 /\\    \\                  /\\    \\              /\\    \\                  /\\    \\                  /\\    \\ ");
        System.out.println("        /::\\____\\               /::::\\    \\               /::\\____\\                /::\\    \\            /::\\    \\                /::\\    \\                /::\\    \\ ");
        System.out.println("       /::::|   |              /::::::\\    \\             /::::|   |                \\:::\\    \\           \\:::\\    \\              /::::\\    \\              /::::\\    \\ ");
        System.out.println("      /:::::|   |             /::::::::\\    \\           /:::::|   |                 \\:::\\    \\           \\:::\\    \\            /::::::\\    \\            /::::::\\    \\ ");
        System.out.println("     /::::::|   |            /:::/~~\\:::\\    \\         /::::::|   |                  \\:::\\    \\           \\:::\\    \\          /:::/\\:::\\    \\          /:::/\\:::\\    \\ ");
        System.out.println("    /:::/|::|   |           /:::/    \\:::\\    \\       /:::/|::|   |                   \\:::\\    \\           \\:::\\    \\        /:::/__\\:::\\    \\        /:::/__\\:::\\    \\ ");
        System.out.println("   /:::/ |::|   |          /:::/    / \\:::\\    \\     /:::/ |::|   |                   /::::\\    \\          /::::\\    \\      /::::\\   \\:::\\    \\      /::::\\   \\:::\\    \\ ");
        System.out.println("  /:::/  |::|___|______   /:::/____/   \\:::\\____\\   /:::/  |::|   | _____    ____    /::::::\\    \\        /::::::\\    \\    /::::::\\   \\:::\\    \\    /::::::\\   \\:::\\    \\ ");
        System.out.println(" /:::/   |::::::::\\    \\ |:::|    |     |:::|    | /:::/   |::|   |/\\    \\  /\\   \\  /:::/\\:::\\    \\      /:::/\\:::\\    \\  /:::/\\:::\\   \\:::\\    \\  /:::/\\:::\\   \\:::\\____\\ ");
        System.out.println("/:::/    |:::::::::\\____\\|:::|____|     |:::|    |/:: /    |::|   /::\\____\\/::\\   \\/:::/  \\:::\\____\\    /:::/  \\:::\\____\\/:::/__\\:::\\   \\:::\\____\\/:::/  \\:::\\   \\:::|    |");
        System.out.println("\\::/    / ~~~~~/:::/    / \\:::\\    \\   /:::/    / \\::/    /|::|  /:::/    /\\:::\\  /:::/    \\::/    /   /:::/    \\::/    /\\:::\\   \\:::\\   \\::/    /\\::/   |::::\\  /:::|____|");
        System.out.println(" \\/____/      /:::/    /   \\:::\\    \\ /:::/    /   \\/____/ |::| /:::/    /  \\:::\\/:::/    / \\/____/   /:::/    / \\/____/  \\:::\\   \\:::\\   \\/____/  \\/____|:::::\\/:::/    /");
        System.out.println("             /:::/    /     \\:::\\    /:::/    /            |::|/:::/    /    \\::::::/    /           /:::/    /            \\:::\\   \\:::\\    \\            |:::::::::/    /");
        System.out.println("            /:::/    /       \\:::\\__/:::/    /             |::::::/    /      \\::::/____/           /:::/    /              \\:::\\   \\:::\\____\\           |::|\\::::/    /");
        System.out.println("           /:::/    /         \\::::::::/    /              |:::::/    /        \\:::\\    \\           \\::/    /                \\:::\\   \\::/    /           |::| \\::/____/");
        System.out.println("          /:::/    /           \\::::::/    /               |::::/    /          \\:::\\    \\           \\/____/                  \\:::\\   \\/____/            |::|  ~|");
        System.out.println("         /:::/    /             \\::::/    /                /:::/    /            \\:::\\    \\                                    \\:::\\    \\                |::|   |");
        System.out.println("        /:::/    /               \\::/____/                /:::/    /              \\:::\\____\\                                    \\:::\\____\\               \\::|   |");
        System.out.println("        \\::/    /                 ~~                      \\::/    /                \\::/    /                                     \\::/    /                \\:|   |");
        System.out.println("         \\/____/                                           \\/____/                  \\/____/                                       \\/____/                  \\|___|");
        System.out.println();
        instrumentation.addTransformer(new AgentApplication());
    }


    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
            if (className == null) {
                return NO_TRANSFORM;
            }
            String finalClassName = className.replace("/", ".");

            ClassPool pool = new ClassPool(true);
            if (loader != null) {
                pool.insertClassPath(new LoaderClassPath(loader));
            } else {
                pool.insertClassPath(new LoaderClassPath(ClassLoader.getSystemClassLoader()));
            }

            for (FilterChain chain : chains) {
                CtClass sourceClass = pool.getCtClass(finalClassName);
                if (chain.isTargetClass(finalClassName, sourceClass)) {
                    System.err.println("嘗試對類: " + className + " 進行增強");
                    try {
                        return chain.processingAgentClass(loader, sourceClass, finalClassName);
                    } catch (Exception e) {
                        System.out.println("無法對類 " + className + " 進行增強, 具體的錯誤原因是: " + e.toString());
                    }
                }
            }

        } catch (Exception e) {
            // TODO ...
        }

        return NO_TRANSFORM;
    }


}
           

開始測試, 發送一個 web 請求

INSERT INTO `call_service`(`id`, `trace`, `span`, `result`, `class_name`, `method_name`, `thread`, `start_time`, `end_time`, `use_time`, `error`) VALUES(1422457117083598849, "1422457116907438080", "0.1.1", '["江西省吉安市","江西省九江市","江西省南昌市","江西省上饒市","江西省宜春市","江西省新餘市","江西省撫州市","江西省本級市","江西省景德鎮市","江西省萍鄉市","江西省鷹潭市","江西省贛州市"]', "com.code.runner.service.DataService", "getAreaKeys", "http-nio-8080-exec-1", "1627975179344,1627975179351,7,"null");
INSERT INTO `call_service`(`id`, `trace`, `span`, `result`, `class_name`, `method_name`, `thread`, `start_time`, `end_time`, `use_time`, `error`) VALUES(1422457117117153280, "1422457116907438080", "0.1.2", '[{"id":84,"key":"江西省吉安市青原區","status":0},{"id":94,"key":"江西省吉安市萬安縣","status":0},{"id":90,"key":"江西省吉安市峽江縣","status":0},{"id":88,"key":"江西省吉安市永豐縣","status":0},{"id":86,"key":"江西省吉安市吉安縣","status":0},{"id":89,"key":"江西省吉安市新幹縣","status":0},{"id":92,"key":"江西省吉安市安福縣","status":0},{"id":93,"key":"江西省吉安市泰和縣","status":0},{"id":85,"key":"江西省吉安市井岡山市","status":0},{"id":95,"key":"江西省吉安市永新縣","status":0},{"id":83,"key":"江西省吉安市吉州區","status":0},{"id":87,"key":"江西省吉安市吉水縣","status":0},{"id":91,"key":"江西省吉安市遂川縣","status":0},{"id":82,"key":"江西省吉安市","status":0}]', "com.code.runner.service.DataService", "detailKeys", "http-nio-8080-exec-1", "1627975179352,1627975179355,3,"null");
INSERT INTO `call_service`(`id`, `trace`, `span`, `result`, `class_name`, `method_name`, `thread`, `start_time`, `end_time`, `use_time`, `error`) VALUES(1422457117133930496, "1422457116907438080", "0.1.3", '[{"id":105,"key":"江西省九江市柴桑區","status":0},{"id":109,"key":"江西省九江市廬山市","status":0},{"id":102,"key":"江西省九江市永修縣","status":1},{"id":97,"key":"江西省九江市武甯縣","status":1},{"id":98,"key":"江西省九江市濂溪區","status":0},{"id":104,"key":"江西省九江市湖口縣","status":0},{"id":103,"key":"江西省九江市彭澤縣","status":0},{"id":107,"key":"江西省九江市瑞昌市","status":0},{"id":106,"key":"江西省九江市都昌縣","status":1},{"id":100,"key":"江西省九江市共青城市","status":1},{"id":108,"key":"江西省九江市德安縣","status":0},{"id":96,"key":"江西省九江市","status":0},{"id":99,"key":"江西省九江市浔陽區","status":1},{"id":101,"key":"江西省九江市修水縣","status":1}]', "com.code.runner.service.DataService", "detailKeys", "http-nio-8080-exec-1", "1627975179356,1627975179357,1,"null");
INSERT INTO `call_service`(`id`, `trace`, `span`, `result`, `class_name`, `method_name`, `thread`, `start_time`, `end_time`, `use_time`, `error`) VALUES(1422457117138124800, "1422457116907438080", "0.1.4", '[{"id":13,"key":"江西省南昌市","status":0},{"id":16,"key":"江西省南昌市青山湖區","status":0},{"id":15,"key":"江西省南昌市西湖區","status":0},{"id":21,"key":"江西省南昌市建立區","status":0},{"id":17,"key":"江西省南昌市青雲譜區","status":1},{"id":18,"key":"江西省南昌市安義縣","status":1},{"id":19,"key":"江西省南昌市進賢縣","status":0},{"id":14,"key":"江西省南昌市東湖區","status":0},{"id":20,"key":"江西省南昌市南昌縣","status":0}]', "com.code.runner.service.DataService", "detailKeys", "http-nio-8080-exec-1", "1627975179357,1627975179358,1,"null");
INSERT INTO `call_service`(`id`, `trace`, `span`, `result`, `class_name`, `method_name`, `thread`, `start_time`, `end_time`, `use_time`, `error`) VALUES(1422457117142319104, "1422457116907438080", "0.1.5", '[{"id":29,"key":"江西省上饒市弋陽縣","status":0},{"id":24,"key":"江西省上饒市廣豐區","status":0},{"id":33,"key":"江西省上饒市婺源縣","status":0},{"id":35,"key":"江西省上饒市上饒經濟技術開發區","status":0},{"id":23,"key":"江西省上饒市廣信區","status":1},{"id":30,"key":"江西省上饒市橫峰縣","status":0},{"id":25,"key":"江西省上饒市信州區","status":0},{"id":31,"key":"江西省上饒市鄱陽縣","status":0},{"id":32,"key":"江西省上饒市萬年縣","status":0},{"id":27,"key":"江西省上饒市","status":0},{"id":26,"key":"江西省上饒市鉛山縣","status":0},{"id":34,"key":"江西省上饒市德興市","status":0},{"id":28,"key":"江西省上饒市玉山縣","status":1},{"id":22,"key":"江西省上饒市餘幹縣","status":0}]', "com.code.runner.service.DataService", "detailKeys", "http-nio-8080-exec-1", "1627975179358,1627975179359,1,"null");
INSERT INTO `call_service`(`id`, `trace`, `span`, `result`, `class_name`, `method_name`, `thread`, `start_time`, `end_time`, `use_time`, `error`) VALUES(1422457117146513408, "1422457116907438080", "0.1.6", '[{"id":45,"key":"江西省宜春市上高縣","status":0},{"id":50,"key":"江西省宜春市樟樹市","status":0},{"id":47,"key":"江西省宜春市靖安縣","status":0},{"id":46,"key":"江西省宜春市宜豐縣","status":0},{"id":52,"key":"江西省宜春市宜經濟技術開發區","status":0},{"id":51,"key":"江西省宜春市高安市","status":0},{"id":41,"key":"江西省宜春市","status":0},{"id":44,"key":"江西省宜春市萬載縣","status":0},{"id":48,"key":"江西省宜春市銅鼓縣","status":0},{"id":42,"key":"江西省宜春市袁州區","status":0},{"id":49,"key":"江西省宜春市豐城市","status":0},{"id":43,"key":"江西省宜春市奉新縣","status":0}]', "com.code.runner.service.DataService", "detailKeys", "http-nio-8080-exec-1", "1627975179359,1627975179360,1,"null");
INSERT INTO `call_service`(`id`, `trace`, `span`, `result`, `class_name`, `method_name`, `thread`, `start_time`, `end_time`, `use_time`, `error`) VALUES(1422457117150707712, "1422457116907438080", "0.1.7", '[{"id":40,"key":"江西省新餘市高新技術産業開發區","status":0},{"id":36,"key":"江西省新餘市","status":0},{"id":37,"key":"江西省新餘市渝水區","status":0},{"id":39,"key":"江西省新餘市仙女湖風景名勝區","status":0},{"id":38,"key":"江西省新餘市分宜縣","status":0}]', "com.code.runner.service.DataService", "detailKeys", "http-nio-8080-exec-1", "1627975179360,1627975179361,1,"null");
INSERT INTO `call_service`(`id`, `trace`, `span`, `result`, `class_name`, `method_name`, `thread`, `start_time`, `end_time`, `use_time`, `error`) VALUES(1422457117154902016, "1422457116907438080", "0.1.8", '[{"id":2,"key":"江西省撫州市宜黃縣","status":0},{"id":3,"key":"江西省撫州市資溪縣","status":0},{"id":5,"key":"江西省撫州市金溪縣","status":0},{"id":8,"key":"江西省撫州市南城縣","status":0},{"id":12,"key":"江西省撫州市廣昌縣","status":0},{"id":10,"key":"江西省撫州市黎川縣","status":0},{"id":4,"key":"江西省撫州市樂安縣","status":0},{"id":7,"key":"江西省撫州市東鄉區","status":0},{"id":9,"key":"江西省撫州市南豐縣","status":0},{"id":6,"key":"江西省撫州市臨川區","status":0},{"id":1,"key":"江西省撫州市","status":0},{"id":11,"key":"江西省撫州市崇仁縣","status":0}]', "com.code.runner.service.DataService", "detailKeys", "http-nio-8080-exec-1", "1627975179361,1627975179362,1,"null");
INSERT INTO `call_service`(`id`, `trace`, `span`, `result`, `class_name`, `method_name`, `thread`, `start_time`, `end_time`, `use_time`, `error`) VALUES(1422457117159096320, "1422457116907438080", "0.1.9", '[{"id":120,"key":"江西省本級市","status":0}]', "com.code.runner.service.DataService", "detailKeys", "http-nio-8080-exec-1", "1627975179362,1627975179362,0,"null");
INSERT INTO `call_service`(`id`, `trace`, `span`, `result`, `class_name`, `method_name`, `thread`, `start_time`, `end_time`, `use_time`, `error`) VALUES(1422457117163290624, "1422457116907438080", "0.1.01", '[{"id":113,"key":"江西省景德鎮市浮梁縣","status":0},{"id":111,"key":"江西省景德鎮市昌江區","status":0},{"id":112,"key":"江西省景德鎮市珠山區","status":0},{"id":115,"key":"江西省景德鎮市景德鎮高新技","status":0},{"id":114,"key":"江西省景德鎮市樂平市","status":1},{"id":110,"key":"江西省景德鎮市","status":0}]', "com.code.runner.service.DataService", "detailKeys", "http-nio-8080-exec-1", "1627975179363,1627975179363,0,"null");
INSERT INTO `call_service`(`id`, `trace`, `span`, `result`, `class_name`, `method_name`, `thread`, `start_time`, `end_time`, `use_time`, `error`) VALUES(1422457117167484928, "1422457116907438080", "0.1.11", '[{"id":57,"key":"江西省萍鄉市上栗縣","status":0},{"id":55,"key":"江西省萍鄉市湘東區","status":1},{"id":53,"key":"江西省萍鄉市","status":0},{"id":59,"key":"江西省萍鄉市經濟技術開發區","status":0},{"id":58,"key":"江西省萍鄉市蘆溪縣","status":0},{"id":56,"key":"江西省萍鄉市蓮花縣","status":0},{"id":54,"key":"江西省萍鄉市安源區","status":0}]', "com.code.runner.service.DataService", "detailKeys", "http-nio-8080-exec-1", "1627975179364,1627975179364,0,"null");
INSERT INTO `call_service`(`id`, `trace`, `span`, `result`, `class_name`, `method_name`, `thread`, `start_time`, `end_time`, `use_time`, `error`) VALUES(1422457117167484929, "1422457116907438080", "0.1.21", '[{"id":118,"key":"江西省鷹潭市月湖區","status":0},{"id":117,"key":"江西省鷹潭市餘江區","status":0},{"id":119,"key":"江西省鷹潭市貴溪市","status":0},{"id":116,"key":"江西省鷹潭市","status":0}]', "com.code.runner.service.DataService", "detailKeys", "http-nio-8080-exec-1", "1627975179364,1627975179365,1,"null");
INSERT INTO `call_service`(`id`, `trace`, `span`, `result`, `class_name`, `method_name`, `thread`, `start_time`, `end_time`, `use_time`, `error`) VALUES(1422457117171679232, "1422457116907438080", "0.1.31", '[{"id":72,"key":"江西省贛州市全南縣","status":0},{"id":81,"key":"江西省贛州市贛州市蓉江新區","status":0},{"id":68,"key":"江西省贛州市安遠縣","status":0},{"id":69,"key":"江西省贛州市龍南經濟技術開發區","status":1},{"id":80,"key":"江西省贛州市贛州經濟技術開發區","status":0},{"id":66,"key":"江西省贛州市上猶縣","status":0},{"id":76,"key":"江西省贛州市會昌縣","status":0},{"id":70,"key":"江西省贛州市龍南縣","status":0},{"id":61,"key":"江西省贛州市章貢區","status":0},{"id":71,"key":"江西省贛州市定南縣","status":0},{"id":60,"key":"江西省贛州市","status":1},{"id":64,"key":"江西省贛州市信豐縣","status":0},{"id":74,"key":"江西省贛州市于都縣","status":0},{"id":63,"key":"江西省贛州市贛縣區","status":0},{"id":79,"key":"江西省贛州市瑞金市","status":0},{"id":65,"key":"江西省贛州市大餘縣","status":0},{"id":67,"key":"江西省贛州市崇義縣","status":0},{"id":77,"key":"江西省贛州市尋烏縣","status":1},{"id":78,"key":"江西省贛州市石城縣","status":0},{"id":62,"key":"江西省贛州市南康區","status":0},{"id":75,"key":"江西省贛州市興國縣","status":0},{"id":73,"key":"江西省贛州市甯都縣","status":0}]', "com.code.runner.service.DataService", "detailKeys", "http-nio-8080-exec-1", "1627975179365,1627975179366,1,"null");
INSERT INTO `call_web`(`id`, `trace`, `span`, `result`, `class_name`, `method_name`, `thread`, `start_time`, `end_time`, `use_time`, `error`) VALUES(1422457117083598848, "1422457116907438080", "0.1", '{"code":200,"data":[{"city":"江西省吉安市","areas":[{"id":84,"key":"江西省吉安市青原區","status":0},{"id":94,"key":"江西省吉安市萬安縣","status":0},{"id":90,"key":"江西省吉安市峽江縣","status":0},{"id":88,"key":"江西省吉安市永豐縣","status":0},{"id":86,"key":"江西省吉安市吉安縣","status":0},{"id":89,"key":"江西省吉安市新幹縣","status":0},{"id":92,"key":"江西省吉安市安福縣","status":0},{"id":93,"key":"江西省吉安市泰和縣","status":0},{"id":85,"key":"江西省吉安市井岡山市","status":0},{"id":95,"key":"江西省吉安市永新縣","status":0},{"id":83,"key":"江西省吉安市吉州區","status":0},{"id":87,"key":"江西省吉安市吉水縣","status":0},{"id":91,"key":"江西省吉安市遂川縣","status":0},{"id":82,"key":"江西省吉安市","status":0}]},{"city":"江西省九江市","areas":[{"id":105,"key":"江西省九江市柴桑區","status":0},{"id":109,"key":"江西省九江市廬山市","status":0},{"id":102,"key":"江西省九江市永修縣","status":1},{"id":97,"key":"江西省九江市武甯縣","status":1},{"id":98,"key":"江西省九江市濂溪區","status":0},{"id":104,"key":"江西省九江市湖口縣","status":0},{"id":103,"key":"江西省九江市彭澤縣","status":0},{"id":107,"key":"江西省九江市瑞昌市","status":0},{"id":106,"key":"江西省九江市都昌縣","status":1},{"id":100,"key":"江西省九江市共青城市","status":1},{"id":108,"key":"江西省九江市德安縣","status":0},{"id":96,"key":"江西省九江市","status":0},{"id":99,"key":"江西省九江市浔陽區","status":1},{"id":101,"key":"江西省九江市修水縣","status":1}]},{"city":"江西省南昌市","areas":[{"id":13,"key":"江西省南昌市","status":0},{"id":16,"key":"江西省南昌市青山湖區","status":0},{"id":15,"key":"江西省南昌市西湖區","status":0},{"id":21,"key":"江西省南昌市建立區","status":0},{"id":17,"key":"江西省南昌市青雲譜區","status":1},{"id":18,"key":"江西省南昌市安義縣","status":1},{"id":19,"key":"江西省南昌市進賢縣","status":0},{"id":14,"key":"江西省南昌市東湖區","status":0},{"id":20,"key":"江西省南昌市南昌縣","status":0}]},{"city":"江西省上饒市","areas":[{"id":29,"key":"江西省上饒市弋陽縣","status":0},{"id":24,"key":"江西省上饒市廣豐區","status":0},{"id":33,"key":"江西省上饒市婺源縣","status":0},{"id":35,"key":"江西省上饒市上饒經濟技術開發區","status":0},{"id":23,"key":"江西省上饒市廣信區","status":1},{"id":30,"key":"江西省上饒市橫峰縣","status":0},{"id":25,"key":"江西省上饒市信州區","status":0},{"id":31,"key":"江西省上饒市鄱陽縣","status":0},{"id":32,"key":"江西省上饒市萬年縣","status":0},{"id":27,"key":"江西省上饒市","status":0},{"id":26,"key":"江西省上饒市鉛山縣","status":0},{"id":34,"key":"江西省上饒市德興市","status":0},{"id":28,"key":"江西省上饒市玉山縣","status":1},{"id":22,"key":"江西省上饒市餘幹縣","status":0}]},{"city":"江西省宜春市","areas":[{"id":45,"key":"江西省宜春市上高縣","status":0},{"id":50,"key":"江西省宜春市樟樹市","status":0},{"id":47,"key":"江西省宜春市靖安縣","status":0},{"id":46,"key":"江西省宜春市宜豐縣","status":0},{"id":52,"key":"江西省宜春市宜經濟技術開發區","status":0},{"id":51,"key":"江西省宜春市高安市","status":0},{"id":41,"key":"江西省宜春市","status":0},{"id":44,"key":"江西省宜春市萬載縣","status":0},{"id":48,"key":"江西省宜春市銅鼓縣","status":0},{"id":42,"key":"江西省宜春市袁州區","status":0},{"id":49,"key":"江西省宜春市豐城市","status":0},{"id":43,"key":"江西省宜春市奉新縣","status":0}]},{"city":"江西省新餘市","areas":[{"id":40,"key":"江西省新餘市高新技術産業開發區","status":0},{"id":36,"key":"江西省新餘市","status":0},{"id":37,"key":"江西省新餘市渝水區","status":0},{"id":39,"key":"江西省新餘市仙女湖風景名勝區","status":0},{"id":38,"key":"江西省新餘市分宜縣","status":0}]},{"city":"江西省撫州市","areas":[{"id":2,"key":"江西省撫州市宜黃縣","status":0},{"id":3,"key":"江西省撫州市資溪縣","status":0},{"id":5,"key":"江西省撫州市金溪縣","status":0},{"id":8,"key":"江西省撫州市南城縣","status":0},{"id":12,"key":"江西省撫州市廣昌縣","status":0},{"id":10,"key":"江西省撫州市黎川縣","status":0},{"id":4,"key":"江西省撫州市樂安縣","status":0},{"id":7,"key":"江西省撫州市東鄉區","status":0},{"id":9,"key":"江西省撫州市南豐縣","status":0},{"id":6,"key":"江西省撫州市臨川區","status":0},{"id":1,"key":"江西省撫州市","status":0},{"id":11,"key":"江西省撫州市崇仁縣","status":0}]},{"city":"江西省本級市","areas":[{"id":120,"key":"江西省本級市","status":0}]},{"city":"江西省景德鎮市","areas":[{"id":113,"key":"江西省景德鎮市浮梁縣","status":0},{"id":111,"key":"江西省景德鎮市昌江區","status":0},{"id":112,"key":"江西省景德鎮市珠山區","status":0},{"id":115,"key":"江西省景德鎮市景德鎮高新技","status":0},{"id":114,"key":"江西省景德鎮市樂平市","status":1},{"id":110,"key":"江西省景德鎮市","status":0}]},{"city":"江西省萍鄉市","areas":[{"id":57,"key":"江西省萍鄉市上栗縣","status":0},{"id":55,"key":"江西省萍鄉市湘東區","status":1},{"id":53,"key":"江西省萍鄉市","status":0},{"id":59,"key":"江西省萍鄉市經濟技術開發區","status":0},{"id":58,"key":"江西省萍鄉市蘆溪縣","status":0},{"id":56,"key":"江西省萍鄉市蓮花縣","status":0},{"id":54,"key":"江西省萍鄉市安源區","status":0}]},{"city":"江西省鷹潭市","areas":[{"id":118,"key":"江西省鷹潭市月湖區","status":0},{"id":117,"key":"江西省鷹潭市餘江區","status":0},{"id":119,"key":"江西省鷹潭市貴溪市","status":0},{"id":116,"key":"江西省鷹潭市","status":0}]},{"city":"江西省贛州市","areas":[{"id":72,"key":"江西省贛州市全南縣","status":0},{"id":81,"key":"江西省贛州市贛州市蓉江新區","status":0},{"id":68,"key":"江西省贛州市安遠縣","status":0},{"id":69,"key":"江西省贛州市龍南經濟技術開發區","status":1},{"id":80,"key":"江西省贛州市贛州經濟技術開發區","status":0},{"id":66,"key":"江西省贛州市上猶縣","status":0},{"id":76,"key":"江西省贛州市會昌縣","status":0},{"id":70,"key":"江西省贛州市龍南縣","status":0},{"id":61,"key":"江西省贛州市章貢區","status":0},{"id":71,"key":"江西省贛州市定南縣","status":0},{"id":60,"key":"江西省贛州市","status":1},{"id":64,"key":"江西省贛州市信豐縣","status":0},{"id":74,"key":"江西省贛州市于都縣","status":0},{"id":63,"key":"江西省贛州市贛縣區","status":0},{"id":79,"key":"江西省贛州市瑞金市","status":0},{"id":65,"key":"江西省贛州市大餘縣","status":0},{"id":67,"key":"江西省贛州市崇義縣","status":0},{"id":77,"key":"江西省贛州市尋烏縣","status":1},{"id":78,"key":"江西省贛州市石城縣","status":0},{"id":62,"key":"江西省贛州市南康區","status":0},{"id":75,"key":"江西省贛州市興國縣","status":0},{"id":73,"key":"江西省贛州市甯都縣","status":0}]}],"message":"請求成功"}', "com.code.runner.web.HttpController", "getCitys", "http-nio-8080-exec-1", 1627975179344,1627975179369,25,"null");
INSERT INTO `call_servlet`(`id`, `trace`, `span`, `session`, `address`, `url`, `method`, `params`,`header`, `cookies`, `thread`, `status`,`start_time`, `end_time`, `use_time`, `error`) VALUES(1422457116894855168, "1422457116907438080", "0", "3F2C71660332A57ECAA76E3E89679030", "0:0:0:0:0:0:0:1", "/api/citys", "GET", '{}', '{"sec-fetch-mode":"navigate","sec-fetch-site":"none","accept-language":"zh-CN,zh;q=0.9","cookie":"Idea-39b9dd95=bd61abab-a6f5-4866-aab2-901cf43d0220; JSESSIONID=68C9DB158B73F3FEA556BCF3C8569BD2","sec-fetch-user":"?1","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9","sec-ch-ua":"\"Chromium\";v=\"92\", \" Not A;Brand\";v=\"99\", \"Google Chrome\";v=\"92\"","sec-ch-ua-mobile":"?0","host":"localhost:8080","upgrade-insecure-requests":"1","connection":"keep-alive","cache-control":"max-age=0","accept-encoding":"gzip, deflate, br","user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36","sec-fetch-dest":"document"}', '[{"name":"Idea-39b9dd95","value":"bd61abab-a6f5-4866-aab2-901cf43d0220","version":0,"maxAge":-1,"secure":false,"httpOnly":false},{"name":"JSESSIONID","value":"68C9DB158B73F3FEA556BCF3C8569BD2","version":0,"maxAge":-1,"secure":false,"httpOnly":false}]', "http-nio-8080-exec-1", 200, 1627975179299,1627975179476,177", null");
           

這裡發現一個小 BUG, span 的值在超過 10 之後顯示的順序有誤. 發現是 StringBuilder 的 reverse 引起的. 于是修改 CallSpan.Span 的 toString 代碼

public String toString() {
    Stack<Integer> stacks = new Stack<>();
    StringBuilder sb = new StringBuilder();
    stacks.push(value);
    Span currentSpan = parentSpan;
    while (currentSpan != null) {
        stacks.push(currentSpan.value);
        currentSpan = currentSpan.parentSpan;
    }

    while (!stacks.isEmpty()) {
        sb.append(stacks.pop()).append(".");
    }

    return sb.length() > 0 ? sb.substring(0, sb.length() - 1) : sb.toString();
}
           

JDBC 層的采集

jdbc 采集應該是最難的那一部分了. 但是我們在這個第一章裡面就介紹了監控 JDBC 的方式, 本質就是通過對 java.sql.Connection 進行一個代理進而在執行中插入我們的監控代碼. 

首先是采集實體類

package org.example.call.pojo;

public class CallJdbc extends BaseCall {
    // 編譯前的 sql
    public String prepareSql;

    // sql 語句使用的 Statement 類型
    public String statementType;

    // 資料庫類型
    public String databaseType;

    // sql 語句的類型, QUERY, EXECUTE, EXECUTE_UPDATE
    public String sqlType;

    // 資料庫連接配接位址
    public String jdbcUrl;

    // 使用者名稱
    public String username;

    // 最終運作的 sql
    public String finallySql;

    // sql 攜帶的參數
    public String parameters;

    @Override
    public String getData() {
        return "INSERT INTO `call_jdbc`(`id`, `trace`, `span`, `prepare_sql`, `statement_type`, `database_type`, `sql_type`, `result`, `jdbc_url`, `username`, `finally_sql`, `parameters`, `thread`, `start_time`, `end_time`, `use_time`, `error`) " +
                "VALUES(" + id + ", \"" + trace + "\", \"" + span + "\", \"" + prepareSql + "\", \"" + statementType + "\", \"" + databaseType + "\", \"" + sqlType + "\", \"" + result + "\", \"" + jdbcUrl + "\", \"" + username + "\", \"" + finallySql + "\", \"" +
                parameters + "\", \"" + thread + "\", " + startTime + ", " + endTime + ", " + useTime + ", \"" + error + "\");\n";
    }
}
           

然後是表結構

CREATE TABLE `call_jdbc`(
	`id` 			    bigint primary key auto_increment comment '主鍵ID',
	`trace` 		    varchar(50) 	not null comment '調用鍊辨別, 同樣的 調用鍊辨別 表示同一次會話',
	`span` 			    varchar(3000)   not null comment '層級 ID, 表示這個方法的運作位于這個調用鍊中的位置. 如果是 0 則表示是調用鍊發起端',
	`prepare_sql`	    varchar(50) 	not null comment '預處理的sql',
	`statement_type`    varchar(30)		not null comment 'Statement 的類型,比如: PreparedStatement, Statement, CallableStatement',
	`database_type`     varchar(30) 	not null comment '資料庫類型, 比如: MySQL, Oracle, Sysbase...',
	`sql_type`			varchar(10)		not null comment 'Sql 類型, 比如: QUERY, EXECUTE, EXECUTE_UPDATE',
	`result`		    varchar(50)   	not null comment '傳回結果, 查詢:傳回條數\ 修改:傳回條數\ 執行:傳回1或0',
	`jdbc_url`			varchar(300)	not null comment 'JDBC 連接配接資訊',
	`username`			varchar(50)		not null comment 'JDBC 的使用者名資訊',
	`finally_sql`		varchar(3000)	not null comment '最終運作的 SQL 資訊',
	`parameters`		varchar(3000)	not null comment 'SQL 語句的具體參數',
	`thread`		    varchar(50)		not null comment '處理的線程的資訊',
	`start_time`	    bigint 		    not null comment '請求發起開始時間',
	`end_time`		    bigint 		    not null comment '請求結束的時間',
	`use_time`		    bigint 		    not null comment '請求消耗時間',
	`error`		        varchar(3000)	not null comment '異常描述資訊'
) ENGINE=INNODB DEFAULT CHARSET='UTF8' COMMENT 'jdbc 層的 APM 監控';
           

Jdbc 插樁入口

package org.example.filter.support;

import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import org.example.filter.FilterChain;
import org.example.proxy.ProxyConnection;

public class SpringJdbcFilterChain implements FilterChain {
    private static final String[] driverClassNames = {
            "com.mysql.jdbc.NonRegisteringDriver",
            "oracle.jdbc.driver.OracleDriver"
    };

    @Override
    public boolean isTargetClass(String className, CtClass ctClass) {
        for (String driverClassName : driverClassNames) {
            if (driverClassName.equals(className)) {
                return true;
            }
        }
        return false;
    }

    public static java.sql.Connection proxyConnection(java.sql.Connection connection) {
        return new ProxyConnection(connection);
    }

    @Override
    public byte[] processingAgentClass(ClassLoader loader, CtClass ctClass, String className) throws Exception {
        CtMethod connect = ctClass.getMethod("connect", "(Ljava/lang/String;Ljava/util/Properties;)Ljava/sql/Connection;");
        ctClass.addMethod(CtNewMethod.copy(connect, "connect$agent", ctClass, null));

        String sb = "{\n" +
                "    java.sql.Connection conn = connect$agent($$);\n"  +
                "    return org.example.filter.support.SpringJdbcFilterChain.proxyConnection(conn);\n" +
                "}\n";
        connect.setBody(sb);
        return ctClass.toBytecode();
    }

}
           
package org.example.proxy;

import org.example.call.CallContext;
import org.example.call.CallSpan;
import org.example.call.pojo.CallJdbc;

import java.sql.*;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;

public class ProxyConnection implements Connection {
    private Connection connection;
    private CallJdbc context = new CallJdbc();

    private static final String STATEMENT = "STATEMENT";
    private static final String PREPARED_STATEMENT = "PREPARED_STATEMENT";
    private static final String CALLABLE_STATEMENT = "CALLABLE_STATEMENT";
    private static final String NATIVE = "NATIVE";

    public ProxyConnection(Connection connection) {
        System.err.println(Thread.currentThread().getName() + ": new Connection ...");
        try {
            context.type = "JDBC";
            DatabaseMetaData metaData = connection.getMetaData();
            context.jdbcUrl = metaData.getURL();
            context.username = metaData.getUserName();
            context.databaseType = metaData.getDatabaseProductName();
            this.connection = connection;
        } catch (SQLException e) {
            e.printStackTrace();
        }


    }

    // 處理原生 sql 的采集資訊
    private void nativeStatement() {
        CallSpan.Span span = CallContext.createEntrySpan(null);
        context.thread = Thread.currentThread().getName();
        context.startTime = System.currentTimeMillis();
        context.trace = CallContext.getTrace();
        context.span = span.toString();
    }

    @Override
    public Statement createStatement() throws SQLException {
        context.statementType = STATEMENT;
        context.className = "java.sql.Statement";
        return new ProxyStatement(connection.createStatement(), context);
    }

    @Override
    public PreparedStatement prepareStatement(String sql) throws SQLException {
        context.statementType = PREPARED_STATEMENT;
        context.className = "java.sql.PreparedStatement";
        context.prepareSql = sql;
        return new ProxyPreparedStatement(connection.prepareStatement(sql), context);
    }

    @Override
    public CallableStatement prepareCall(String sql) throws SQLException {
        context.statementType = CALLABLE_STATEMENT;
        context.className = "java.sql.CallableStatement";
        context.prepareSql = sql;
        return new ProxyCallableStatement(connection.prepareCall(sql), context);
    }

    @Override
    public String nativeSQL(String sql) throws SQLException {
        context.statementType = NATIVE;
        context.className = "java.sql.NativeStatement";
        // 如果是 nativeSql, 那真的沒辦法了. 隻能通過方法補充一些資訊上去, 比如 trace, span...
        nativeStatement();
        context.prepareSql = sql;
        return connection.nativeSQL(sql);
    }

    @Override
    public void setAutoCommit(boolean autoCommit) throws SQLException {
        connection.setAutoCommit(autoCommit);
    }

    @Override
    public boolean getAutoCommit() throws SQLException {
        return connection.getAutoCommit();
    }

    @Override
    public void commit() throws SQLException {
        connection.commit();
    }

    @Override
    public void rollback() throws SQLException {
        System.err.println("close Connection ...");
        connection.rollback();
    }

    @Override
    public void close() throws SQLException {
        connection.close();
    }

    @Override
    public boolean isClosed() throws SQLException {
        return connection.isClosed();
    }

    @Override
    public DatabaseMetaData getMetaData() throws SQLException {
        return connection.getMetaData();
    }

    @Override
    public void setReadOnly(boolean readOnly) throws SQLException {
        connection.setReadOnly(readOnly);
    }

    @Override
    public boolean isReadOnly() throws SQLException {
        return connection.isReadOnly();
    }

    @Override
    public void setCatalog(String catalog) throws SQLException {
        connection.setCatalog(catalog);
    }

    @Override
    public String getCatalog() throws SQLException {
        return connection.getCatalog();
    }

    @Override
    public void setTransactionIsolation(int level) throws SQLException {
        connection.setTransactionIsolation(level);
    }

    @Override
    public int getTransactionIsolation() throws SQLException {
        return connection.getTransactionIsolation();
    }

    @Override
    public SQLWarning getWarnings() throws SQLException {
        return connection.getWarnings();
    }

    @Override
    public void clearWarnings() throws SQLException {
        connection.clearWarnings();
    }

    @Override
    public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
        context.statementType = STATEMENT;
        context.className = "java.sql.Statement";
        return new ProxyStatement(connection.createStatement(resultSetType, resultSetConcurrency), context);
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
        context.statementType = PREPARED_STATEMENT;
        context.className = "java.sql.PreparedStatement";
        context.prepareSql = sql;
        return new ProxyPreparedStatement(connection.prepareStatement(sql, resultSetType, resultSetConcurrency), context);
    }

    @Override
    public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
        context.statementType = CALLABLE_STATEMENT;
        context.className = "java.sql.CallableStatement";
        context.prepareSql = sql;
        return new ProxyCallableStatement(connection.prepareCall(sql, resultSetType, resultSetConcurrency), context);
    }

    @Override
    public Map<String, Class<?>> getTypeMap() throws SQLException {
        return connection.getTypeMap();
    }

    @Override
    public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
        connection.setTypeMap(map);
    }

    @Override
    public void setHoldability(int holdability) throws SQLException {
        connection.setHoldability(holdability);
    }

    @Override
    public int getHoldability() throws SQLException {
        return connection.getHoldability();
    }

    @Override
    public Savepoint setSavepoint() throws SQLException {
        return connection.setSavepoint();
    }

    @Override
    public Savepoint setSavepoint(String name) throws SQLException {
        return connection.setSavepoint(name);
    }

    @Override
    public void rollback(Savepoint savepoint) throws SQLException {
        connection.rollback(savepoint);
    }

    @Override
    public void releaseSavepoint(Savepoint savepoint) throws SQLException {
        connection.releaseSavepoint(savepoint);
    }

    @Override
    public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
        context.statementType = STATEMENT;
        context.className = "java.sql.Statement";
        return new ProxyStatement(connection.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability), context);
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
        context.statementType = PREPARED_STATEMENT;
        context.className = "java.sql.PreparedStatement";
        context.prepareSql = sql;
        return new ProxyPreparedStatement(connection.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability), context);
    }

    @Override
    public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
        context.statementType = CALLABLE_STATEMENT;
        context.className = "java.sql.CallableStatement";
        context.prepareSql = sql;
        return new ProxyCallableStatement(connection.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability), context);
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
        context.statementType = PREPARED_STATEMENT;
        context.className = "java.sql.PreparedStatement";
        context.prepareSql = sql;
        return new ProxyPreparedStatement(connection.prepareStatement(sql, autoGeneratedKeys), context);
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
        context.statementType = PREPARED_STATEMENT;
        context.className = "java.sql.PreparedStatement";
        context.prepareSql = sql;
        return new ProxyPreparedStatement(connection.prepareStatement(sql, columnIndexes), context);
    }

    @Override
    public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
        context.statementType = PREPARED_STATEMENT;
        context.className = "java.sql.PreparedStatement";
        context.prepareSql = sql;
        return new ProxyPreparedStatement(connection.prepareStatement(sql, columnNames), context);
    }

    @Override
    public Clob createClob() throws SQLException {
        return connection.createClob();
    }

    @Override
    public Blob createBlob() throws SQLException {
        return connection.createBlob();
    }

    @Override
    public NClob createNClob() throws SQLException {
        return connection.createNClob();
    }

    @Override
    public SQLXML createSQLXML() throws SQLException {
        return connection.createSQLXML();
    }

    @Override
    public boolean isValid(int timeout) throws SQLException {
        return connection.isValid(timeout);
    }

    @Override
    public void setClientInfo(String name, String value) throws SQLClientInfoException {
        connection.setClientInfo(name, value);
    }

    @Override
    public void setClientInfo(Properties properties) throws SQLClientInfoException {
        connection.setClientInfo(properties);
    }

    @Override
    public String getClientInfo(String name) throws SQLException {
        return connection.getClientInfo(name);
    }

    @Override
    public Properties getClientInfo() throws SQLException {
        return connection.getClientInfo();
    }

    @Override
    public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
        return connection.createArrayOf(typeName, elements);
    }

    @Override
    public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
        return connection.createStruct(typeName, attributes);
    }

    @Override
    public void setSchema(String schema) throws SQLException {
        connection.setSchema(schema);
    }

    @Override
    public String getSchema() throws SQLException {
        return connection.getSchema();
    }

    @Override
    public void abort(Executor executor) throws SQLException {
        connection.abort(executor);
    }

    @Override
    public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
        connection.setNetworkTimeout(executor, milliseconds);
    }

    @Override
    public int getNetworkTimeout() throws SQLException {
        return connection.getNetworkTimeout();
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return connection.unwrap(iface);
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return connection.isWrapperFor(iface);
    }
}
           
package org.example.proxy;

import com.codetool.common.FileHelper;
import com.codetool.common.JsonHelper;
import com.codetool.common.SnowFlakeHelper;
import org.example.call.CallContext;
import org.example.call.CallSpan;
import org.example.call.pojo.CallJdbc;

import java.io.File;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;

public class StatementWrapper {
    protected CallJdbc context;
    private static final String QUERY = "QUERY";
    private static final String EXECUTE = "EXECUTE";
    private static final String EXECUTE_UPDATE = "EXECUTE_UPDATE";
    private static final String EXECUTE_BATCH = "EXECUTE_BATCH";
    private static final String BATCH = "BATCH";

    StatementWrapper(CallJdbc context) {
        System.err.println("new Statement ...");
        CallSpan.Span span = CallContext.createEntrySpan(null);
        this.context = onStartUp(context);
        this.context.id = SnowFlakeHelper.getInstance(1000).nextId();
        this.context.thread = Thread.currentThread().getName();
        this.context.startTime = System.currentTimeMillis();
        this.context.trace = CallContext.getTrace();
        this.context.span = span.toString();
    }

    void handleStatement() {
        // 1. 擷取 sql 攜帶的參數
        Object parameters = context.context.get("parameters");
        if (parameters != null) {
            context.parameters = JsonHelper.stringify(parameters);
        }
        FileHelper.append(context.getData(), new File("D:\\tmp\\data\\" + context.trace + ".call"));
        CallContext.exitSpan();
    }

    private CallJdbc onStartUp(CallJdbc context) {
        CallJdbc app = new CallJdbc();
        app.jdbcUrl = context.jdbcUrl;
        app.username = context.username;
        app.databaseType = context.databaseType;
        app.className = context.className;
        app.type = context.type;
        app.prepareSql = context.prepareSql;
        return app;
    }


    /*==============================================================================*/
    /*================================= 查詢切面 ====================================*/
    /*==============================================================================*/

    void queryBefore(String sql) {
        context.sqlType = QUERY;
        context.finallySql = sql;
        context.startTime = System.currentTimeMillis();
    }

    void queryEnd(ResultSet resultSet) throws SQLException {
        context.endTime = System.currentTimeMillis();
        context.useTime = context.endTime - context.startTime;

        resultSet.last();
        context.result = resultSet.getRow() + "";
        resultSet.first();
    }


    /*==============================================================================*/
    /*================================= 執行切面 ====================================*/
    /*==============================================================================*/

    void executeBefore(String sql) {
        context.sqlType = EXECUTE;
        context.finallySql = sql;
        context.startTime = System.currentTimeMillis();
    }

    void executeEnd(boolean status) {
        context.endTime = System.currentTimeMillis();
        context.useTime = context.endTime - context.startTime;
        context.result = status + "";
    }


    /*==============================================================================*/
    /*================================= 修改切面 ====================================*/
    /*==============================================================================*/

    void executeUpdateBefore(String sql) {
        context.sqlType = EXECUTE_UPDATE;
        context.finallySql = sql;
        context.startTime = System.currentTimeMillis();
    }

    void executeUpdateEnd(int row) {
        context.endTime = System.currentTimeMillis();
        context.useTime = context.endTime - context.startTime;
        context.result = row + "";
    }


    /*==============================================================================*/
    /*================================= 批量切面 ====================================*/
    /*==============================================================================*/

    void executeBatchBefore(String sql) {
        context.sqlType = EXECUTE_BATCH;
        context.finallySql = sql;
        context.startTime = System.currentTimeMillis();
    }

    void executeBatchEnd(int[] result) {
        context.endTime = System.currentTimeMillis();
        context.useTime = context.endTime - context.startTime;
        context.result = Arrays.toString(result);
    }


    /*==============================================================================*/
    /*================================= 添加批量 ====================================*/
    /*==============================================================================*/

    void batchBefore(String sql) {
        context.sqlType = BATCH;
        context.finallySql += sql;
    }

    void batchClear() {
        context.finallySql = null;
    }

}
           
package org.example.proxy;

import org.example.call.pojo.CallJdbc;

import java.sql.*;

public class ProxyStatement extends StatementWrapper implements Statement {
    private Statement statement;

    public ProxyStatement(Statement statement, CallJdbc context) {
        super(context);
        this.statement = statement;
    }

    @Override
    public ResultSet executeQuery(String sql) throws SQLException {
        queryBefore(sql);
        ResultSet resultSet = statement.executeQuery(sql);
        queryEnd(resultSet);
        return resultSet;
    }

    @Override
    public int executeUpdate(String sql) throws SQLException {
        executeUpdateBefore(sql);
        int row = statement.executeUpdate(sql);
        executeUpdateEnd(row);
        return row;
    }

    @Override
    public void close() throws SQLException {
        statement.close();
        super.handleStatement();
    }

    @Override
    public int getMaxFieldSize() throws SQLException {
        return statement.getMaxFieldSize();
    }

    @Override
    public void setMaxFieldSize(int max) throws SQLException {
        statement.setMaxFieldSize(max);
    }

    @Override
    public int getMaxRows() throws SQLException {
        return statement.getMaxRows();
    }

    @Override
    public void setMaxRows(int max) throws SQLException {
        statement.setMaxRows(max);
    }

    @Override
    public void setEscapeProcessing(boolean enable) throws SQLException {
        statement.setEscapeProcessing(enable);
    }

    @Override
    public int getQueryTimeout() throws SQLException {
        return statement.getQueryTimeout();
    }

    @Override
    public void setQueryTimeout(int seconds) throws SQLException {
        statement.setQueryTimeout(seconds);
    }

    @Override
    public void cancel() throws SQLException {
        statement.cancel();
    }

    @Override
    public SQLWarning getWarnings() throws SQLException {
        return statement.getWarnings();
    }

    @Override
    public void clearWarnings() throws SQLException {
        statement.clearWarnings();
    }

    @Override
    public void setCursorName(String name) throws SQLException {
        statement.setCursorName(name);
    }

    @Override
    public boolean execute(String sql) throws SQLException {
        executeBefore(sql);
        boolean result = statement.execute(sql);
        executeEnd(result);
        return result;
    }

    @Override
    public ResultSet getResultSet() throws SQLException {
        return statement.getResultSet();
    }

    @Override
    public int getUpdateCount() throws SQLException {
        return statement.getUpdateCount();
    }

    @Override
    public boolean getMoreResults() throws SQLException {
        return statement.getMoreResults();
    }

    @Override
    public void setFetchDirection(int direction) throws SQLException {
        statement.setFetchDirection(direction);
    }

    @Override
    public int getFetchDirection() throws SQLException {
        return statement.getFetchDirection();
    }

    @Override
    public void setFetchSize(int rows) throws SQLException {
        statement.setFetchSize(rows);
    }

    @Override
    public int getFetchSize() throws SQLException {
        return statement.getFetchSize();
    }

    @Override
    public int getResultSetConcurrency() throws SQLException {
        return statement.getResultSetConcurrency();
    }

    @Override
    public int getResultSetType() throws SQLException {
        return statement.getResultSetType();
    }

    @Override
    public void addBatch(String sql) throws SQLException {
        batchBefore(sql);
        statement.addBatch(sql);
    }

    @Override
    public void clearBatch() throws SQLException {
        batchClear();
        statement.clearBatch();
    }

    @Override
    public int[] executeBatch() throws SQLException {
        executeBatchBefore("");
        int[] result = statement.executeBatch();
        executeBatchEnd(result);
        return result;
    }

    @Override
    public Connection getConnection() throws SQLException {
        return statement.getConnection();
    }

    @Override
    public boolean getMoreResults(int current) throws SQLException {
        return statement.getMoreResults(current);
    }

    @Override
    public ResultSet getGeneratedKeys() throws SQLException {
        return statement.getGeneratedKeys();
    }

    @Override
    public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
        executeUpdateBefore(sql);
        int result = statement.executeUpdate(sql, autoGeneratedKeys);
        executeUpdateEnd(result);
        return result;
    }

    @Override
    public int executeUpdate(String sql, int[] columnIndexes) throws SQLException {
        executeUpdateBefore(sql);
        int result = statement.executeUpdate(sql, columnIndexes);
        executeUpdateEnd(result);
        return result;
    }

    @Override
    public int executeUpdate(String sql, String[] columnNames) throws SQLException {
        executeUpdateBefore(sql);
        int result = statement.executeUpdate(sql, columnNames);
        executeUpdateEnd(result);
        return result;
    }

    @Override
    public boolean execute(String sql, int autoGeneratedKeys) throws SQLException {
        executeBefore(sql);
        boolean result = statement.execute(sql, autoGeneratedKeys);
        executeEnd(result);
        return result;
    }

    @Override
    public boolean execute(String sql, int[] columnIndexes) throws SQLException {
        executeBefore(sql);
        boolean result = statement.execute(sql, columnIndexes);
        executeEnd(result);
        return result;
    }

    @Override
    public boolean execute(String sql, String[] columnNames) throws SQLException {
        executeBefore(sql);
        boolean result = statement.execute(sql, columnNames);
        executeEnd(result);
        return result;
    }

    @Override
    public int getResultSetHoldability() throws SQLException {
        return statement.getResultSetHoldability();
    }

    @Override
    public boolean isClosed() throws SQLException {
        return statement.isClosed();
    }

    @Override
    public void setPoolable(boolean poolable) throws SQLException {
        statement.setPoolable(poolable);
    }

    @Override
    public boolean isPoolable() throws SQLException {
        return statement.isPoolable();
    }

    @Override
    public void closeOnCompletion() throws SQLException {
        statement.closeOnCompletion();
    }

    @Override
    public boolean isCloseOnCompletion() throws SQLException {
        return statement.isCloseOnCompletion();
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return statement.unwrap(iface);
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return statement.isWrapperFor(iface);
    }
}
           
package org.example.proxy;

import org.example.call.pojo.CallJdbc;

import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.net.URL;
import java.sql.*;
import java.util.ArrayList;
import java.util.Calendar;

public class ProxyPreparedStatement extends ProxyStatement implements PreparedStatement {
    private PreparedStatement preparedStatement;
    private ArrayList<Object> parameters;

    ProxyPreparedStatement(PreparedStatement preparedStatement, CallJdbc context) {
        super(preparedStatement, context);
        try {
            parameters = new ArrayList<>();
            int parameterCount = preparedStatement.getParameterMetaData().getParameterCount();
            for (int i = 0; i < parameterCount; i++) {
                parameters.add(null);
            }
            context.context.put("parameters", parameters);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        this.preparedStatement = preparedStatement;
    }

    private String getSql() {
        String sql = preparedStatement.toString();
        if (sql.contains(":")) {
            sql = sql.substring(sql.indexOf(":") + 2);
        }
        return sql;
    }

    @Override
    public ResultSet executeQuery() throws SQLException {
        queryBefore(getSql());
        ResultSet resultSet = preparedStatement.executeQuery();
        queryEnd(resultSet);
        return resultSet;
    }

    @Override
    public int executeUpdate() throws SQLException {
        executeUpdateBefore(getSql());
        int row = preparedStatement.executeUpdate();
        executeUpdateEnd(row);
        return row;
    }

    @Override
    public void setNull(int parameterIndex, int sqlType) throws SQLException {
        parameters.set(parameterIndex - 1, null);
        preparedStatement.setNull(parameterIndex, sqlType);
    }

    @Override
    public void setBoolean(int parameterIndex, boolean x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setBoolean(parameterIndex, x);
    }

    @Override
    public void setByte(int parameterIndex, byte x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setByte(parameterIndex, x);
    }

    @Override
    public void setShort(int parameterIndex, short x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setShort(parameterIndex, x);
    }

    @Override
    public void setInt(int parameterIndex, int x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setInt(parameterIndex, x);
    }

    @Override
    public void setLong(int parameterIndex, long x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setLong(parameterIndex, x);
    }

    @Override
    public void setFloat(int parameterIndex, float x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setFloat(parameterIndex, x);
    }

    @Override
    public void setDouble(int parameterIndex, double x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setDouble(parameterIndex, x);
    }

    @Override
    public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setBigDecimal(parameterIndex, x);
    }

    @Override
    public void setString(int parameterIndex, String x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setString(parameterIndex, x);
    }

    @Override
    public void setBytes(int parameterIndex, byte[] x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setBytes(parameterIndex, x);
    }

    @Override
    public void setDate(int parameterIndex, Date x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setDate(parameterIndex, x);
    }

    @Override
    public void setTime(int parameterIndex, Time x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setTime(parameterIndex, x);
    }

    @Override
    public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setTimestamp(parameterIndex, x);
    }

    @Override
    public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setAsciiStream(parameterIndex, x, length);
    }

    @Override
    public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setUnicodeStream(parameterIndex, x, length);
    }

    @Override
    public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setBinaryStream(parameterIndex, x, length);
    }

    @Override
    public void clearParameters() throws SQLException {
        parameters.clear();
        preparedStatement.clearParameters();
    }

    @Override
    public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setObject(parameterIndex, x, targetSqlType);
    }

    @Override
    public void setObject(int parameterIndex, Object x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setObject(parameterIndex, x);
    }

    @Override
    public boolean execute() throws SQLException {
        executeBefore(getSql());
        boolean result = preparedStatement.execute();
        executeEnd(result);
        return result;
    }

    @Override
    public void addBatch() throws SQLException {
        preparedStatement.addBatch();
    }

    @Override
    public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException {
        parameters.set(parameterIndex - 1, reader);
        preparedStatement.setCharacterStream(parameterIndex, reader, length);
    }

    @Override
    public void setRef(int parameterIndex, Ref x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setRef(parameterIndex, x);
    }

    @Override
    public void setBlob(int parameterIndex, Blob x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setBlob(parameterIndex, x);
    }

    @Override
    public void setClob(int parameterIndex, Clob x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setClob(parameterIndex, x);
    }

    @Override
    public void setArray(int parameterIndex, Array x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setArray(parameterIndex, x);
    }

    @Override
    public ResultSetMetaData getMetaData() throws SQLException {
        return preparedStatement.getMetaData();
    }

    @Override
    public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setDate(parameterIndex, x, cal);
    }

    @Override
    public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setTime(parameterIndex, x, cal);
    }

    @Override
    public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setTimestamp(parameterIndex, x, cal);
    }

    @Override
    public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException {
        parameters.set(parameterIndex - 1, null);
        preparedStatement.setNull(parameterIndex, sqlType, typeName);
    }

    @Override
    public void setURL(int parameterIndex, URL x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setURL(parameterIndex, x);
    }

    @Override
    public ParameterMetaData getParameterMetaData() throws SQLException {
        return preparedStatement.getParameterMetaData();
    }

    @Override
    public void setRowId(int parameterIndex, RowId x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setRowId(parameterIndex, x);
    }

    @Override
    public void setNString(int parameterIndex, String value) throws SQLException {
        parameters.set(parameterIndex - 1, value);
        preparedStatement.setNString(parameterIndex, value);
    }

    @Override
    public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException {
        parameters.set(parameterIndex - 1, value);
        preparedStatement.setNCharacterStream(parameterIndex, value, length);
    }

    @Override
    public void setNClob(int parameterIndex, NClob value) throws SQLException {
        parameters.set(parameterIndex - 1, value);
        preparedStatement.setNClob(parameterIndex, value);
    }

    @Override
    public void setClob(int parameterIndex, Reader reader, long length) throws SQLException {
        parameters.set(parameterIndex - 1, reader);
        preparedStatement.setClob(parameterIndex, reader, length);
    }

    @Override
    public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException {
        parameters.set(parameterIndex - 1, inputStream);
        preparedStatement.setBlob(parameterIndex, inputStream, length);
    }

    @Override
    public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException {
        parameters.set(parameterIndex - 1, reader);
        preparedStatement.setNClob(parameterIndex, reader, length);
    }

    @Override
    public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException {
        parameters.set(parameterIndex - 1, xmlObject);
        preparedStatement.setSQLXML(parameterIndex, xmlObject);
    }

    @Override
    public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setObject(parameterIndex, x, targetSqlType, scaleOrLength);
    }

    @Override
    public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setAsciiStream(parameterIndex, x, length);
    }

    @Override
    public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setBinaryStream(parameterIndex, x, length);
    }

    @Override
    public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException {
        parameters.set(parameterIndex - 1, reader);
        preparedStatement.setCharacterStream(parameterIndex, reader, length);
    }

    @Override
    public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setAsciiStream(parameterIndex, x);
    }

    @Override
    public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException {
        parameters.set(parameterIndex - 1, x);
        preparedStatement.setBinaryStream(parameterIndex, x);
    }

    @Override
    public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException {
        parameters.set(parameterIndex - 1, reader);
        preparedStatement.setCharacterStream(parameterIndex, reader);
    }

    @Override
    public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException {
        parameters.set(parameterIndex - 1, value);
        preparedStatement.setNCharacterStream(parameterIndex, value);
    }

    @Override
    public void setClob(int parameterIndex, Reader reader) throws SQLException {
        parameters.set(parameterIndex - 1, reader);
        preparedStatement.setClob(parameterIndex, reader);
    }

    @Override
    public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException {
        parameters.set(parameterIndex - 1, inputStream);
        preparedStatement.setBlob(parameterIndex, inputStream);
    }

    @Override
    public void setNClob(int parameterIndex, Reader reader) throws SQLException {
        parameters.set(parameterIndex - 1, reader);
        preparedStatement.setNClob(parameterIndex, reader);
    }

}
           
package org.example.proxy;

import org.example.call.pojo.CallJdbc;

import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.net.URL;
import java.sql.*;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;

public class ProxyCallableStatement extends ProxyPreparedStatement implements CallableStatement {
    private CallableStatement callableStatement;
    private Map<String, Object> parameterMap = new HashMap<>();

    public ProxyCallableStatement(CallableStatement callableStatement, CallJdbc context) {
        super(callableStatement, context);
        context.context.put("parameters", parameterMap);
        this.callableStatement = callableStatement;
    }

    @Override
    public void registerOutParameter(int parameterIndex, int sqlType) throws SQLException {
        callableStatement.registerOutParameter(parameterIndex, sqlType);
    }

    @Override
    public void registerOutParameter(int parameterIndex, int sqlType, int scale) throws SQLException {
        callableStatement.registerOutParameter(parameterIndex, sqlType, scale);
    }

    @Override
    public boolean wasNull() throws SQLException {
        return callableStatement.wasNull();
    }

    @Override
    public String getString(int parameterIndex) throws SQLException {
        return callableStatement.getString(parameterIndex);
    }

    @Override
    public boolean getBoolean(int parameterIndex) throws SQLException {
        return callableStatement.getBoolean(parameterIndex);
    }

    @Override
    public byte getByte(int parameterIndex) throws SQLException {
        return callableStatement.getByte(parameterIndex);
    }

    @Override
    public short getShort(int parameterIndex) throws SQLException {
        return callableStatement.getShort(parameterIndex);
    }

    @Override
    public int getInt(int parameterIndex) throws SQLException {
        return callableStatement.getInt(parameterIndex);
    }

    @Override
    public long getLong(int parameterIndex) throws SQLException {
        return callableStatement.getLong(parameterIndex);
    }

    @Override
    public float getFloat(int parameterIndex) throws SQLException {
        return callableStatement.getFloat(parameterIndex);
    }

    @Override
    public double getDouble(int parameterIndex) throws SQLException {
        return callableStatement.getDouble(parameterIndex);
    }

    @Override
    public BigDecimal getBigDecimal(int parameterIndex, int scale) throws SQLException {
        return callableStatement.getBigDecimal(parameterIndex, scale);
    }

    @Override
    public byte[] getBytes(int parameterIndex) throws SQLException {
        return callableStatement.getBytes(parameterIndex);
    }

    @Override
    public Date getDate(int parameterIndex) throws SQLException {
        return callableStatement.getDate(parameterIndex);
    }

    @Override
    public Time getTime(int parameterIndex) throws SQLException {
        return callableStatement.getTime(parameterIndex);
    }

    @Override
    public Timestamp getTimestamp(int parameterIndex) throws SQLException {
        return callableStatement.getTimestamp(parameterIndex);
    }

    @Override
    public Object getObject(int parameterIndex) throws SQLException {
        return callableStatement.getObject(parameterIndex);
    }

    @Override
    public BigDecimal getBigDecimal(int parameterIndex) throws SQLException {
        return callableStatement.getBigDecimal(parameterIndex);
    }

    @Override
    public Object getObject(int parameterIndex, Map<String, Class<?>> map) throws SQLException {
        return callableStatement.getObject(parameterIndex, map);
    }

    @Override
    public Ref getRef(int parameterIndex) throws SQLException {
        return callableStatement.getRef(parameterIndex);
    }

    @Override
    public Blob getBlob(int parameterIndex) throws SQLException {
        return callableStatement.getBlob(parameterIndex);
    }

    @Override
    public Clob getClob(int parameterIndex) throws SQLException {
        return callableStatement.getClob(parameterIndex);
    }

    @Override
    public Array getArray(int parameterIndex) throws SQLException {
        return callableStatement.getArray(parameterIndex);
    }

    @Override
    public Date getDate(int parameterIndex, Calendar cal) throws SQLException {
        return callableStatement.getDate(parameterIndex, cal);
    }

    @Override
    public Time getTime(int parameterIndex, Calendar cal) throws SQLException {
        return callableStatement.getTime(parameterIndex, cal);
    }

    @Override
    public Timestamp getTimestamp(int parameterIndex, Calendar cal) throws SQLException {
        return callableStatement.getTimestamp(parameterIndex, cal);
    }

    @Override
    public void registerOutParameter(int parameterIndex, int sqlType, String typeName) throws SQLException {
        callableStatement.registerOutParameter(parameterIndex, sqlType, typeName);
    }

    @Override
    public void registerOutParameter(String parameterName, int sqlType) throws SQLException {
        callableStatement.registerOutParameter(parameterName, sqlType);
    }

    @Override
    public void registerOutParameter(String parameterName, int sqlType, int scale) throws SQLException {
        callableStatement.registerOutParameter(parameterName, sqlType, scale);
    }

    @Override
    public void registerOutParameter(String parameterName, int sqlType, String typeName) throws SQLException {
        callableStatement.registerOutParameter(parameterName, sqlType, typeName);
    }

    @Override
    public URL getURL(int parameterIndex) throws SQLException {
        return callableStatement.getURL(parameterIndex);
    }

    @Override
    public void setURL(String parameterName, URL val) throws SQLException {
        parameterMap.put(parameterName, val);
        callableStatement.setURL(parameterName, val);
    }

    @Override
    public void setNull(String parameterName, int sqlType) throws SQLException {
        parameterMap.put(parameterName, null);
        callableStatement.setNull(parameterName, sqlType);
    }

    @Override
    public void setBoolean(String parameterName, boolean x) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setBoolean(parameterName, x);
    }

    @Override
    public void setByte(String parameterName, byte x) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setByte(parameterName, x);
    }

    @Override
    public void setShort(String parameterName, short x) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setShort(parameterName, x);
    }

    @Override
    public void setInt(String parameterName, int x) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setInt(parameterName, x);
    }

    @Override
    public void setLong(String parameterName, long x) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setLong(parameterName, x);
    }

    @Override
    public void setFloat(String parameterName, float x) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setFloat(parameterName, x);
    }

    @Override
    public void setDouble(String parameterName, double x) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setDouble(parameterName, x);
    }

    @Override
    public void setBigDecimal(String parameterName, BigDecimal x) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setBigDecimal(parameterName, x);
    }

    @Override
    public void setString(String parameterName, String x) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setString(parameterName, x);
    }

    @Override
    public void setBytes(String parameterName, byte[] x) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setBytes(parameterName, x);
    }

    @Override
    public void setDate(String parameterName, Date x) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setDate(parameterName, x);
    }

    @Override
    public void setTime(String parameterName, Time x) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setTime(parameterName, x);
    }

    @Override
    public void setTimestamp(String parameterName, Timestamp x) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setTimestamp(parameterName, x);
    }

    @Override
    public void setAsciiStream(String parameterName, InputStream x, int length) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setAsciiStream(parameterName, x);
    }

    @Override
    public void setBinaryStream(String parameterName, InputStream x, int length) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setBinaryStream(parameterName, x);
    }

    @Override
    public void setObject(String parameterName, Object x, int targetSqlType, int scale) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setObject(parameterName, x, targetSqlType, scale);
    }

    @Override
    public void setObject(String parameterName, Object x, int targetSqlType) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setObject(parameterName, x, targetSqlType);
    }

    @Override
    public void setObject(String parameterName, Object x) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setObject(parameterName, x);
    }

    @Override
    public void setCharacterStream(String parameterName, Reader reader, int length) throws SQLException {
        parameterMap.put(parameterName, reader);
        callableStatement.setCharacterStream(parameterName, reader, length);
    }

    @Override
    public void setDate(String parameterName, Date x, Calendar cal) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setDate(parameterName, x, cal);
    }

    @Override
    public void setTime(String parameterName, Time x, Calendar cal) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setTime(parameterName, x, cal);
    }

    @Override
    public void setTimestamp(String parameterName, Timestamp x, Calendar cal) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setTimestamp(parameterName, x, cal);
    }

    @Override
    public void setNull(String parameterName, int sqlType, String typeName) throws SQLException {
        parameterMap.put(parameterName, null);
        callableStatement.setNull(parameterName, sqlType, typeName);
    }

    @Override
    public String getString(String parameterName) throws SQLException {
        return callableStatement.getString(parameterName);
    }

    @Override
    public boolean getBoolean(String parameterName) throws SQLException {
        return callableStatement.getBoolean(parameterName);
    }

    @Override
    public byte getByte(String parameterName) throws SQLException {
        return callableStatement.getByte(parameterName);
    }

    @Override
    public short getShort(String parameterName) throws SQLException {
        return callableStatement.getShort(parameterName);
    }

    @Override
    public int getInt(String parameterName) throws SQLException {
        return callableStatement.getInt(parameterName);
    }

    @Override
    public long getLong(String parameterName) throws SQLException {
        return callableStatement.getLong(parameterName);
    }

    @Override
    public float getFloat(String parameterName) throws SQLException {
        return callableStatement.getFloat(parameterName);
    }

    @Override
    public double getDouble(String parameterName) throws SQLException {
        return callableStatement.getDouble(parameterName);
    }

    @Override
    public byte[] getBytes(String parameterName) throws SQLException {
        return callableStatement.getBytes(parameterName);
    }

    @Override
    public Date getDate(String parameterName) throws SQLException {
        return callableStatement.getDate(parameterName);
    }

    @Override
    public Time getTime(String parameterName) throws SQLException {
        return callableStatement.getTime(parameterName);
    }

    @Override
    public Timestamp getTimestamp(String parameterName) throws SQLException {
        return callableStatement.getTimestamp(parameterName);
    }

    @Override
    public Object getObject(String parameterName) throws SQLException {
        return callableStatement.getObject(parameterName);
    }

    @Override
    public BigDecimal getBigDecimal(String parameterName) throws SQLException {
        return callableStatement.getBigDecimal(parameterName);
    }

    @Override
    public Object getObject(String parameterName, Map<String, Class<?>> map) throws SQLException {
        return callableStatement.getObject(parameterName, map);
    }

    @Override
    public Ref getRef(String parameterName) throws SQLException {
        return callableStatement.getRef(parameterName);
    }

    @Override
    public Blob getBlob(String parameterName) throws SQLException {
        return callableStatement.getBlob(parameterName);
    }

    @Override
    public Clob getClob(String parameterName) throws SQLException {
        return callableStatement.getClob(parameterName);
    }

    @Override
    public Array getArray(String parameterName) throws SQLException {
        return callableStatement.getArray(parameterName);
    }

    @Override
    public Date getDate(String parameterName, Calendar cal) throws SQLException {
        return callableStatement.getDate(parameterName, cal);
    }

    @Override
    public Time getTime(String parameterName, Calendar cal) throws SQLException {
        return callableStatement.getTime(parameterName, cal);
    }

    @Override
    public Timestamp getTimestamp(String parameterName, Calendar cal) throws SQLException {
        return callableStatement.getTimestamp(parameterName, cal);
    }

    @Override
    public URL getURL(String parameterName) throws SQLException {
        return callableStatement.getURL(parameterName);
    }

    @Override
    public RowId getRowId(int parameterIndex) throws SQLException {
        return callableStatement.getRowId(parameterIndex);
    }

    @Override
    public RowId getRowId(String parameterName) throws SQLException {
        return callableStatement.getRowId(parameterName);
    }

    @Override
    public void setRowId(String parameterName, RowId x) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setRowId(parameterName, x);
    }

    @Override
    public void setNString(String parameterName, String value) throws SQLException {
        parameterMap.put(parameterName, value);
        callableStatement.setNString(parameterName, value);
    }

    @Override
    public void setNCharacterStream(String parameterName, Reader value, long length) throws SQLException {
        parameterMap.put(parameterName, value);
        callableStatement.setNCharacterStream(parameterName, value, length);
    }

    @Override
    public void setNClob(String parameterName, NClob value) throws SQLException {
        parameterMap.put(parameterName, value);
        callableStatement.setNClob(parameterName, value);
    }

    @Override
    public void setClob(String parameterName, Reader reader, long length) throws SQLException {
        parameterMap.put(parameterName, reader);
        callableStatement.setClob(parameterName, reader, length);
    }

    @Override
    public void setBlob(String parameterName, InputStream inputStream, long length) throws SQLException {
        parameterMap.put(parameterName, inputStream);
        callableStatement.setBlob(parameterName, inputStream, length);
    }

    @Override
    public void setNClob(String parameterName, Reader reader, long length) throws SQLException {
        parameterMap.put(parameterName, reader);
        callableStatement.setNClob(parameterName, reader, length);
    }

    @Override
    public NClob getNClob(int parameterIndex) throws SQLException {
        return callableStatement.getNClob(parameterIndex);
    }

    @Override
    public NClob getNClob(String parameterName) throws SQLException {
        return callableStatement.getNClob(parameterName);
    }

    @Override
    public void setSQLXML(String parameterName, SQLXML xmlObject) throws SQLException {
        parameterMap.put(parameterName, xmlObject);
        callableStatement.setSQLXML(parameterName, xmlObject);
    }

    @Override
    public SQLXML getSQLXML(int parameterIndex) throws SQLException {
        return callableStatement.getSQLXML(parameterIndex);
    }

    @Override
    public SQLXML getSQLXML(String parameterName) throws SQLException {
        return callableStatement.getSQLXML(parameterName);
    }

    @Override
    public String getNString(int parameterIndex) throws SQLException {
        return callableStatement.getNString(parameterIndex);
    }

    @Override
    public String getNString(String parameterName) throws SQLException {
        return callableStatement.getNString(parameterName);
    }

    @Override
    public Reader getNCharacterStream(int parameterIndex) throws SQLException {
        return callableStatement.getNCharacterStream(parameterIndex);
    }

    @Override
    public Reader getNCharacterStream(String parameterName) throws SQLException {
        return callableStatement.getNCharacterStream(parameterName);
    }

    @Override
    public Reader getCharacterStream(int parameterIndex) throws SQLException {
        return callableStatement.getCharacterStream(parameterIndex);
    }

    @Override
    public Reader getCharacterStream(String parameterName) throws SQLException {
        return callableStatement.getCharacterStream(parameterName);
    }

    @Override
    public void setBlob(String parameterName, Blob x) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setBlob(parameterName, x);
    }

    @Override
    public void setClob(String parameterName, Clob x) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setClob(parameterName, x);
    }

    @Override
    public void setAsciiStream(String parameterName, InputStream x, long length) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setAsciiStream(parameterName, x, length);
    }

    @Override
    public void setBinaryStream(String parameterName, InputStream x, long length) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setBinaryStream(parameterName, x, length);
    }

    @Override
    public void setCharacterStream(String parameterName, Reader reader, long length) throws SQLException {
        parameterMap.put(parameterName, reader);
        callableStatement.setCharacterStream(parameterName, reader, length);
    }

    @Override
    public void setAsciiStream(String parameterName, InputStream x) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setAsciiStream(parameterName, x);
    }

    @Override
    public void setBinaryStream(String parameterName, InputStream x) throws SQLException {
        parameterMap.put(parameterName, x);
        callableStatement.setBinaryStream(parameterName, x);
    }

    @Override
    public void setCharacterStream(String parameterName, Reader reader) throws SQLException {
        parameterMap.put(parameterName, reader);
        callableStatement.setCharacterStream(parameterName, reader);
    }

    @Override
    public void setNCharacterStream(String parameterName, Reader value) throws SQLException {
        parameterMap.put(parameterName, value);
        callableStatement.setNCharacterStream(parameterName, value);
    }

    @Override
    public void setClob(String parameterName, Reader reader) throws SQLException {
        parameterMap.put(parameterName, reader);
        callableStatement.setClob(parameterName, reader);
    }

    @Override
    public void setBlob(String parameterName, InputStream inputStream) throws SQLException {
        parameterMap.put(parameterName, inputStream);
        callableStatement.setBlob(parameterName, inputStream);
    }

    @Override
    public void setNClob(String parameterName, Reader reader) throws SQLException {
        parameterMap.put(parameterName, reader);
        callableStatement.setNClob(parameterName, reader);
    }

    @Override
    public <T> T getObject(int parameterIndex, Class<T> type) throws SQLException {
        return callableStatement.getObject(parameterIndex, type);
    }

    @Override
    public <T> T getObject(String parameterName, Class<T> type) throws SQLException {
        return callableStatement.getObject(parameterName, type);
    }
}
           

最後, 在 AgentApplication 添加 Jdbc 插樁

package org.example;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.LoaderClassPath;
import org.apache.commons.lang3.StringUtils;
import org.example.context.VMInfo;
import org.example.filter.FilterChain;
import org.example.filter.support.HttpServletFilterChain;
import org.example.filter.support.SpringControllerFilterChain;
import org.example.filter.support.SpringJdbcFilterChain;
import org.example.filter.support.SpringServiceFilterChain;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class AgentApplication implements ClassFileTransformer {
    private final List<FilterChain> chains = new ArrayList<>();
    private static final byte[] NO_TRANSFORM = null;

    private AgentApplication() {
        chains.add(new HttpServletFilterChain());
        chains.add(new SpringControllerFilterChain());
        chains.add(new SpringServiceFilterChain());
        chains.add(new SpringJdbcFilterChain());
    }


    public static void premain(String args, Instrumentation instrumentation) {
        boolean showVm = false;
        if (StringUtils.isNoneBlank(args) && args.equals("debug")) {
            showVm = true;
        }

        // 開始 JVM 監控
        if (showVm) {
            // 開啟一個線程
            // 每隔 1分鐘 執行一次
            Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> {
                VMInfo.memoryInfo();
                VMInfo.gcInfo();
                System.out.println();
            }, 0, 60, TimeUnit.SECONDS);
        }

        System.out.println("          _____                   _______                   _____                    _____                _____                    _____                    _____");
        System.out.println("         /\\    \\                 /::\\    \\                 /\\    \\                  /\\    \\              /\\    \\                  /\\    \\                  /\\    \\ ");
        System.out.println("        /::\\____\\               /::::\\    \\               /::\\____\\                /::\\    \\            /::\\    \\                /::\\    \\                /::\\    \\ ");
        System.out.println("       /::::|   |              /::::::\\    \\             /::::|   |                \\:::\\    \\           \\:::\\    \\              /::::\\    \\              /::::\\    \\ ");
        System.out.println("      /:::::|   |             /::::::::\\    \\           /:::::|   |                 \\:::\\    \\           \\:::\\    \\            /::::::\\    \\            /::::::\\    \\ ");
        System.out.println("     /::::::|   |            /:::/~~\\:::\\    \\         /::::::|   |                  \\:::\\    \\           \\:::\\    \\          /:::/\\:::\\    \\          /:::/\\:::\\    \\ ");
        System.out.println("    /:::/|::|   |           /:::/    \\:::\\    \\       /:::/|::|   |                   \\:::\\    \\           \\:::\\    \\        /:::/__\\:::\\    \\        /:::/__\\:::\\    \\ ");
        System.out.println("   /:::/ |::|   |          /:::/    / \\:::\\    \\     /:::/ |::|   |                   /::::\\    \\          /::::\\    \\      /::::\\   \\:::\\    \\      /::::\\   \\:::\\    \\ ");
        System.out.println("  /:::/  |::|___|______   /:::/____/   \\:::\\____\\   /:::/  |::|   | _____    ____    /::::::\\    \\        /::::::\\    \\    /::::::\\   \\:::\\    \\    /::::::\\   \\:::\\    \\ ");
        System.out.println(" /:::/   |::::::::\\    \\ |:::|    |     |:::|    | /:::/   |::|   |/\\    \\  /\\   \\  /:::/\\:::\\    \\      /:::/\\:::\\    \\  /:::/\\:::\\   \\:::\\    \\  /:::/\\:::\\   \\:::\\____\\ ");
        System.out.println("/:::/    |:::::::::\\____\\|:::|____|     |:::|    |/:: /    |::|   /::\\____\\/::\\   \\/:::/  \\:::\\____\\    /:::/  \\:::\\____\\/:::/__\\:::\\   \\:::\\____\\/:::/  \\:::\\   \\:::|    |");
        System.out.println("\\::/    / ~~~~~/:::/    / \\:::\\    \\   /:::/    / \\::/    /|::|  /:::/    /\\:::\\  /:::/    \\::/    /   /:::/    \\::/    /\\:::\\   \\:::\\   \\::/    /\\::/   |::::\\  /:::|____|");
        System.out.println(" \\/____/      /:::/    /   \\:::\\    \\ /:::/    /   \\/____/ |::| /:::/    /  \\:::\\/:::/    / \\/____/   /:::/    / \\/____/  \\:::\\   \\:::\\   \\/____/  \\/____|:::::\\/:::/    /");
        System.out.println("             /:::/    /     \\:::\\    /:::/    /            |::|/:::/    /    \\::::::/    /           /:::/    /            \\:::\\   \\:::\\    \\            |:::::::::/    /");
        System.out.println("            /:::/    /       \\:::\\__/:::/    /             |::::::/    /      \\::::/____/           /:::/    /              \\:::\\   \\:::\\____\\           |::|\\::::/    /");
        System.out.println("           /:::/    /         \\::::::::/    /              |:::::/    /        \\:::\\    \\           \\::/    /                \\:::\\   \\::/    /           |::| \\::/____/");
        System.out.println("          /:::/    /           \\::::::/    /               |::::/    /          \\:::\\    \\           \\/____/                  \\:::\\   \\/____/            |::|  ~|");
        System.out.println("         /:::/    /             \\::::/    /                /:::/    /            \\:::\\    \\                                    \\:::\\    \\                |::|   |");
        System.out.println("        /:::/    /               \\::/____/                /:::/    /              \\:::\\____\\                                    \\:::\\____\\               \\::|   |");
        System.out.println("        \\::/    /                 ~~                      \\::/    /                \\::/    /                                     \\::/    /                \\:|   |");
        System.out.println("         \\/____/                                           \\/____/                  \\/____/                                       \\/____/                  \\|___|");
        System.out.println();
        instrumentation.addTransformer(new AgentApplication());
    }


    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
            if (className == null) {
                return NO_TRANSFORM;
            }
            String finalClassName = className.replace("/", ".");

            ClassPool pool = new ClassPool(true);
            if (loader != null) {
                pool.insertClassPath(new LoaderClassPath(loader));
            } else {
                pool.insertClassPath(new LoaderClassPath(ClassLoader.getSystemClassLoader()));
            }

            for (FilterChain chain : chains) {
                CtClass sourceClass = pool.getCtClass(finalClassName);
                if (chain.isTargetClass(finalClassName, sourceClass)) {
                    System.err.println("嘗試對類: " + className + " 進行增強");
                    try {
                        return chain.processingAgentClass(loader, sourceClass, finalClassName);
                    } catch (Exception e) {
                        System.out.println("無法對類 " + className + " 進行增強, 具體的錯誤原因是: " + e.toString());
                    }
                }
            }

        } catch (Exception e) {
            // TODO ...
        }

        return NO_TRANSFORM;
    }


}
           

此時, 整個流程到這裡就結束了. 現在我們隻需要将 trace 資訊收集起來然後以可視化的方式展示. 就 OK 了

具體的源碼位址: https://gitee.com/tomcatBbzzzs/call-chain

繼續閱讀