PDF书籍《手写调用链监控APM系统-Java版》第9章 插件与链路的结合:Mysql插件实现

本人阅读了 Skywalking 的大部分核心代码,也了解了相关的文献,对此深有感悟,特此借助巨人的思想自己手动用JAVA语言实现了一个 "调用链监控APM" 系统。本书采用边讲解实现原理边编写代码的方式,看本书时一定要跟着敲代码。

作者已经将过程写成一部书籍,奈何没有钱发表,如果您知道渠道可以联系本人。一定重谢。

本书涉及到的核心技术与思想

JavaAgent , ByteBuddy,SPI服务,类加载器的命名空间,增强JDK类,kafka,插件思想,切面,链路栈等等。实际上远不止这么多,差不多贯通了整个java体系。

适用人群

自己公司要实现自己的调用链的;写架构的;深入java编程的;阅读Skywalking源码的;

版权

本书是作者呕心沥血亲自编写的代码,不经同意切勿拿出去商用,否则会追究其责任。

原版PDF+源码请见:

本章涉及到的工具类也在这里面:

PDF书籍《手写调用链监控APM系统-Java版》第1章 开篇介绍-CSDN博客

第9章 插件与链路的结合:Mysql插件实现

9.1 Mysql插件的流程分析

数据库归根结底就是JDBC的操作,在学习时期,我们肯定会学习基本的jdbc查询数据库的写法:

复制代码
// 1. 加载驱动
Class.forName("com.mysql.jdbc.Driver");
// 2. 获取连接 对象
ConnectionImpl connection = (ConnectionImpl) DriverManager.getConnection("", "", "");
// 3. 准备 statement
PreparedStatement statement = connection.prepareStatement("select * from t_user");
// 4. 执行sql
statement.executeQuery();

无论用什么框架,数据库操作都避免不了上面原始步骤。我们分析下上面的流程:

  1. 加载驱动,我们不关心。

  2. 通过DriverManager的静态方法getConnection获取到ConnectionImpl 连接对象。

  3. 通ConnectionImpl的prepareStatement方法,去配合sql准备一个PreparedStatement 。

  4. 最后通过PreparedStatement的executeQuery方法去查询sql语句并返回结果。

数据库插件要想采集到调用的sql信息,就必须要拦截ConnectionImpl类的prepareStatement方法。 如果还要采集sql调用的返回信息,还需要拦截 PreparedStatement 类的executeQuery方法。但是我们还需要数据库服务器的地址信息,这个还必须要拦截 DriverManager.getConnection 。

拦截的三个类我们梳理出来了,但是这里有个很严重的问题,当我们拦截DriverManager.getConnection获取到数据库地址后,没办法向后传递到ConnectionImpl类的prepareStatement中。

我们架设一个猜想:

拦截DriverManager.getConnection返回的是ConnectionImpl。如果能在这个阶段将数据库的地址信息设置到返回的ConnectionImpl对象中,后面拦截ConnectionImpl的prepareStatement方法时,方法切面那里是不是有个参数能获取到当前对象,也就能拿到DriverManager.getConnection拦截时的数据库信息了。

这也就需要在增强类中添加Object字段,用于参数传递的思想。

我们目前的字节码增强代码时无法实现上述思想的,需要进行改造。接下来我们来讲解下如何实现。

9.2 插桩类改造,新增Object字段和实现EnhancedInstance接口

由于篇幅过长,请到第一章查看原版PDF和源码:

PDF书籍《手写调用链监控APM系统-Java版》第1章 开篇介绍-CSDN博客

9**.3 Mysql插件真正实现**

根据前面的分析,我们依次需要拦截

类名: java.sql.DriverManager

方法:getConnection

JDK类库

然后拦截:

类名: com.mysql.jdbc.ConnectionImpl

方法:prepareStatement

非JDK类库

最后拦截:

类名: com.mysql.jdbc.PreparedStatement

方法:executeQuery

非JDK类库

在插件模块下新增apm-mysql-plugin项目,POM内容:

复制代码
<dependency>
    <groupId>com.hadluo.apm</groupId>
    <artifactId>apm-commons</artifactId>
    <version>1.0</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.44</version>
    <scope>provided</scope>
</dependency>

hadluo-apm-plugin.def 插件定义文件:

复制代码
mysql5-DriverConnect=com.hadluo.apm.plugin.mysql5.DriverConnectInstrumentation

mysql5-PrepareStatement=com.hadluo.apm.plugin.mysql5.PrepareStatementInstrumentation

mysql5-PrepareStatementExecute=com.hadluo.apm.plugin.mysql5.PrepareStatementExecuteInstrumentation

三个类我就不建了,值得注意的是,DriverManager#getConnection方法最终调用下面方法:

DriverConnectInstrumentation配置拦截类名时,还需要指定参数签名:

还有就是isBootstrapInstrumentation 一定要返回true, 因为它是rt.jar的JDK类。

三个方法环绕执行器分别是:

DriverConnectInterceptor

PrepareStatementInterceptor

PrepareStatementExecuteInterceptor

DriverConnectInterceptor 代码实现:

由于篇幅过长,请到第一章查看原版PDF和源码:

PDF书籍《手写调用链监控APM系统-Java版》第1章 开篇介绍-CSDN博客

打包测试,kafka数据json如下:

复制代码
{
    "msgTypeClass": "com.hadluo.apm.commons.kafka.Segment",
    "sampleTime": 1734056030540,
    "serviceName": null,
    "serviceInstance": "1a91d6d937ea4d6b8c2cb34dc75bf240@192.168.2.125",
    "traceId": "c133c183325b48fdbd3c94eca8bf341e.44.17340560303730001",
    "traceSegmentId": "c133c183325b48fdbd3c94eca8bf341e.44.17340560303710000",
    "spans": [
        {
            "spanId": 1,
            "parentSpanId": 0,
            "startTime": 1734056030528,
            "endTime": 1734056030529,
            "refs": [

            ],
            "operationName": "jdbc:mysql://127.0.0.1:3306/test/select * from t_user/executeQuery",
            "peer": null,
            "spanType": "Exit",
            "spanLayer": "DB",
            "component": "MySQL",
            "tags": {
                "remotePeer": "jdbc:mysql://127.0.0.1:3306/test",
                "extra": "{password=, user=root}",
                "sql": "select * from t_user"
            },
            "logs": {

            }
        },
        {
            "spanId": 0,
            "parentSpanId": -1,
            "startTime": 1734056030373,
            "endTime": 1734056030538,
            "refs": [

            ],
            "operationName": "/order",
            "peer": null,
            "spanType": "Entry",
            "spanLayer": "HTTP",
            "component": "Tomcat",
            "tags": {
                "http.method": "GET",
                "url": "/order"
            },
            "logs": {

            }
        }
    ]
}

上述json我想应该很熟悉了,不用我过多分析,只是还有一个问题,就是serviceName为空。 熟悉SkyWalking的读者都应该知道,应用名称是在启动参数上通过 agent name 来配置的。

作者不这样做,我们可以拦截SpringBoot的启动流程,在解析到Enviroment后,将 spring.application.name 的值设置到Config,

当然,这仅限制于springboot服务生效。下节我们就来分析如何拦截springboot的Enviroment。

相关推荐
Ray Liang34 分钟前
用六边形架构与整洁架构对比是伪命题?
java·python·c#·架构设计
Java水解1 小时前
Java 中间件:Dubbo 服务降级(Mock 机制)
java·后端
SimonKing5 小时前
OpenCode AI辅助编程,不一样的编程思路,不写一行代码
java·后端·程序员
FastBean5 小时前
Jackson View Extension Spring Boot Starter
java·后端
Seven976 小时前
剑指offer-79、最⻓不含重复字符的⼦字符串
java
皮皮林55116 小时前
Java性能调优黑科技!1行代码实现毫秒级耗时追踪,效率飙升300%!
java
冰_河16 小时前
QPS从300到3100:我靠一行代码让接口性能暴涨10倍,系统性能原地起飞!!
java·后端·性能优化
桦说编程19 小时前
从 ForkJoinPool 的 Compensate 看并发框架的线程补偿思想
java·后端·源码阅读
躺平大鹅20 小时前
Java面向对象入门(类与对象,新手秒懂)
java