天天看点

基于 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

继续阅读