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();
    }
}
相关推荐
前行的小黑炭23 分钟前
设计模式:为什么使用模板设计模式(不相同的步骤进行抽取,使用不同的子类实现)减少重复代码,让代码更好维护。
android·java·kotlin
Java技术小馆28 分钟前
如何设计一个本地缓存
java·面试·架构
utmhikari1 小时前
【日常随笔】万字长文,如何用pyside6开发一个python桌面工具
前端·python·pyqt
XuanXu1 小时前
Java AQS原理以及应用
java
小杨4043 小时前
python入门系列十四(多进程)
人工智能·python·pycharm
风象南4 小时前
SpringBoot中6种自定义starter开发方法
java·spring boot·后端
mghio13 小时前
Dubbo 中的集群容错
java·微服务·dubbo
咖啡教室18 小时前
java日常开发笔记和开发问题记录
java
咖啡教室18 小时前
java练习项目记录笔记
java