上一章中, 基本上将 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 的简单例子, 我们的调用链会将上面的整个调用链路给梳理出来并且记录到日志中. 首先, 它们的调用关系如下:

在第一篇文章中, 是使用 Stack 来记录调用层级关系的, 但是那个代码只能记录调用层级, 而不能记录具体的调用顺序. 得到的结果只能得出如上图那样的结果, 而我们需要实现的功能是这样的, 能够记录每一个方法的调用层级, 同一个层级下的两个方法存在不同的值来表示它们在同一个层级中的调用顺序

接下来, 使用代码实现这个功能, 这里我使用了一个栈和指针来实现这样的功能. 整个流程是这样的:

首先, 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)

当方法指向完毕以后, 我们只需要方向移动指针的位置就可以了. 比如 r.r1 指向完毕以后, 调用链会到了 s.s1. 此时指针就有 0 -> 1 -> 1 变成了 0 -> 1. 现在开始指向后面的部分. 然后方法继续运行. 当 s.s1 方法执行完毕后. 执行 s.s2 方法, 此时. span 的变化如下

当调用 s.s2 时, 此时 0 -> 1 已经存在了, 于是我们只能创建 0 -> 2, 然后指针指向 0 -> 2, 接着调用 r.r2. 由于 0 -> 2 下面没有子节点. 所以创建一个 0 -> 2 -> 1. 然后等待执行完毕后. 开始反向调整指针.

此时, 整个流程就已经走完了. 整个原理就是这样. 接下来我们使用代码来进行实现:

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;}@Overrideprotected TreeSpan clone() {return new TreeSpan(pointSpan.toString());}@Deprecatedpublic 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.1span.createEntrySpan(); // 0.1.1span.exitSpan();        // 0.1span.createEntrySpan(); // 0.1.2System.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;}@Overrideprotected 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 里面把这两行代码插进去就可以了.  好的, 整个调用链就写完了....

就这, 就这. 就这? 就结束了?

这里已经完成了最基本的调用链追踪, 接下来要配合信息收集. 从最外层开始收集, 知道最内层. 也就是 servlet -> web -> service -> jdbc. 写了很多之后发现, 其实有很多通用的部分, 于是基于面向对象的设计原理, 首先抽象出一个接口, 然后巴拉巴拉巴拉..., 最终结构图如下:

首先有一个最大的接口 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 {// 主键 IDpublic Long id = SnowFlakeHelper.getInstance(1000).nextId();// 调用链的类型, SERVLET | WEB | SERVICE | JDBCpublic 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, 然后修改原来方法的方法体, 大致原理如下:

原始的 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 {@Overrideprotected 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");}@Overrideprotected 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;}@Overridepublic void before(BaseCall context) {Object[] paramValues = (Object[]) context.context.get("values");HttpServletRequest request = (HttpServletRequest) paramValues[0];// 获取 span 和 traceString 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();}@Overridepublic 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);}@Overridepublic boolean isTargetClass(String className, CtClass ctClass) {return servletClassName.equals(className);}@Overridepublic 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());}@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {try {// 如果当前类名称是 null, 则直接返回 nullif (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 采集的信息

为了避免影响业务系统的效率, 调用链系统只是负责生成sql语句, 然后存在另外一个系统来处理这些 sql 语句.  补充一点: 在给 traceId 命名时附带一些其他信息可以更加方便我们进行统计, 比如 alibaba 的鹰眼的全局唯一的ID

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 {@Overridepublic 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"};@Overridepublic void before(BaseCall context) {CallSpan.Span span = CallContext.createEntrySpan(null);context.trace = CallContext.getTrace();context.span = span.toString();}@Overridepublic 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;}@Overridepublic 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());}@Overridepublic 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 {@Overridepublic 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";@Overridepublic 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;}}@Overridepublic void before(BaseCall context) {CallSpan.Span span = CallContext.createEntrySpan(null);context.span = span.toString();context.trace = CallContext.getTrace();}@Overridepublic void finale(BaseCall context) {super.finale(context);}@Overridepublic 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());}@Overridepublic 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 {// 编译前的 sqlpublic String prepareSql;// sql 语句使用的 Statement 类型public String statementType;// 数据库类型public String databaseType;// sql 语句的类型, QUERY, EXECUTE, EXECUTE_UPDATEpublic String sqlType;// 数据库连接地址public String jdbcUrl;// 用户名称public String username;// 最终运行的 sqlpublic String finallySql;// sql 携带的参数public String parameters;@Overridepublic 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"};@Overridepublic 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);}@Overridepublic 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();}@Overridepublic Statement createStatement() throws SQLException {context.statementType = STATEMENT;context.className = "java.sql.Statement";return new ProxyStatement(connection.createStatement(), context);}@Overridepublic 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);}@Overridepublic 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);}@Overridepublic 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);}@Overridepublic void setAutoCommit(boolean autoCommit) throws SQLException {connection.setAutoCommit(autoCommit);}@Overridepublic boolean getAutoCommit() throws SQLException {return connection.getAutoCommit();}@Overridepublic void commit() throws SQLException {connection.commit();}@Overridepublic void rollback() throws SQLException {System.err.println("close Connection ...");connection.rollback();}@Overridepublic void close() throws SQLException {connection.close();}@Overridepublic boolean isClosed() throws SQLException {return connection.isClosed();}@Overridepublic DatabaseMetaData getMetaData() throws SQLException {return connection.getMetaData();}@Overridepublic void setReadOnly(boolean readOnly) throws SQLException {connection.setReadOnly(readOnly);}@Overridepublic boolean isReadOnly() throws SQLException {return connection.isReadOnly();}@Overridepublic void setCatalog(String catalog) throws SQLException {connection.setCatalog(catalog);}@Overridepublic String getCatalog() throws SQLException {return connection.getCatalog();}@Overridepublic void setTransactionIsolation(int level) throws SQLException {connection.setTransactionIsolation(level);}@Overridepublic int getTransactionIsolation() throws SQLException {return connection.getTransactionIsolation();}@Overridepublic SQLWarning getWarnings() throws SQLException {return connection.getWarnings();}@Overridepublic void clearWarnings() throws SQLException {connection.clearWarnings();}@Overridepublic Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {context.statementType = STATEMENT;context.className = "java.sql.Statement";return new ProxyStatement(connection.createStatement(resultSetType, resultSetConcurrency), context);}@Overridepublic 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);}@Overridepublic 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);}@Overridepublic Map<String, Class<?>> getTypeMap() throws SQLException {return connection.getTypeMap();}@Overridepublic void setTypeMap(Map<String, Class<?>> map) throws SQLException {connection.setTypeMap(map);}@Overridepublic void setHoldability(int holdability) throws SQLException {connection.setHoldability(holdability);}@Overridepublic int getHoldability() throws SQLException {return connection.getHoldability();}@Overridepublic Savepoint setSavepoint() throws SQLException {return connection.setSavepoint();}@Overridepublic Savepoint setSavepoint(String name) throws SQLException {return connection.setSavepoint(name);}@Overridepublic void rollback(Savepoint savepoint) throws SQLException {connection.rollback(savepoint);}@Overridepublic void releaseSavepoint(Savepoint savepoint) throws SQLException {connection.releaseSavepoint(savepoint);}@Overridepublic 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);}@Overridepublic 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);}@Overridepublic 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);}@Overridepublic 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);}@Overridepublic 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);}@Overridepublic 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);}@Overridepublic Clob createClob() throws SQLException {return connection.createClob();}@Overridepublic Blob createBlob() throws SQLException {return connection.createBlob();}@Overridepublic NClob createNClob() throws SQLException {return connection.createNClob();}@Overridepublic SQLXML createSQLXML() throws SQLException {return connection.createSQLXML();}@Overridepublic boolean isValid(int timeout) throws SQLException {return connection.isValid(timeout);}@Overridepublic void setClientInfo(String name, String value) throws SQLClientInfoException {connection.setClientInfo(name, value);}@Overridepublic void setClientInfo(Properties properties) throws SQLClientInfoException {connection.setClientInfo(properties);}@Overridepublic String getClientInfo(String name) throws SQLException {return connection.getClientInfo(name);}@Overridepublic Properties getClientInfo() throws SQLException {return connection.getClientInfo();}@Overridepublic Array createArrayOf(String typeName, Object[] elements) throws SQLException {return connection.createArrayOf(typeName, elements);}@Overridepublic Struct createStruct(String typeName, Object[] attributes) throws SQLException {return connection.createStruct(typeName, attributes);}@Overridepublic void setSchema(String schema) throws SQLException {connection.setSchema(schema);}@Overridepublic String getSchema() throws SQLException {return connection.getSchema();}@Overridepublic void abort(Executor executor) throws SQLException {connection.abort(executor);}@Overridepublic void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {connection.setNetworkTimeout(executor, milliseconds);}@Overridepublic int getNetworkTimeout() throws SQLException {return connection.getNetworkTimeout();}@Overridepublic <T> T unwrap(Class<T> iface) throws SQLException {return connection.unwrap(iface);}@Overridepublic 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;}@Overridepublic ResultSet executeQuery(String sql) throws SQLException {queryBefore(sql);ResultSet resultSet = statement.executeQuery(sql);queryEnd(resultSet);return resultSet;}@Overridepublic int executeUpdate(String sql) throws SQLException {executeUpdateBefore(sql);int row = statement.executeUpdate(sql);executeUpdateEnd(row);return row;}@Overridepublic void close() throws SQLException {statement.close();super.handleStatement();}@Overridepublic int getMaxFieldSize() throws SQLException {return statement.getMaxFieldSize();}@Overridepublic void setMaxFieldSize(int max) throws SQLException {statement.setMaxFieldSize(max);}@Overridepublic int getMaxRows() throws SQLException {return statement.getMaxRows();}@Overridepublic void setMaxRows(int max) throws SQLException {statement.setMaxRows(max);}@Overridepublic void setEscapeProcessing(boolean enable) throws SQLException {statement.setEscapeProcessing(enable);}@Overridepublic int getQueryTimeout() throws SQLException {return statement.getQueryTimeout();}@Overridepublic void setQueryTimeout(int seconds) throws SQLException {statement.setQueryTimeout(seconds);}@Overridepublic void cancel() throws SQLException {statement.cancel();}@Overridepublic SQLWarning getWarnings() throws SQLException {return statement.getWarnings();}@Overridepublic void clearWarnings() throws SQLException {statement.clearWarnings();}@Overridepublic void setCursorName(String name) throws SQLException {statement.setCursorName(name);}@Overridepublic boolean execute(String sql) throws SQLException {executeBefore(sql);boolean result = statement.execute(sql);executeEnd(result);return result;}@Overridepublic ResultSet getResultSet() throws SQLException {return statement.getResultSet();}@Overridepublic int getUpdateCount() throws SQLException {return statement.getUpdateCount();}@Overridepublic boolean getMoreResults() throws SQLException {return statement.getMoreResults();}@Overridepublic void setFetchDirection(int direction) throws SQLException {statement.setFetchDirection(direction);}@Overridepublic int getFetchDirection() throws SQLException {return statement.getFetchDirection();}@Overridepublic void setFetchSize(int rows) throws SQLException {statement.setFetchSize(rows);}@Overridepublic int getFetchSize() throws SQLException {return statement.getFetchSize();}@Overridepublic int getResultSetConcurrency() throws SQLException {return statement.getResultSetConcurrency();}@Overridepublic int getResultSetType() throws SQLException {return statement.getResultSetType();}@Overridepublic void addBatch(String sql) throws SQLException {batchBefore(sql);statement.addBatch(sql);}@Overridepublic void clearBatch() throws SQLException {batchClear();statement.clearBatch();}@Overridepublic int[] executeBatch() throws SQLException {executeBatchBefore("");int[] result = statement.executeBatch();executeBatchEnd(result);return result;}@Overridepublic Connection getConnection() throws SQLException {return statement.getConnection();}@Overridepublic boolean getMoreResults(int current) throws SQLException {return statement.getMoreResults(current);}@Overridepublic ResultSet getGeneratedKeys() throws SQLException {return statement.getGeneratedKeys();}@Overridepublic int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException {executeUpdateBefore(sql);int result = statement.executeUpdate(sql, autoGeneratedKeys);executeUpdateEnd(result);return result;}@Overridepublic int executeUpdate(String sql, int[] columnIndexes) throws SQLException {executeUpdateBefore(sql);int result = statement.executeUpdate(sql, columnIndexes);executeUpdateEnd(result);return result;}@Overridepublic int executeUpdate(String sql, String[] columnNames) throws SQLException {executeUpdateBefore(sql);int result = statement.executeUpdate(sql, columnNames);executeUpdateEnd(result);return result;}@Overridepublic boolean execute(String sql, int autoGeneratedKeys) throws SQLException {executeBefore(sql);boolean result = statement.execute(sql, autoGeneratedKeys);executeEnd(result);return result;}@Overridepublic boolean execute(String sql, int[] columnIndexes) throws SQLException {executeBefore(sql);boolean result = statement.execute(sql, columnIndexes);executeEnd(result);return result;}@Overridepublic boolean execute(String sql, String[] columnNames) throws SQLException {executeBefore(sql);boolean result = statement.execute(sql, columnNames);executeEnd(result);return result;}@Overridepublic int getResultSetHoldability() throws SQLException {return statement.getResultSetHoldability();}@Overridepublic boolean isClosed() throws SQLException {return statement.isClosed();}@Overridepublic void setPoolable(boolean poolable) throws SQLException {statement.setPoolable(poolable);}@Overridepublic boolean isPoolable() throws SQLException {return statement.isPoolable();}@Overridepublic void closeOnCompletion() throws SQLException {statement.closeOnCompletion();}@Overridepublic boolean isCloseOnCompletion() throws SQLException {return statement.isCloseOnCompletion();}@Overridepublic <T> T unwrap(Class<T> iface) throws SQLException {return statement.unwrap(iface);}@Overridepublic 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;}@Overridepublic ResultSet executeQuery() throws SQLException {queryBefore(getSql());ResultSet resultSet = preparedStatement.executeQuery();queryEnd(resultSet);return resultSet;}@Overridepublic int executeUpdate() throws SQLException {executeUpdateBefore(getSql());int row = preparedStatement.executeUpdate();executeUpdateEnd(row);return row;}@Overridepublic void setNull(int parameterIndex, int sqlType) throws SQLException {parameters.set(parameterIndex - 1, null);preparedStatement.setNull(parameterIndex, sqlType);}@Overridepublic void setBoolean(int parameterIndex, boolean x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setBoolean(parameterIndex, x);}@Overridepublic void setByte(int parameterIndex, byte x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setByte(parameterIndex, x);}@Overridepublic void setShort(int parameterIndex, short x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setShort(parameterIndex, x);}@Overridepublic void setInt(int parameterIndex, int x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setInt(parameterIndex, x);}@Overridepublic void setLong(int parameterIndex, long x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setLong(parameterIndex, x);}@Overridepublic void setFloat(int parameterIndex, float x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setFloat(parameterIndex, x);}@Overridepublic void setDouble(int parameterIndex, double x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setDouble(parameterIndex, x);}@Overridepublic void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setBigDecimal(parameterIndex, x);}@Overridepublic void setString(int parameterIndex, String x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setString(parameterIndex, x);}@Overridepublic void setBytes(int parameterIndex, byte[] x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setBytes(parameterIndex, x);}@Overridepublic void setDate(int parameterIndex, Date x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setDate(parameterIndex, x);}@Overridepublic void setTime(int parameterIndex, Time x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setTime(parameterIndex, x);}@Overridepublic void setTimestamp(int parameterIndex, Timestamp x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setTimestamp(parameterIndex, x);}@Overridepublic void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setAsciiStream(parameterIndex, x, length);}@Overridepublic void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setUnicodeStream(parameterIndex, x, length);}@Overridepublic void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setBinaryStream(parameterIndex, x, length);}@Overridepublic void clearParameters() throws SQLException {parameters.clear();preparedStatement.clearParameters();}@Overridepublic void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setObject(parameterIndex, x, targetSqlType);}@Overridepublic void setObject(int parameterIndex, Object x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setObject(parameterIndex, x);}@Overridepublic boolean execute() throws SQLException {executeBefore(getSql());boolean result = preparedStatement.execute();executeEnd(result);return result;}@Overridepublic void addBatch() throws SQLException {preparedStatement.addBatch();}@Overridepublic void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException {parameters.set(parameterIndex - 1, reader);preparedStatement.setCharacterStream(parameterIndex, reader, length);}@Overridepublic void setRef(int parameterIndex, Ref x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setRef(parameterIndex, x);}@Overridepublic void setBlob(int parameterIndex, Blob x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setBlob(parameterIndex, x);}@Overridepublic void setClob(int parameterIndex, Clob x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setClob(parameterIndex, x);}@Overridepublic void setArray(int parameterIndex, Array x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setArray(parameterIndex, x);}@Overridepublic ResultSetMetaData getMetaData() throws SQLException {return preparedStatement.getMetaData();}@Overridepublic void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setDate(parameterIndex, x, cal);}@Overridepublic void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setTime(parameterIndex, x, cal);}@Overridepublic void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setTimestamp(parameterIndex, x, cal);}@Overridepublic void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException {parameters.set(parameterIndex - 1, null);preparedStatement.setNull(parameterIndex, sqlType, typeName);}@Overridepublic void setURL(int parameterIndex, URL x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setURL(parameterIndex, x);}@Overridepublic ParameterMetaData getParameterMetaData() throws SQLException {return preparedStatement.getParameterMetaData();}@Overridepublic void setRowId(int parameterIndex, RowId x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setRowId(parameterIndex, x);}@Overridepublic void setNString(int parameterIndex, String value) throws SQLException {parameters.set(parameterIndex - 1, value);preparedStatement.setNString(parameterIndex, value);}@Overridepublic void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException {parameters.set(parameterIndex - 1, value);preparedStatement.setNCharacterStream(parameterIndex, value, length);}@Overridepublic void setNClob(int parameterIndex, NClob value) throws SQLException {parameters.set(parameterIndex - 1, value);preparedStatement.setNClob(parameterIndex, value);}@Overridepublic void setClob(int parameterIndex, Reader reader, long length) throws SQLException {parameters.set(parameterIndex - 1, reader);preparedStatement.setClob(parameterIndex, reader, length);}@Overridepublic void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException {parameters.set(parameterIndex - 1, inputStream);preparedStatement.setBlob(parameterIndex, inputStream, length);}@Overridepublic void setNClob(int parameterIndex, Reader reader, long length) throws SQLException {parameters.set(parameterIndex - 1, reader);preparedStatement.setNClob(parameterIndex, reader, length);}@Overridepublic void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException {parameters.set(parameterIndex - 1, xmlObject);preparedStatement.setSQLXML(parameterIndex, xmlObject);}@Overridepublic void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setObject(parameterIndex, x, targetSqlType, scaleOrLength);}@Overridepublic void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setAsciiStream(parameterIndex, x, length);}@Overridepublic void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setBinaryStream(parameterIndex, x, length);}@Overridepublic void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException {parameters.set(parameterIndex - 1, reader);preparedStatement.setCharacterStream(parameterIndex, reader, length);}@Overridepublic void setAsciiStream(int parameterIndex, InputStream x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setAsciiStream(parameterIndex, x);}@Overridepublic void setBinaryStream(int parameterIndex, InputStream x) throws SQLException {parameters.set(parameterIndex - 1, x);preparedStatement.setBinaryStream(parameterIndex, x);}@Overridepublic void setCharacterStream(int parameterIndex, Reader reader) throws SQLException {parameters.set(parameterIndex - 1, reader);preparedStatement.setCharacterStream(parameterIndex, reader);}@Overridepublic void setNCharacterStream(int parameterIndex, Reader value) throws SQLException {parameters.set(parameterIndex - 1, value);preparedStatement.setNCharacterStream(parameterIndex, value);}@Overridepublic void setClob(int parameterIndex, Reader reader) throws SQLException {parameters.set(parameterIndex - 1, reader);preparedStatement.setClob(parameterIndex, reader);}@Overridepublic void setBlob(int parameterIndex, InputStream inputStream) throws SQLException {parameters.set(parameterIndex - 1, inputStream);preparedStatement.setBlob(parameterIndex, inputStream);}@Overridepublic 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;}@Overridepublic void registerOutParameter(int parameterIndex, int sqlType) throws SQLException {callableStatement.registerOutParameter(parameterIndex, sqlType);}@Overridepublic void registerOutParameter(int parameterIndex, int sqlType, int scale) throws SQLException {callableStatement.registerOutParameter(parameterIndex, sqlType, scale);}@Overridepublic boolean wasNull() throws SQLException {return callableStatement.wasNull();}@Overridepublic String getString(int parameterIndex) throws SQLException {return callableStatement.getString(parameterIndex);}@Overridepublic boolean getBoolean(int parameterIndex) throws SQLException {return callableStatement.getBoolean(parameterIndex);}@Overridepublic byte getByte(int parameterIndex) throws SQLException {return callableStatement.getByte(parameterIndex);}@Overridepublic short getShort(int parameterIndex) throws SQLException {return callableStatement.getShort(parameterIndex);}@Overridepublic int getInt(int parameterIndex) throws SQLException {return callableStatement.getInt(parameterIndex);}@Overridepublic long getLong(int parameterIndex) throws SQLException {return callableStatement.getLong(parameterIndex);}@Overridepublic float getFloat(int parameterIndex) throws SQLException {return callableStatement.getFloat(parameterIndex);}@Overridepublic double getDouble(int parameterIndex) throws SQLException {return callableStatement.getDouble(parameterIndex);}@Overridepublic BigDecimal getBigDecimal(int parameterIndex, int scale) throws SQLException {return callableStatement.getBigDecimal(parameterIndex, scale);}@Overridepublic byte[] getBytes(int parameterIndex) throws SQLException {return callableStatement.getBytes(parameterIndex);}@Overridepublic Date getDate(int parameterIndex) throws SQLException {return callableStatement.getDate(parameterIndex);}@Overridepublic Time getTime(int parameterIndex) throws SQLException {return callableStatement.getTime(parameterIndex);}@Overridepublic Timestamp getTimestamp(int parameterIndex) throws SQLException {return callableStatement.getTimestamp(parameterIndex);}@Overridepublic Object getObject(int parameterIndex) throws SQLException {return callableStatement.getObject(parameterIndex);}@Overridepublic BigDecimal getBigDecimal(int parameterIndex) throws SQLException {return callableStatement.getBigDecimal(parameterIndex);}@Overridepublic Object getObject(int parameterIndex, Map<String, Class<?>> map) throws SQLException {return callableStatement.getObject(parameterIndex, map);}@Overridepublic Ref getRef(int parameterIndex) throws SQLException {return callableStatement.getRef(parameterIndex);}@Overridepublic Blob getBlob(int parameterIndex) throws SQLException {return callableStatement.getBlob(parameterIndex);}@Overridepublic Clob getClob(int parameterIndex) throws SQLException {return callableStatement.getClob(parameterIndex);}@Overridepublic Array getArray(int parameterIndex) throws SQLException {return callableStatement.getArray(parameterIndex);}@Overridepublic Date getDate(int parameterIndex, Calendar cal) throws SQLException {return callableStatement.getDate(parameterIndex, cal);}@Overridepublic Time getTime(int parameterIndex, Calendar cal) throws SQLException {return callableStatement.getTime(parameterIndex, cal);}@Overridepublic Timestamp getTimestamp(int parameterIndex, Calendar cal) throws SQLException {return callableStatement.getTimestamp(parameterIndex, cal);}@Overridepublic void registerOutParameter(int parameterIndex, int sqlType, String typeName) throws SQLException {callableStatement.registerOutParameter(parameterIndex, sqlType, typeName);}@Overridepublic void registerOutParameter(String parameterName, int sqlType) throws SQLException {callableStatement.registerOutParameter(parameterName, sqlType);}@Overridepublic void registerOutParameter(String parameterName, int sqlType, int scale) throws SQLException {callableStatement.registerOutParameter(parameterName, sqlType, scale);}@Overridepublic void registerOutParameter(String parameterName, int sqlType, String typeName) throws SQLException {callableStatement.registerOutParameter(parameterName, sqlType, typeName);}@Overridepublic URL getURL(int parameterIndex) throws SQLException {return callableStatement.getURL(parameterIndex);}@Overridepublic void setURL(String parameterName, URL val) throws SQLException {parameterMap.put(parameterName, val);callableStatement.setURL(parameterName, val);}@Overridepublic void setNull(String parameterName, int sqlType) throws SQLException {parameterMap.put(parameterName, null);callableStatement.setNull(parameterName, sqlType);}@Overridepublic void setBoolean(String parameterName, boolean x) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setBoolean(parameterName, x);}@Overridepublic void setByte(String parameterName, byte x) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setByte(parameterName, x);}@Overridepublic void setShort(String parameterName, short x) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setShort(parameterName, x);}@Overridepublic void setInt(String parameterName, int x) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setInt(parameterName, x);}@Overridepublic void setLong(String parameterName, long x) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setLong(parameterName, x);}@Overridepublic void setFloat(String parameterName, float x) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setFloat(parameterName, x);}@Overridepublic void setDouble(String parameterName, double x) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setDouble(parameterName, x);}@Overridepublic void setBigDecimal(String parameterName, BigDecimal x) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setBigDecimal(parameterName, x);}@Overridepublic void setString(String parameterName, String x) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setString(parameterName, x);}@Overridepublic void setBytes(String parameterName, byte[] x) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setBytes(parameterName, x);}@Overridepublic void setDate(String parameterName, Date x) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setDate(parameterName, x);}@Overridepublic void setTime(String parameterName, Time x) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setTime(parameterName, x);}@Overridepublic void setTimestamp(String parameterName, Timestamp x) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setTimestamp(parameterName, x);}@Overridepublic void setAsciiStream(String parameterName, InputStream x, int length) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setAsciiStream(parameterName, x);}@Overridepublic void setBinaryStream(String parameterName, InputStream x, int length) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setBinaryStream(parameterName, x);}@Overridepublic void setObject(String parameterName, Object x, int targetSqlType, int scale) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setObject(parameterName, x, targetSqlType, scale);}@Overridepublic void setObject(String parameterName, Object x, int targetSqlType) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setObject(parameterName, x, targetSqlType);}@Overridepublic void setObject(String parameterName, Object x) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setObject(parameterName, x);}@Overridepublic void setCharacterStream(String parameterName, Reader reader, int length) throws SQLException {parameterMap.put(parameterName, reader);callableStatement.setCharacterStream(parameterName, reader, length);}@Overridepublic void setDate(String parameterName, Date x, Calendar cal) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setDate(parameterName, x, cal);}@Overridepublic void setTime(String parameterName, Time x, Calendar cal) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setTime(parameterName, x, cal);}@Overridepublic void setTimestamp(String parameterName, Timestamp x, Calendar cal) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setTimestamp(parameterName, x, cal);}@Overridepublic void setNull(String parameterName, int sqlType, String typeName) throws SQLException {parameterMap.put(parameterName, null);callableStatement.setNull(parameterName, sqlType, typeName);}@Overridepublic String getString(String parameterName) throws SQLException {return callableStatement.getString(parameterName);}@Overridepublic boolean getBoolean(String parameterName) throws SQLException {return callableStatement.getBoolean(parameterName);}@Overridepublic byte getByte(String parameterName) throws SQLException {return callableStatement.getByte(parameterName);}@Overridepublic short getShort(String parameterName) throws SQLException {return callableStatement.getShort(parameterName);}@Overridepublic int getInt(String parameterName) throws SQLException {return callableStatement.getInt(parameterName);}@Overridepublic long getLong(String parameterName) throws SQLException {return callableStatement.getLong(parameterName);}@Overridepublic float getFloat(String parameterName) throws SQLException {return callableStatement.getFloat(parameterName);}@Overridepublic double getDouble(String parameterName) throws SQLException {return callableStatement.getDouble(parameterName);}@Overridepublic byte[] getBytes(String parameterName) throws SQLException {return callableStatement.getBytes(parameterName);}@Overridepublic Date getDate(String parameterName) throws SQLException {return callableStatement.getDate(parameterName);}@Overridepublic Time getTime(String parameterName) throws SQLException {return callableStatement.getTime(parameterName);}@Overridepublic Timestamp getTimestamp(String parameterName) throws SQLException {return callableStatement.getTimestamp(parameterName);}@Overridepublic Object getObject(String parameterName) throws SQLException {return callableStatement.getObject(parameterName);}@Overridepublic BigDecimal getBigDecimal(String parameterName) throws SQLException {return callableStatement.getBigDecimal(parameterName);}@Overridepublic Object getObject(String parameterName, Map<String, Class<?>> map) throws SQLException {return callableStatement.getObject(parameterName, map);}@Overridepublic Ref getRef(String parameterName) throws SQLException {return callableStatement.getRef(parameterName);}@Overridepublic Blob getBlob(String parameterName) throws SQLException {return callableStatement.getBlob(parameterName);}@Overridepublic Clob getClob(String parameterName) throws SQLException {return callableStatement.getClob(parameterName);}@Overridepublic Array getArray(String parameterName) throws SQLException {return callableStatement.getArray(parameterName);}@Overridepublic Date getDate(String parameterName, Calendar cal) throws SQLException {return callableStatement.getDate(parameterName, cal);}@Overridepublic Time getTime(String parameterName, Calendar cal) throws SQLException {return callableStatement.getTime(parameterName, cal);}@Overridepublic Timestamp getTimestamp(String parameterName, Calendar cal) throws SQLException {return callableStatement.getTimestamp(parameterName, cal);}@Overridepublic URL getURL(String parameterName) throws SQLException {return callableStatement.getURL(parameterName);}@Overridepublic RowId getRowId(int parameterIndex) throws SQLException {return callableStatement.getRowId(parameterIndex);}@Overridepublic RowId getRowId(String parameterName) throws SQLException {return callableStatement.getRowId(parameterName);}@Overridepublic void setRowId(String parameterName, RowId x) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setRowId(parameterName, x);}@Overridepublic void setNString(String parameterName, String value) throws SQLException {parameterMap.put(parameterName, value);callableStatement.setNString(parameterName, value);}@Overridepublic void setNCharacterStream(String parameterName, Reader value, long length) throws SQLException {parameterMap.put(parameterName, value);callableStatement.setNCharacterStream(parameterName, value, length);}@Overridepublic void setNClob(String parameterName, NClob value) throws SQLException {parameterMap.put(parameterName, value);callableStatement.setNClob(parameterName, value);}@Overridepublic void setClob(String parameterName, Reader reader, long length) throws SQLException {parameterMap.put(parameterName, reader);callableStatement.setClob(parameterName, reader, length);}@Overridepublic void setBlob(String parameterName, InputStream inputStream, long length) throws SQLException {parameterMap.put(parameterName, inputStream);callableStatement.setBlob(parameterName, inputStream, length);}@Overridepublic void setNClob(String parameterName, Reader reader, long length) throws SQLException {parameterMap.put(parameterName, reader);callableStatement.setNClob(parameterName, reader, length);}@Overridepublic NClob getNClob(int parameterIndex) throws SQLException {return callableStatement.getNClob(parameterIndex);}@Overridepublic NClob getNClob(String parameterName) throws SQLException {return callableStatement.getNClob(parameterName);}@Overridepublic void setSQLXML(String parameterName, SQLXML xmlObject) throws SQLException {parameterMap.put(parameterName, xmlObject);callableStatement.setSQLXML(parameterName, xmlObject);}@Overridepublic SQLXML getSQLXML(int parameterIndex) throws SQLException {return callableStatement.getSQLXML(parameterIndex);}@Overridepublic SQLXML getSQLXML(String parameterName) throws SQLException {return callableStatement.getSQLXML(parameterName);}@Overridepublic String getNString(int parameterIndex) throws SQLException {return callableStatement.getNString(parameterIndex);}@Overridepublic String getNString(String parameterName) throws SQLException {return callableStatement.getNString(parameterName);}@Overridepublic Reader getNCharacterStream(int parameterIndex) throws SQLException {return callableStatement.getNCharacterStream(parameterIndex);}@Overridepublic Reader getNCharacterStream(String parameterName) throws SQLException {return callableStatement.getNCharacterStream(parameterName);}@Overridepublic Reader getCharacterStream(int parameterIndex) throws SQLException {return callableStatement.getCharacterStream(parameterIndex);}@Overridepublic Reader getCharacterStream(String parameterName) throws SQLException {return callableStatement.getCharacterStream(parameterName);}@Overridepublic void setBlob(String parameterName, Blob x) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setBlob(parameterName, x);}@Overridepublic void setClob(String parameterName, Clob x) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setClob(parameterName, x);}@Overridepublic void setAsciiStream(String parameterName, InputStream x, long length) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setAsciiStream(parameterName, x, length);}@Overridepublic void setBinaryStream(String parameterName, InputStream x, long length) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setBinaryStream(parameterName, x, length);}@Overridepublic void setCharacterStream(String parameterName, Reader reader, long length) throws SQLException {parameterMap.put(parameterName, reader);callableStatement.setCharacterStream(parameterName, reader, length);}@Overridepublic void setAsciiStream(String parameterName, InputStream x) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setAsciiStream(parameterName, x);}@Overridepublic void setBinaryStream(String parameterName, InputStream x) throws SQLException {parameterMap.put(parameterName, x);callableStatement.setBinaryStream(parameterName, x);}@Overridepublic void setCharacterStream(String parameterName, Reader reader) throws SQLException {parameterMap.put(parameterName, reader);callableStatement.setCharacterStream(parameterName, reader);}@Overridepublic void setNCharacterStream(String parameterName, Reader value) throws SQLException {parameterMap.put(parameterName, value);callableStatement.setNCharacterStream(parameterName, value);}@Overridepublic void setClob(String parameterName, Reader reader) throws SQLException {parameterMap.put(parameterName, reader);callableStatement.setClob(parameterName, reader);}@Overridepublic void setBlob(String parameterName, InputStream inputStream) throws SQLException {parameterMap.put(parameterName, inputStream);callableStatement.setBlob(parameterName, inputStream);}@Overridepublic void setNClob(String parameterName, Reader reader) throws SQLException {parameterMap.put(parameterName, reader);callableStatement.setNClob(parameterName, reader);}@Overridepublic <T> T getObject(int parameterIndex, Class<T> type) throws SQLException {return callableStatement.getObject(parameterIndex, type);}@Overridepublic <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());}@Overridepublic 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

基于 javaagent + javassist 一步步实现调用链系统 (2)相关推荐

  1. WDK_基于Fabric的区块链系统开发

    文章目录 摘要 一.Fabric环境搭建 1.1 安装docker 1.2 安装go 1.3 安装Node.js 1.4 部署hyperledger Fabric 二.Fabric的概念 2.1 逻辑 ...

  2. java agent_GitHub - dingjs/javaagent: 基于javaagent开发的APM工具,收集方法的执行次数和执行时间,定时输出成json格式的日志。...

    Javaagent 概述 javaagent是一个简单优雅的java agent,利用java自带的instrument特性+javassist字节码编辑技术,实现了无侵入的方法级性能监控.相比于Ne ...

  3. 牛逼,一整套基于Java开发的的区块链系统(附完整源码)

    前言 近几年区块链概念越来越火,特别是区块链技术被纳入国家基础设施建设名单后,各大企业也开始招兵买马,对区块链技术进行研究,从各大招聘网站的区块链职位来看,薪资待遇都很不错,月薪30K到80K的都有, ...

  4. 基于Java开发一套完整的区块链系统(附源码)

    来源:https://blog.csdn.net/victory_long 前言 近几年区块链概念越来越火,特别是区块链技术被纳入国家基础设施建设名单后,各大企业也开始招兵买马,对区块链技术进行研究, ...

  5. 基于Java开发一套完整的区块链系统(附完整源码)

    前言 近几年区块链概念越来越火,特别是区块链技术被纳入国家基础设施建设名单后,各大企业也开始招兵买马,对区块链技术进行研究,从各大招聘网站的区块链职位来看,薪资待遇都很不错,月薪30K到80K的都有, ...

  6. php 菜谱 源码,基于php的菜谱大全api调用代码实例

    代码描述:基于php的菜谱大全api调用代码实例 接口地址:http://www.juhe.cn/docs/api/id/46 PHP代码 // +-------------------------- ...

  7. 外汇汇率接口 java_基于JAVA的货币汇率api调用代码实例

    代码描述:基于JAVA的货币汇率api调用代码实例 关联数据:货币汇率 接口地址:http://www.juhe.cn/docs/api/id/23 1.[代码][Java]代码 import jav ...

  8. Hystrix面试 - 基于 timeout 机制为服务接口调用超时提供安全保护

    Hystrix面试 - 基于 timeout 机制为服务接口调用超时提供安全保护 一般来说,在调用依赖服务的接口的时候,比较常见的一个问题就是超时.超时是在一个复杂的分布式系统中,导致系统不稳定,或者 ...

  9. java 金数据推送数据_基于JAVA的黄金数据接口调用代码实例

    代码描述:基于JAVA的黄金数据接口调用代码实例 接口地址:http://www.juhe.cn/docs/api/id/29 1.[代码][Java]代码 import java.io.Buffer ...

最新文章

  1. [数据结构] 时间复杂度计算
  2. 微信小程序开发分销制度济南_花店微信小程序开发教程
  3. 突破androidstudio 的控制
  4. 面试题整理 2:求链表倒数第 k 个结点
  5. 控制ALV单元格可编辑
  6. 杨清彦:《像三国》游戏3D动效制作经验分享
  7. c++如何将int数组中的值取出*号运算符_如何用动态规划巧妙解决 “双十一” 购物时的凑单问题?羊毛薅起来!!!...
  8. ERROR manager.SqlManager: Error reading from database: java.sql.SQLException: Streaming result set
  9. Elasticsearch--Docker安装ES---全文检索引擎ElasticSearch工作笔记002
  10. android activity启动模式_Android知识点【Activity】清单文件
  11. 远古有圆孔的头颅可能是穿越者
  12. 【UVA-10891】Game of Sum【区间DP】
  13. 等保2.0二级安全要求
  14. spring源码解析之---InstantiationAwareBeanPostProcessor解析
  15. 什么是视频比特率:完整指南
  16. Shipyard的几个概念
  17. 康考迪亚大学应用计算机科学,康考迪亚大学计算机
  18. 我热爱计算机作文450字,热爱音乐的我作文450字
  19. 《非凡的公主希瑞》(She-Ra Princess of Power)[93集全][数码修复][国英双语][RMVB]
  20. List去除重复数据的几种方式和性能比较

热门文章

  1. IR Cut Filter主要作用分别是什么?_安防 | 说说监控摄像头中IR-CUT双滤光片哪些事...
  2. SOCKS代理的常见误区
  3. C++--顺时针打印矩阵
  4. MP530注墨 连供及传真的心得体会
  5. 微金所案例总结——Bootstrap应用模板引擎的使用
  6. 以图搜图 相似图片搜索的原理(一)
  7. 手机闪存速度排行_真机闪存实测:闪存读取速度到底什么鬼差距有多大
  8. vscode快速生成代码块
  9. 桔皮加蜂蜜的制作方法?桔皮加蜂蜜泡水喝吗?
  10. Oculus检测到你的NVIDIA驱动问题,解决办法