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();
    }
}
相关推荐
找了一圈尾巴4 分钟前
Spring Boot 日志管理(官网文档解读)
java·spring boot
升讯威在线客服系统4 分钟前
如何通过 Docker 在没有域名的情况下快速上线客服系统
java·运维·前端·python·docker·容器·.net
s:1031 小时前
【框架】参考 Spring Security 安全框架设计出,轻量化高可扩展的身份认证与授权架构
java·开发语言
久绊A2 小时前
Python 基本语法的详细解释
开发语言·windows·python
南山十一少4 小时前
Spring Security+JWT+Redis实现项目级前后端分离认证授权
java·spring·bootstrap
Hylan_J6 小时前
【VSCode】MicroPython环境配置
ide·vscode·python·编辑器
莫忘初心丶6 小时前
在 Ubuntu 22 上使用 Gunicorn 启动 Flask 应用程序
python·ubuntu·flask·gunicorn
427724006 小时前
IDEA使用git不提示账号密码登录,而是输入token问题解决
java·git·intellij-idea
chengooooooo6 小时前
苍穹外卖day8 地址上传 用户下单 订单支付
java·服务器·数据库
李长渊哦6 小时前
常用的 JVM 参数:配置与优化指南
java·jvm