PDF书籍《手写调用链监控APM系统-Java版》第2章 第一个Agent应用

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

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

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

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

适用人群

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

版权

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

原版PDF+源码请见:

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

第1章 第一个Agent应用

JavaAgent其实就是jdk提供的一个工具,让我们有能力去修改字节码,然后加载到JVM使其生效。调用链监控本质就是对各框架的方法调用进行监控,在方法逻辑里面进行埋点,能做到这点的非JavaAgent莫属。

JavaAgent在修改字节码有两个时机,第一个就是在类加载前修改,这种就是静态绑定。第二种就是在类加载后JVM进程正在运行了,然后进行绑定修改,这种就是动态绑定,但是动态绑定对原始字节码修改作了很多限制,比如:新增方法,字段等。由于我们制作的调用链监控需要在原始字节码新增字段,所以采用了静态绑定,动态绑定就不在本书讨论范畴。

JavaAgent的静态绑定

JavaAgent技术的核心就是制作一个agent jar包,在启动JVM时,使用-javaagent参数就可以指定这个agent jar。JVM启动时在main方法之前会先加载这个agent jar里面类的premain方法,这个就是字节码修改的入口方法。

这个agent jar 打包时,需要在Manifest文件声明premain的路径等信息,具体实例如下图:

下面我们先来制作一个agent jar,顺便把我们的项目架构搭建起来。

1.1 创建项目结构

打开idea,创建一个名为hadluo-smart-apm的顶级maven聚合工程(聚合工程就是packaging为POM的模块项目),然后新建两个子模块jar项目:

  • apm-agent-core: 核心逻辑实现。
  • apm-commons: 工具类,公共接口和类。

hadluo-smart-apm的POM文件如下:

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>https://zhida.zhihu.com/search?content_id=251888894&content_type=Article&match_order=1&q=spring-boot-starter-parent&zhida_source=entity</artifactId>
        <version>2.4.10</version>
        <relativePath   />
    </parent>
    <groupId>com.hadluo.apm</groupId>
    <artifactId>hadluo-smart-apm</artifactId>
    <version>1.0</version>
    <packaging>pom</packaging>
<modules>
<module>apm-agent-core</module>
<module>apm-commons</module>
    </modules>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

注意:为了书面优美,简洁,后续代码只贴关键性代码,可能会省略非必要的代码。

apm-agent-core的POM文件如下:

<parent>
    <groupId>com.hadluo.apm</groupId>
    <artifactId>hadluo-smart-apm</artifactId>
    <version>1.0</version>
</parent>
<artifactId>apm-agent-core</artifactId>
<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
    <dependency>
        <groupId>org.apache.https://zhida.zhihu.com/search?content_id=251888894&content_type=Article&match_order=2&q=kafka&zhida_source=entity</groupId>
        <artifactId>kafka_2.13</artifactId>
        <version>3.9.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.kafka</groupId>
        <artifactId>https://zhida.zhihu.com/search?content_id=251888894&content_type=Article&match_order=1&q=kafka-clients&zhida_source=entity</artifactId>
        <version>3.9.0</version>
    </dependency>
<!-- 获取cpu,内存信息工具用-->
    <dependency>
        <groupId>com.github.oshi</groupId>
        <artifactId>oshi-core</artifactId>
        <version>3.12.2</version>
    </dependency>
<!-- 获取cpu,内存信息工具用-->
    <dependency>
        <groupId>net.java.dev.jna</groupId>
        <artifactId>jna</artifactId>
        <version>5.2.0</version>
    </dependency>
<!-- 获取cpu,内存信息工具用-->
    <dependency>
        <groupId>net.java.dev.jna</groupId>
        <artifactId>jna-platform</artifactId>
        <version>5.2.0</version>
    </dependency>
    <dependency>
        <groupId>com.hadluo.apm</groupId>
        <artifactId>apm-commons</artifactId>
        <version>1.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>3.1.1</version>
    </dependency>
</dependencies>

apm-agent-core的POM依赖了kafka,oshi等都是我们后面要用到的,我们索性先添加,后面缺的在后面补充。注意还要依赖apm-commons工具项目。

apm-commons的POM如下:

<parent>
    <groupId>com.hadluo.apm</groupId>
    <artifactId>hadluo-smart-apm</artifactId>
    <version>1.0</version>
</parent>
<artifactId>apm-commons</artifactId>
<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.20</version>
    </dependency>


    <dependency>
        <groupId>net.bytebuddy</groupId>
        <artifactId>byte-buddy</artifactId>
        <version>1.10.19</version>
    </dependency>
<dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
   </dependency>

</dependencies>

byte-buddy是修改字节码的一个好用的工具,我们也先添加上。后面还用到了spring的读取环境下文件工具,也先添加上。

到此我们创建的项目结构如下图所示:

1.2 Agent Jar的制作与打包配置

前面说到Agent Jar需要配置指定premain路径的manifest文件,这个需要在apm-agent-core的POM文件中添加以下内容:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.6.0</version>
            <executions>
                <execution>
                    <id>apm-agent-core</id>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <transformers>
                            <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                <manifestEntries>
                                    <!--Premain-Class: 代表 Agent 静态加载时会调用的类全路径名。 -->
                                    <Premain-Class>com.hadluo.apm.agentcore.AgentMain</Premain-Class>
                                    <!--Agent-Class: 代表 Agent 动态加载时会调用的类全路径名。 -->
                                    <Agent-Class>com.hadluo.apm.agentcore.AgentMain</Agent-Class>
                                    <!--Can-Redefine-Classes: 是否可进行类定义。 -->
                                    <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                    <!--Can-Retransform-Classes: 是否可进行类转换。 -->
                                    <Can-Retransform-Classes>true</Can-Retransform-Classes>
                                </manifestEntries>
                            </transformer>

                            <!-- 多个同名配置文件会合并,而不是覆盖-->
                            <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                <resource>hadluo-apm-plugin.def</resource>
                            </transformer>
                        </transformers>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

上面pom中配置了一个很重要的打包插件:maven-shade-plugin ,这个插件可以指定mainfest清单文件的配置(比如:Agent-Class),这个JavaAgent技术所必须的。

打包插件的第二个重要功能就是可以将所有依赖的第三方jar的同名配置文件进行合并。

这里先引出了插桩插件 的概念:本书通过对mysql,redis,tomcat等组件进行字节码插桩(增强)都是通过插桩插件 来实现的,而且一个组件对应一个插桩插件,每个插桩插件 需要一份 hadluo-apm-plugin.def 文件进行声明,因此打包后需要将多个def文件内容进行合并,不然就会丢失def文件内容。

maven-shade-plugin配置的AppendingTransformer就可以进行同名文件的合并。

下面我们实现premain方法,在apm-agent-core项目下,新建类:

com.hadluo.apm.agentcore.AgentMain

public class AgentMain {

    public static void premain(String args, Instrumentation instrumentation) {
        Logs.debug(AgentMain.class, "premain开始执行,args=" + args);
    }
}

Logs为apm-commoms的打印日志工具,本书针对所有工具类都不会贴代码,会指定统一的下载路径,后续只要是没贴出的都是工具代码,后面不在赘述。

当premain执行时就会打印出日志,Instrumentation 为字节码修改的口子。

1.3 测试 Agent Jar

通过maven的package或install我们可以得到打包后的apm-agent-core-1.0.jar,如下图所示:

然后自己新建一个springboot的测试项目,启动项目前,我们指定JVM启动参数:

-javaagent:F:\hadluo\mvn_res\res\com\hadluo\apm\apm-agent-core\1.0\apm-agent-core-1.0.jar

当然你的apm-agent-core-1.0.jar路径肯定跟我的不一样。启动发现我们的premain执行了打印,如图所示:

说明我们的第一个agent制作成功。

1.4 本章小结

本章内容较为简单,主要就是讲解了JAVA为开发者提供了一个修改字节码的途径,这个途径就是JavaAgent,然后我们实现了第一个agent应用,并且进行测试通过。

我们还搭建了项目的结构,后续都是围绕这个项目结构进行开发,我们在实现一个较为复杂的系统时,也都是这样一步一步循序渐进,就像砌房子一样需要一块砖一块砖累积。

本章涉及到的工具类:

com.hadluo.apm.commons.Logs

package com.hadluo.apm.commons;

public class Logs {
    private static final String LINE_SEPARATOR = System.getProperty("line.separator");

    public static void debug(Class<?> clazz, String msg) {
        System.err.println(msg);
    }


    public static void info(Class<?> clazz, String msg) {
        System.out.println(msg);
    }


    public static void err(Class<?> clazz, String msg, Throwable e) {
        e.printStackTrace();
        System.err.println(msg);
    }

    public static void err(Class<?> clazz, String msg) {
        System.err.println(msg);
    }

    private static String printExceptionInfo(Throwable causeException) {
        return causeException.toString() + LINE_SEPARATOR;
    }

    public static String convert2String(Throwable throwable, final int maxLength) {
        final StringBuilder stackMessage = new StringBuilder();
        Throwable causeException = throwable;

        int depth = 5;
        while (causeException != null && depth != 0) {
            stackMessage.append(printExceptionInfo(causeException));

            boolean isLookDeeper = printStackElement(causeException.getStackTrace(), new AppendListener() {
                @Override
                public void append(String value) {
                    stackMessage.append(value);
                }

                @Override
                public boolean overMaxLength() {
                    return stackMessage.length() > maxLength;
                }
            });

            if (isLookDeeper) {
                break;
            }

            causeException = causeException.getCause();
            depth--;
        }

        return stackMessage.toString();
    }

    private static boolean printStackElement(StackTraceElement[] stackTrace, AppendListener printListener) {
        if (stackTrace.length == 0) {
            /**
             * In some cases, people would fill empty stackTrace intentionally.
             * This is a quick stop.
             */
            return true;
        }

        for (StackTraceElement traceElement : stackTrace) {
            printListener.append("at " + traceElement + LINE_SEPARATOR);
            if (printListener.overMaxLength()) {
                return true;
            }
        }
        return false;
    }

    private interface AppendListener {
        void append(String value);

        boolean overMaxLength();
    }
}
相关推荐
Bruce_Liuxiaowei40 分钟前
农历节日倒计时:基于Python的公历与农历日期转换及节日查询小程序(升级版)
python·节日·日期函数·农历日期
Spcarrydoinb42 分钟前
python学习笔记——函数以及函数传参
笔记·python·学习
炸鸡配泡面1 小时前
Qt 12.28 day3
java·开发语言
get_money_1 小时前
代码随想录Day37 动态规划:完全背包理论基础,518.零钱兑换II,本周小结动态规划,377. 组合总和 Ⅳ,70. 爬楼梯(进阶版)。
java·笔记·算法·动态规划
get_money_1 小时前
代码随想录38 322. 零钱兑换,279.完全平方数,本周小结动态规划,139.单词拆分,动态规划:关于多重背包,你该了解这些!背包问题总结篇。
java·开发语言·笔记·算法·动态规划
憶巷3 小时前
设计模式的分类及作用
java·设计模式
sanx183 小时前
体育实时数据是怎么获取的
python
向宇it4 小时前
【从零开始入门unity游戏开发之——C#篇36】C#的out协变和in逆变如何解决泛型委托的类型转换问题
java·开发语言·unity·c#·游戏引擎
天空之外1364 小时前
Spring Boot Actuator、Spring Boot Actuator使用、Spring Boot Actuator 监控、Spring程序监控
java·spring boot·spring
baihb10244 小时前
Docker 默认安装位置迁移
java·docker