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。

相关推荐
聪明的笨猪猪6 分钟前
Java “线程池(1)”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
Miraitowa_cheems10 分钟前
LeetCode算法日记 - Day 63: 图像渲染、岛屿数量
java·数据结构·算法·leetcode·决策树·贪心算法·深度优先
karry_k15 分钟前
ThreadLocal原理以及内存泄漏
java·后端·面试
羚羊角uou2 小时前
【Linux】POSIX信号量、环形队列、基于环形队列实现生产者消费者模型
java·开发语言
你是狒狒吗3 小时前
为什么mysql要有主从复制,主库,从库这种东西
数据库·mysql
代码萌新知8 小时前
设计模式学习(五)装饰者模式、桥接模式、外观模式
java·学习·设计模式·桥接模式·装饰器模式·外观模式
iナナ10 小时前
Spring Web MVC入门
java·前端·网络·后端·spring·mvc
驱动探索者10 小时前
find 命令使用介绍
java·linux·运维·服务器·前端·学习·microsoft
卷Java11 小时前
违规通知功能修改说明
java·数据库·微信小程序·uni-app
CoderYanger11 小时前
优选算法-双指针:2.复写零
java·后端·算法·leetcode·职场和发展