第08讲:搭建 SkyWalking 源码环境,开启征途

在第一课时中,我们已经成功安装并运行了 SkyWalking 环境,本课时将带你完成 SkyWalking 源码环境的搭建 ,并在 IDEA 中尝试调试 SkyWalking Agent。

搭建 SkyWalking 源码环境

  • 下载 SkyWalking 源码

执行 git clone 命令从 GitHub下载 SkyWalking 源码,如下所示 :


git clone git@github.com:apache/skywalking.git


  • 切换分支

等待 clone 完成之后,我们通过命令行窗口进入 SkyWalking 源码根目录,执行如下命令:


git checkout -b 6.2.0 v6.2.0


     切换到 v6.2.0 tag 的源码,后续源码分析过程都是基于 6.2.0 版本进行分析的。

  • 导入 IDEA

在 IDEA 中点击“Import Project”,选择 SkyWalking 源码目录导入 IDEA 中。SkyWalking 是一个 Maven,在导入过程中会下载相关的依赖 jar 包,过程可能会比较慢,需要你耐心等待。

  • 更新 submodule

全部 Maven 依赖下载完成后,在 SkyWalking 源码根目录中执行如下两条命令,更新 submodule:


git submodule init
git submodule update


  • 打包

上述操作执行完毕之后,执行如下命令,开始打包:


mvn clean package -DskipTests -Dcheckstyle.skip


  • 标记 Generated Source Code 目录

在打包过程中,会自动生成一些代码,需要我们将其目录设置为 Generated Source Codes,这样 IDEA 才能识别这些代码,生成代码主要来源于以下两种方式:

  • SkyWalking Agent 与后端 OAP 之间通信用了 gRPC,其中的 proto 文件会生成一些 Java 代码。

  • SkyWalking OAP 中定义了 OAL 语言,打包过程中会生成一些 Java 代码。

具体的标记方式如下下图所示:



需要标记的目录有:

    • apm-protocol/apm-network/target/generated-sources/protobuf 路径下的 grpc-java 目录和 java 目录。

    • oap-server/server-core/target/generated-sources/protobuf 路径下的 grpc-java 目录和 java 目录。

    • oap-server/server-receiver-plugin/receiver-proto/target/generated-sources/protobuf 路径下的 grpc-java 目录和 java 目录。

    • oap-server/exporter/target/generated-sources/protobuf 路径下的 grpc-java 目录和 java 目录。

    • oap-server/server-configuration/grpc-configuration-sync/target/generated-sources/protobuf 路径下的 grpc-java 目录和 java 目录。

    • /Users/xxx/SW/skywalking/oap-server/generated-analysis/target/generated-sources 路径下的 oal 目录。

  • 安装 ElasticSearch

前文已经完成了 ElasticSearch 的安装,这里不再展开。

  • 启动 OAP

在 IDEA 中,找到 oap-server 模块中 OAPServerStartUp 这个类,右键执行 main() 方法即可。启动过程中无异常日志,并看到如下信息,即表示 OAP 启动成功:


... ... // 省略其他日志
Server started, host 0.0.0.0 listening on 11800


  • 启动 SkyWalking Rocketbot

在 IDEA 中,找到 apm-webapp 模块,这是 Spring Boot 的 Web项目,执行 ApplicationStartUp  中的 main() 方法。正常启动之后,访问 localhost:8080,看到 SkyWalking Rocketbot 的 UI 界面即为启动成功。

  • 启动 demo-webapp 和 demo-provider

为了验证后端的 OAP 以及前面打包生成的 SkyWalking Agent 是否可用,这里需要启动 demo-webapp 和 demo-provider 两个示例 demo。

  • 首先将整个 skywalking-demo 项目移动到与 SkyWalking 源码项目同一级目录,并导入 IDEA 中,如下图所示:



  • 然后修改 demo-provider 和 demo-webapp 模块的 VM options 参数,将其中 -javaagent: 命令指向的 skywalking-agent.jar 换成 SkyWalking 源码项目中的 skywalking-agent.jar,具体路径如下所示:


SkyWalking源码目录/skywalking-agent/skywalking-agent.jar


  • 其他配置无需修改,依次启动 Zookeeper、demo-provider、demo-webapp。启动成功后访问 http://localhost:8000/hello/xxx

  • 待请求正常响应后,在上一步启动的 SkyWalking Rocketbot 中可以查询到相应的 Trace 信息以及两个项目相关的 Metrics 监控信息,即表示整个源码环境搭建完成。


  • Debug SkyWalking 源码

按照上述方式成功搭建 SkyWalking 源码环境之后,我们尝试 Debug SkyWalking 源码。

  • 首先在 SkyWalking 源码项目中找到 SkyWalkingAgent.java 这个类(位于 apm-sniffer 模块下的 apm-agent 子模块中),该类是 SkyWalking Agent 的入口,提供了 premain() 方法实现,我们可以在其中打一个断点,然后以 Debug 模式重启 demo-webapp,此时 demo-webapp 会停在该断点处,如下图所示:


SkyWalking源码结构

完成 SkyWalking 源码环境的搭建以及 Debug 的测试之后,我们回到 SkyWalking 源码项目,简单介绍一下 SkyWalking 源码中各模块的基本功能。


SkyWalking 源码的整体结构如下图所示:



  • apm-application-toolkit 模块:SkyWalking 提供给用户调用的工具箱。该模块提供了对 log4j、log4j2、logback 等常见日志框架的接入接口,提供了 @Trace 注解等。apm-application-toolkit 模块类似于暴露 API 定义,对应的处理逻辑在 apm-sniffer/apm-toolkit-activation 模块中实现,如下图所示:



  • apm-commons 模块:SkyWalking 的公共组件和工具类。如下图所示,其中包含两个子模块,apm-datacarrier 模块提供了一个生产者-消费者模式的缓存组件(DataCarrier),无论是在 Agent 端还是 OAP 端都依赖该组件。apm-util 模块则提供了一些常用的工具类,例如,字符串处理工具类(StringUtil)、占位符处理的工具类(PropertyPlaceholderHelper、PlaceholderConfigurerSupport)等等。



  • apache-skywalking-apm 目录SkyWalking 打包后使用的命令文件都在此目录中,例如,前文启动 OAP 和 SkyWalking Rocketbot 使用的 startup.sh 文件。

  • apm-protocol 模块:该模块中只有一个 apm-network 模块,我们需要关注的是其中定义的 .proto 文件,定义 Agent 与后端 OAP 使用 gRPC 交互时的协议。

  • apm-sniffer 模块:apm-protocol 模块中有 4 个子模块,如下图所示:



  • apm-agent 模块:其中包含了刚才使用的 SkyWalkingAgent 这个类,是整个 Agent 的入口。

  • apm-agent-core 模块:SkyWalking Agent 的核心实现都在该模块中,也是本课程第二部分重点分析的模块之一。

  • apm-sdk-plugin 模块:SkyWalking Agent 使用了微内核+插件的架构,该模块下包含了 SkyWalking Agent 的全部插件,如下图所示:



  • apm-toolkit-activation 模块:apm-application-toolkit 模块的具体实现,不再赘述。

  • apm-webapp 模块:SkyWalking Rocketbot 对应的后端。

  • oap-server 模块:SkyWalking OAP 的全部实现都在 oap-server 模块,其中包含了多个子模块,如下图所示:



  • exporter 模块:负责导出数据。

  • generate-tool、generate-tool-grammar、generated-analysis 三个模块:与 SkyWalking 自定义的 OAL 语言有关,后面的课时将对 OAL 进行详细介绍。

  • server-alarm-plugin 模块:负责实现 SkyWalking 的告警功能。

  • server-cluster-pulgin 模块:负责 OAP 的集群信息管理,其中提供了接入多种第三方组件的相关插件,如下图所示:



  • server-configuration 模块:负责管理 OAP 的配置信息,也提供了接入多种配置管理组件的相关插件,如下图所示:



  • server-core模块:SkyWalking OAP 的核心实现都在该模块中。

  • server-library 模块:OAP 以及 OAP 各个插件依赖的公共模块,其中提供了双队列 Buffer、请求远端的 Client 等工具类,这些模块都是对立于 SkyWalking OAP 体系之外的类库,我们可以直接拿走使用。

  • server-query-plugin 模块:SkyWalking Rocketbot 发送的请求首先由该模块接收处理,目前该模块只支持 GraphQL 查询。

  • server-receiver-plugin 模块:SkyWalking Agent 发送来的 Metrics、Trace 以及 Register 等写入请求都是首先由该模块接收处理的,不仅如此,该模块还提供了多种接收其他格式写入请求的插件,如下图所示:



  • server-starter 模块:OAP 服务启动的入口。

  • server-storage-plugin 模块:OAP 服务底层可以使用多种存储来保存 Metrics 数据以及Trace 数据,该模块中包含了接入相关存储的插件,如下图所示:



  • skywalking-agent 目录:SkyWalking Agent 编译后生成的 jar 包都会放到该目录中。

  • skywalking-ui 目录:SkyWalking Rocketbot 的前端。

总结

本课时重点介绍了 SkyWalking 源码环境的搭建流程,并在搭建完成之后,启动 skywalking-demo 项目进行了简单的测试。之后深入介绍了 SkyWalking 源码中各个模块的核心功能,了解各模块的主要功能可以让你对后续的源码分析更加游刃有余。


第09讲:SkyWalking Agent 启动流程剖析,领略微内核架构之美

通过此前 8 个课时的学习,相信你已经了解了 SkyWalking Agent 是通过 Java Agent 的方式随应用程序一起启动,然后通过 Byte Buddy 库动态插入埋点收集 Trace 信息。从本课时开始,我会带你深入研究 SkyWalking Agent 的架构、原理以及具体实现,还将深入分析 Tomcat、Dubbo、MySQL 等常用的插件。

微内核架构

SkyWalking Agent 采用了微内核架构(Microkernel Architecture),那什么是微内核架构呢?微内核架构也被称为插件化架构(Plug-in Architecture),是一种面向功能进行拆分的可扩展性架构。在基于产品的应用中通常会使用微内核架构,例如,IDEA、Eclipse 这类 IDE 开发工具,内核都是非常精简的,对 Maven、Gradle 等新功能的支持都是以插件的形式增加的。

如下图所示,微内核架构分为核心系统和插件模块两大部分。

在上图展示的微内核架构中,内核功能是比较稳定的,只负责管理插件的生命周期,不会因为系统功能的扩展而不断进行修改。功能上的扩展全部封装到插件之中,插件模块是独立存在的模块,包含特定的功能,能拓展核心系统的功能。通常,不同的插件模块互相之间独立,当然,你可以设计成一个插件依赖于另外一个插件,但应尽量让插件之间的相互依赖关系降低到最小,避免繁杂的依赖带来扩展性问题。

最终所有插件会由内核系统统一接入和管理:

  • 首先,内核系统必须知道要加载哪些插件,一般会通过配置文件或是扫描 ClassPath 的方式(例如前文介绍的 SPI 技术)确定待加载的插件;
  • 之后,内核系统还需要了解如何使用这些插件,微内核架构中需要定义一套插件的规范,内核系统会按照统一的方式初始化、启动这些插件;
  • 最后,虽然插件之间完全解耦,但实际开发中总会有一些意想不到的需求会导致插件之间产生依赖或是某些底层插件被复用,此时内核需要提供一套规则,识别插件消息并能正确的在插件之间转发消息,成为插件消息的中转站。

由此可见微内核架构的好处:

  • 测试成本下降。从软件工程的角度看,微内核架构将变化的部分和不变的部分拆分,降低了测试的成本,符合设计模式中的开放封闭原则。
  • 稳定性。由于每个插件模块相对独立,即使其中一个插件有问题,也可以保证内核系统以及其他插件的稳定性。
  • 可扩展性。在增加新功能或接入新业务的时候,只需要新增相应插件模块即可;在进行历史功能下线时,也只需删除相应插件模块即可。

SkyWalking Agent 就是微内核架构的一种落地方式。在前面的课时中我已经介绍了 SkyWalking 中各个模块的功能,其中 apm-agent-core 模块对应微内核架构中的内核系统,apm-sdk-plugin 模块中的各个子模块都是微内核架构中的插件模块。

SkyWalking Agent 启动流程概述

此前,在搭建 SkyWalking 源码环境的最后,我们尝试 Debug 了一下 SkyWalking Agent 的源码,其入口是 apm-agent 模块中 SkyWalkingAgent 类的 premain() 方法,其中完成了 Agent 启动的流程:

  1. 初始化配置信息。该步骤中会加载 agent.config 配置文件,其中会检测 Java Agent 参数以及环境变量是否覆盖了相应配置项。
  2. 查找并解析 skywalking-plugin.def 插件文件。
  3. AgentClassLoader 加载插件。
  4. PluginFinder 对插件进行分类管理。
  5. 使用 Byte Buddy 库创建 AgentBuilder。这里会根据已加载的插件动态增强目标类,插入埋点逻辑。
  6. 使用 JDK SPI 加载并启动 BootService 服务。BootService 接口的实现会在后面的课时中展开详细介绍。
  7. 添加一个 JVM 钩子,在 JVM 退出时关闭所有 BootService 服务。

SkywalkingAgent.premain() 方法的具体实现如下,其中省略了 try/catch 代码块以及异常处理逻辑:

public static void premain(String agentArgs, 
       Instrumentation instrumentation) throws PluginException {
    // 步骤1、初始化配置信息
    SnifferConfigInitializer.initialize(agentArgs); 
    // 步骤2~4、查找并解析skywalking-plugin.def插件文件;
    // AgentClassLoader加载插件类并进行实例化;PluginFinder提供插件匹配的功能
    final PluginFinder pluginFinder = new PluginFinder(
       new PluginBootstrap().loadPlugins());
    // 步骤5、使用 Byte Buddy 库创建 AgentBuilder
    final ByteBuddy byteBuddy = new ByteBuddy()
       .with(TypeValidation.of(Config.Agent.IS_OPEN_DEBUGGING_CLASS));
    new AgentBuilder.Default(byteBuddy)...installOn(instrumentation);
    // 这里省略创建 AgentBuilder的具体代码,后面展开详细说
    // 步骤6、使用 JDK SPI加载的方式并启动 BootService 服务。
    ServiceManager.INSTANCE.boot();
    // 步骤7、添加一个JVM钩子
    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
      public void run() { ServiceManager.INSTANCE.shutdown(); }
    }, "skywalking service shutdown thread"));
}

了解了 SkyWalking Agent 启动的核心步骤之后,本课时剩余部分将对每个步骤进行深入分析。

初始化配置

在启动 demo-webapp 和 demo-provider 两个 demo 应用的时候,需要在 VM options 中指定 agent.confg 配置文件(skywalking_config 参数),agent.config 配置文件中的配置项如下:

# 当前应用的服务名称,通过Skywalking Agent上报的Metrics、Trace数据都会
# 携带该信息进行标识
agent.service_name=${SW_AGENT_NAME:Your_ApplicationName}

在  SnifferConfigInitializer.initialize() 方法中会将最终的配置信息填充到 Config 的静态字段中,填充过程如下:

  1. 将 agent.config 文件中全部配置信息填充到 Config 中相应的静态字段中。
  2. 解析系统环境变量值,覆盖 Config 中相应的静态字段。
  3. 解析 Java Agent 的参数,覆盖 Config 中相应的静态字段。

SnifferConfigInitializer.initialize() 方法的具体实现如下:

public static void initialize(String agentOptions) {
    // 步骤1、加载 agent.config配置文件
    InputStreamReader configFileStream = loadConfig();
    Properties properties = new Properties();
    properties.load(configFileStream);
    for (String key : properties.stringPropertyNames()) {
        String value = (String)properties.get(key);
        // 按照${配置项名称:默认值}的格式解析各个配置项
        properties.put(key, PropertyPlaceholderHelper.INSTANCE
            .replacePlaceholders(value, properties));
    }
    // 填充 Config中的静态字段
    ConfigInitializer.initialize(properties, Config.class);
    // 步骤2、解析环境变量,并覆盖 Config中相应的静态字段
    overrideConfigBySystemProp();
    // 步骤3、解析 Java Agent参数,并覆盖 Config中相应的静态字段
    overrideConfigByAgentOptions(agentOptions);
    // 检测SERVICE_NAME和BACKEND_SERVICE两个配置项,若为空则抛异常(略)
    IS_INIT_COMPLETED = true; // 更新初始化标记
}

步骤 1 中的 loadConfig() 方法会优先根据环境变量(skywalking_config)指定的 agent.config 文件路径加载。若环境变量未指定 skywalking_ config 配置,则到 skywalking-agent.jar 同级的 config 目录下查找 agent.confg 配置文件。

将 agent.config 文件中的配置信息加载到 Properties 对象之后,将使用 PropertyPlaceholderHelper 对配置信息进行解析,将当前的“${配置项名称:默认值}”格式的配置值,替换成其中的默认值,demo-provider 解析结果如下图所示:

完成解析之后,会通过 ConfigInitializer 工具类,将配置信息填充到 Config 中的静态字段中,具体填充规则如下:

在接下来的 overrideConfigBySystemProp() 方法中会遍历环境变量(即 System.getProperties() 集合),如果环境变 是以 "skywalking." 开头的,则认为是 SkyWalking 的配置,同样会填充到 Config 类中,以覆盖 agent.config 中的默认值。

最后的 overrideConfigByAgentOptions() 方法解析的是 Java Agent 的参数,填充 Config 类的规则与前面两步相同,不再重复。

到此为止,SkyWalking Agent 启动所需的全部配置都已经填充到 Config 中,后续使用配置信息时直接访问 Config 中的相应静态字段即可。

插件加载原理

完成 Config 类的初始化之后,SkyWalking Agent 开始扫描指定目录下的 SkyWalking Agent 插件 jar 包并进行加载。

AgentClassLoader

SkyWalking Agent 加载插件时使用到一个自定义的 ClassLoader —— AgentClassLoader,之所以自定义类加载器,目的是不在应用的 Classpath 中引入 SkyWalking 的插件 jar 包,这样就可以让应用无依赖、无感知的插件。

并行加载优化

AgentClassLoader 的静态代码块中会调动 tryRegisterAsParallelCapable() 方法,其中会通过反射方式尝试开启 JDK 的并行加载功能:

private static void tryRegisterAsParallelCapable() {
    Method[] methods = ClassLoader.class.getDeclaredMethods();
    for (int i = 0; i < methods.length; i++) {
        Method method = methods[i];
        String methodName = method.getName();
        // 查找 ClassLoader中的registerAsParallelCapable()静态方法
        if ("registerAsParallelCapable".equalsIgnoreCase(methodName)) 
        {
            method.setAccessible(true);
            method.invoke(null); // 调用registerAsParallelCapable()方法
            return;
        }
    }
}

在使用 ClassLoader 加载一个类的时候,JVM 会进行加锁同步,这也是我们能够利用类加载机制实现单例的原因。在 Java 6 中,ClassLoader.loadClass() 方法是用 synchronized 加锁同步的,需要全局竞争一把锁,效率略低。

在 Java 7 之后提供了两种加锁模式:

  • 串行模式下,锁的对象是还是 ClassLoader 本身,和 Java 6 里面的行为一样;
  • 另外一种就是调用 registerAsParallelCapable() 方法之后,开启的并行加载模式。在并行模式下加载类时,会按照 classname 去获取锁。ClassLoader.loadClass() 方法中相应的实现片段如下:
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException{
    // getClassLoadingLock() 方法会返回加锁的对象
    synchronized (getClassLoadingLock(name)) { 
       ... ... // 加载指定类,具体加载细节不展开介绍
    }
}

protected Object getClassLoadingLock(String className) {
   Object lock = this;
   if (parallelLockMap != null) { // 检测是否开启了并行加载功能
       Object newLock = new Object();
       // 若开启了并行加载,则一个className对应一把锁;否则还是只
       // 对当前ClassLoader进行加锁
       lock = parallelLockMap.putIfAbsent(className, newLock);
       if (lock == null) {
           lock = newLock;
       }
   }
   return lock;
}

AgentClassLoader 核心实现

在 AgentClassLoader 的构造方法中会初始化其 classpath 字段,该字段指向了 AgentClassLoader 要扫描的目录(skywalking-agent.jar 包同级别的 plugins 目录和 activations 目录),如下所示:

private List<File> classpath; 

public AgentClassLoader(ClassLoader parent) {
   super(parent); // 双亲委派机制
   // 获取 skywalking-agent.jar所在的目录
   File agentDictionary = AgentPackagePath.getPath();
   classpath = new LinkedList<File>();
   // 初始化 classpath集合,指向了skywalking-agent.jar包同目录的两个目录
   classpath.add(new File(agentDictionary, “plugins”));
   classpath.add(new File(agentDictionary, “activations”));
}

AgentClassLoader 作为一个类加载器,主要工作还是从其 Classpath 下加载类(或资源文件),对应的就是其 findClass() 方法和 findResource() 方法,这里简单看一下 findClass() 方法的实现:

// 在下面的getAllJars()方法中会扫描全部jar文件,并缓存到
// allJars字段(List<Jar>类型)中,后续再次扫描时会重用该缓
private List<Jar> allJars;

protected Class<?> findClass(String name) {
   List<Jar> allJars = getAllJars();  // 扫描过程比较简单,不再展开介绍
   String path = name.replace(‘.’, ‘/’).concat(“.class”);
   for (Jar jar : allJars) { // 扫描所有jar包,查找类文件
       JarEntry entry = jar.jarFile.getJarEntry(path);
       if (entry != null) {
           URL classFileUrl = new URL(“jar:file:” +
               jar.sourceFile.getAbsolutePath() + “!/” + path);
           byte[] data = …;// 省略读取".class"文件的逻辑                          // 加载类文件内容,创建相应的Class对象
           return defineClass(name, data, 0, data.length);
       }
   } // 类查找失败,直接抛出异常
   throw new ClassNotFoundException("Can’t find " + name);
}

findResource() 方法会遍历 allJars 集合缓存的全部 jar 包,从中查找指定的资源文件并返回,遍历逻辑与 findClass() 方法类似,不再展开分析。

最后,AgentClassLoader 中有一个 DEFAULT_LOADER 静态字段,记录了 默认的 AgentClassLoader,如下所示,但是注意,AgentClassLoader 并不是单例,后面会看到其他创建 AgentClassLoader 的地方。

private static AgentClassLoader DEFAULT_LOADER;

解析插件定义

每个 Agent 插件中都会定义一个 skywalking-plugin.def 文件,如下图 tomcat-7.x-8.x-plugin 插件所示:

tomcat-7.x-8.x-plugin 插件中 skywalking-plugin.def 文件的内容如下,其中每一行都是一个插件类的定义:

tomcat-7.x/8.x=org.apache.skywalking.apm.plugin.tomcat78x.define 
.TomcatInstrumentation

tomcat-7.x/8.x=org.apache.skywalking.apm.plugin.tomcat78x.define
.ApplicationDispatcherInstrumentation

PluginResourcesResolver 是 Agent 插件的资源解析器,会通过 AgentClassLoader 中的 findResource() 方法读取所有 Agent 插件中的 skywalking-plugin.def 文件。

AbstractClassEnhancePluginDefine

拿到全部插件的 skywalking-plugin.def 文件之后,PluginCfg 会逐行进行解析,转换成 PluginDefine 对象。PluginDefine 中有两个字段:

// 插件名称,以 tomcat-7.x-8.x-plugin 插件第一行为例,就是tomcat-7.x/8.x
private String name; 
// 插件类,对应上例中的 org.apache.skywalking.apm.plugin.tomcat78x.define
// .TomcatInstrumentation
private String defineClass;

PluginCfg 是通过枚举实现的、单例的工具类,逻辑非常简单,不再展开介绍。

接下来会遍历全部 PluginDefine 对象,通过反射将其中 defineClass 字段中记录的插件类实例化,核心逻辑如下:

for (PluginDefine pluginDefine : pluginClassList) {
    // 注意,这里使用类加载器是默认的AgentClassLoader实例
    AbstractClassEnhancePluginDefine plugin =
        (AbstractClassEnhancePluginDefine)
            Class.forName(pluginDefine.getDefineClass(), true,
            AgentClassLoader.getDefault()).newInstance();
    plugins.add(plugin); // 记录AbstractClassEnhancePluginDefine 对象
}

AbstractClassEnhancePluginDefine 抽象类是所有 Agent 插件类的顶级父类,其中定义了四个核心方法,决定了一个插件类应该增强哪些目标类、应该如何增强、具体插入哪些逻辑,如下所示:

  • enhanceClass() 方法:返回的 ClassMatch,用于匹配当前插件要增强的目标类。
  • define() 方法:插件类增强逻辑的入口,底层会调用下面的 enhance() 方法和 witnessClass() 方法。
  • enhance() 方法:真正执行增强逻辑的地方。
  • witnessClass() 方法:一个开源组件可能有多个版本,插件会通过该方法识别组件的不同版本,防止对不兼容的版本进行增强。

在后续的课时中会详细介绍每个方法的具体功能和实现,你先知道 AbstractClassEnhancePluginDefine 中大致有这四个方法即可。

ClassMatch

enhanceClass() 方法决定了一个插件类要增强的目标类,返回值为 ClassMatch 类型对象。ClassMatch 类似于一个过滤器,可以通过多种方式匹配到目标类,ClassMatch 接口的实现如下:

  • **NameMatch:**根据其 className 字段(String 类型)匹配目标类的名称。
  • **IndirectMatch:**子接口中定义了两个方法。
// Junction是Byte Buddy中的类,可以通过and、or等操作串联多个ElementMatcher
// 进行匹配
ElementMatcher.Junction buildJunction(); 
// 用于检测传入的类型是否匹配该Match
boolean isMatch(TypeDescription typeDescription);
  • MultiClassNameMatch:其中会指定一个 matchClassNames 集合,该集合内的类即为目标类。
  • ClassAnnotationMatch:根据标注在类上的注解匹配目标类。
  • MethodAnnotationMatch:根据标注在方法上的注解匹配目标类。
  • HierarchyMatch:根据父类或是接口匹配目标类。

这里以 ClassAnnotationMatch 为例展开分析,其中的 annotations 字段(String[] 类型)指定了该 ClassAnnotationMatch 对象需要检查的注解。在 buildJunction() 方法中将为每一个注解创建相应的 Junction 并将它们以 and 形式连接起来并返回,如下所示:

public ElementMatcher.Junction buildJunction() {
    ElementMatcher.Junction junction = null;
    for (String annotation : annotations) { // 遍历全部注解
        if (junction == null) { 
            // 该Junction用于检测类是否标注了指定注解
            junction = buildEachAnnotation(annotation);
        } else {// 使用 and 方式将所有Junction对象连接起来
            junction = junction.and(buildEachAnnotation(annotation));
        }
    }
    junction = junction.and(not(isInterface())); // 排除接口
    return junction;
}

isMatch() 方法的实现类似,只有包含所有指定注解的类,才能匹配成功,如下所示:

public boolean isMatch(TypeDescription typeDescription) {
    List<String> annotationList = 
        new ArrayList<String>(Arrays.asList(annotations));
    // 获取该类上的注解
    AnnotationList declaredAnnotations = 
          typeDescription.getDeclaredAnnotations();
    // 匹配一个删除一个
    for (AnnotationDescription annotation : declaredAnnotations) {
        annotationList.remove(annotation
              .getAnnotationType().getActualName());
    }
    if (annotationList.isEmpty()) { // 删空了,就匹配成功了
        return true;
    }
    return false;
}

其他 ClassMatch 接口的实现原理类似,不再展开分析,如果你感兴趣可以看一下代码。

PluginFinder

PluginFinder 是 AbstractClassEnhancePluginDefine 查找器,可以根据给定的类查找用于增强的 AbstractClassEnhancePluginDefine 集合。

在 PluginFinder 的构造函数中会遍历前面课程已经实例化的 AbstractClassEnhancePluginDefine ,并根据 enhanceClass() 方法返回的 ClassMatcher 类型进行分类,得到如下两个集合:

// 如果返回值为NameMatch类型,则相应 AbstractClassEnhancePluginDefine 
// 对象会记录到该集合
private Map<String, LinkedList<AbstractClassEnhancePluginDefine>> nameMatchDefine;

// 如果是其他类型返回值,则相应 AbstractClassEnhancePluginDefine
// 对象会记录到该集合
private List<AbstractClassEnhancePluginDefine> signatureMatchDefine;

find() 方法是 PluginFinder 对外暴露的查询方法,其中会先后遍历 nameMatchDefine 集合和 signatureMatchDefine 集合,通过 ClassMatch.isMatch() 方法确定所有的匹配插件。find() 方法的实现并不复杂,不再展开介绍。

AgentBuilder

前面已经分析了 Skywalking Agent 启动过程中加载配置信息、初始化 Config 类、查找 skywalking-pluing.def 文件、初始化 AbstractClassEnhancePluginDefine 对象等步骤。现在开始介绍 Byte Buddy 如何使用加载到的插件类增强目标方法。

在 SkywalkingAgent.premain() 方法中的步骤 5 中,首先会创建 ByteBuddy 对象,正如前面 Byte Buddy 基础课时中提到的,它是 Byte Buddy 的基础对象之一:

// 步骤5、通过Byte Buddy API创建Agent
final ByteBuddy byteBuddy = new ByteBuddy()
   .with(TypeValidation.of(Config.Agent.IS_OPEN_DEBUGGING_CLASS));

Config.Agent.IS_OPEN_DEBUGGING_CLASS 在 agent.config 文件中对应的配置项是:

agent.is_open_debugging_class

如果将其配置为 true,则会将动态生成的类输出到 debugging 目录中。

接下来创建 AgentBuilder 对象,AgentBuilder 是 Byte Buddy 库专门用来支持 Java Agent 的一个 API,如下所示:

new AgentBuilder.Default(byteBuddy) // 设置使用的ByteBuddy对象
.ignore(nameStartsWith("net.bytebuddy.")// 不会拦截下列包中的类
       .or(nameStartsWith("org.slf4j."))
       .or(nameStartsWith("org.apache.logging."))
       .or(nameStartsWith("org.groovy."))
       .or(nameContains("javassist"))
       .or(nameContains(".asm."))
       .or(nameStartsWith("sun.reflect"))
       .or(allSkyWalkingAgentExcludeToolkit()) // 处理 Skywalking 的类
       // synthetic类和方法是由编译器生成的,这种类也需要忽略
       .or(ElementMatchers.<TypeDescription>isSynthetic()))
.type(pluginFinder.buildMatch())// 拦截
.transform(new Transformer(pluginFinder)) // 设置Transform
.with(new Listener()) // 设置Listener
.installOn(instrumentation)

简单解释一下这里使用到的 AgentBuilder 的方法:

  • ignore() 方法:忽略指定包中的类,对这些类不会进行拦截增强。
  • type() 方法:在类加载时根据传入的 ElementMatcher 进行拦截,拦截到的目标类将会被 transform() 方法中指定的 Transformer 进行增强。
  • transform() 方法:这里指定的 Transformer 会对前面拦截到的类进行增强。
  • with() 方法:添加一个 Listener 用来监听 AgentBuilder 触发的事件。

首先, PluginFInder.buildMatch() 方法返回的 ElementMatcher 对象会将全部插件的匹配规则(即插件的 enhanceClass() 方法返回的 ClassMatch)用 OR 的方式连接起来,这样,所有插件能匹配到的所有类都会交给 Transformer 处理。

再来看 with() 方法中添加的监听器 —— SkywalkingAgent.Listener,它继承了 AgentBuilder.Listener 接口,当监听到 Transformation 事件时,会根据 IS_OPEN_DEBUGGING_CLASS 配置决定是否将增强之后的类持久化成 class 文件保存到指定的 log 目录中。注意,该操作是需要加锁的,会影响系统的性能,一般只在测试环境中开启,在生产环境中不会开启。

最后来看 Skywalking.Transformer,它实现了 AgentBuilder.Transformer 接口,其 transform() 方法是插件增强目标类的入口。Skywalking.Transformer 会通过 PluginFinder 查找目标类匹配的插件(即 AbstractClassEnhancePluginDefine 对象),然后交由 AbstractClassEnhancePluginDefine 完成增强,核心实现如下:

public DynamicType.Builder<?> transform(DynamicType.Builder<?>builder,
    TypeDescription typeDescription, // 被拦截的目标类
    ClassLoader classLoader,  // 加载目标类的ClassLoader
    JavaModule module) {
    // 从PluginFinder中查找匹配该目标类的插件,PluginFinder的查找逻辑不再重复
    List<AbstractClassEnhancePluginDefine> pluginDefines =
           pluginFinder.find(typeDescription);
    if (pluginDefines.size() >0){ 
        DynamicType.Builder<?>newBuilder = builder;
        EnhanceContext context = new EnhanceContext();
        for (AbstractClassEnhancePluginDefinedefine : pluginDefines) {
            // AbstractClassEnhancePluginDefine.define()方法是插件入口,
            // 在其中完成了对目标类的增强
            DynamicType.Builder<?>possibleNewBuilder = 
                 define.define(typeDescription, 
                      newBuilder, classLoader,context);
            if (possibleNewBuilder != null) {
                // 注意这里,如果匹配了多个插件,会被增强多次
                newBuilder = possibleNewBuilder;
            }
        }
        return newBuilder;
    }
    return builder;
}

这里需要注意:如果一个类被多个插件匹配会被增强多次,当你打开 IS_OPEN_DEBUGGING_CLASS 配置项时,会看到对应的多个 class 文件。

加载 BootService

SkyWalking Agent  启动的最后一步是使用前面介绍的 JDK SPI 技术加载 BootService 接口的所有实现类,BootService 接口中定义了 SkyWalking Agent 核心服务的行为,其定义如下:

public interface BootService {
    void prepare() throws Throwable;
    void boot() throws Throwable;
    void onComplete() throws Throwable;
    void shutdown() throws Throwable;
}

ServiceManager 是 BootService 实例的管理器,主要负责管理 BootService 实例的生命周期。

ServiceManager 是个单例,底层维护了一个 bootedServices 集合(Map<Class, BootService> 类型),记录了每个 BootService 实现对应的实例。boot() 方法是 ServiceManager 的核心方法,它首先通过 load() 方法实例化全部 BootService 接口实现,如下所示:

void load(List<BootService> allServices) { 
    // 很明显使用了 JDK SPI 技术加载并实例化 META-INF/services下的全部 
    // BootService接口实现
    Iterator<BootService> iterator = ServiceLoader.load(
        BootService.class,AgentClassLoader.getDefault()).iterator();
    while (iterator.hasNext()) {
        // 记录到方法参数传入的 allServices集合中
        allServices.add(iterator.next()); 
    }
}

在 apm-agent-core 模块的 resource/META-INF.services/org.apache.skywalking.apm.agent.core.boot.BootService 文件中,记录了 ServiceManager 要加载的 BootService 接口实现类,如下所示,这些类在后面的课时中会逐个详细介绍其具体功能:

org.apache.skywalking.apm.agent.core.remote.TraceSegmentServiceClient
org.apache.skywalking.apm.agent.core.context.ContextManager
org.apache.skywalking.apm.agent.core.sampling.SamplingService
org.apache.skywalking.apm.agent.core.remote.GRPCChannelManager
org.apache.skywalking.apm.agent.core.jvm.JVMService
org.apache.skywalking.apm.agent.core.remote.ServiceAndEndpointRegisterClient
org.apache.skywalking.apm.agent.core.context.ContextManagerExtendService

加载完上述 BootService 实现类型之后,ServiceManager 会针对 BootService 上的 @DefaultImplementor 和 @OverrideImplementor 注解进行处理:

  • @DefaultImplementor 注解用于标识 BootService 接口的默认实现。
  • @OverrideImplementor 注解用于覆盖默认 BootService 实现,通过其 value 字段指定要覆盖的默认实现。

BootService 的覆盖逻辑如下图所示:

确定完要使用的 BootService 实现之后,ServiceManager 将统一初始化 bootServices 集合中的 BootService 实现,同样是在 ServiceManager.boot() 方法中,会逐个调用 BootService 实现的 prepare()、startup()、onComplete() 方法,具体实现如下:

public void boot() {
    bootedServices = loadAllServices(); 
    prepare(); // 调用全部BootService对象的prepare()方法
    startup(); // 调用全部BootService对象的boot()方法
    onComplete(); // 调用全部BootService对象的onComplete()方法
}

在 Skywalking Agent 启动流程的最后,会添加一个 JVM 退出钩子,并通过 ServiceManager.shutdown() 方法,关闭前文启动的全部 BootService 服务。

SkywalkingAgent.premain() 方法中相关的代码片段如下:

Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
    @Override public void run() {
        ServiceManager.INSTANCE.shutdown(); 
    }
}, "skywalking service shutdown thread"));

总结

本课时重点介绍了 SkyWalking Agent 启动核心流程的实现,深入分析了 Skywalking Agent 配置信息的初始化、插件加载原理、AgentBuilder 如何与插件类配合增强目标类、BootService 的加载流程。本课时是整个 Skywalking Agent 的框架性流程介绍,在后续的课时中将详细介绍 AbstractClassEnhancePluginDefine 以及 BootService 接口的实现。


第10讲:深入剖析 Agent 插件原理,无侵入性埋点

在前面介绍 SkyWalking Agent 启动流程时,同时介绍了插件中 skywalking-agent.def 文件的查找、解析流程,AbstractClassEnhancePluginDefine 抽象类的核心定义,以及插件类与 AgentBuilder 配合为目标类动态添加埋点功能的核心流程。本课时将深入介绍 AbstractClassEnhancePluginDefine 抽象类以及其子类的运行原理。

AbstractClassEnhancePluginDefine 核心实现

在开始之前,先简单回顾上一课时中关于 AbstractClassEnhancePluginDefine 的一个核心知识点:AbstractClassEnhancePluginDefine 是所有插件的父类,SkywalkingAgent.Transformer 会通过其 enhanceClass() 方法返回的 ClassMatch 对象,匹配到要增强的目标类。在不同的插件实现类中,enhanceClass() 方法返回的 ClassMatch 对象不同,例如:

  • Dubbo 插件拦截的是 com.alibaba.dubbo.monitor.support.MonitorFilter 这个类;
  • Tomcat 插件拦截的是 org.apache.catalina.core.StandardHostValve 这个类。

后面会详细介绍上述两个插件的具体实现。

完成目标类和插件类的匹配之后,会进入 define() 方法,其核心逻辑如下:

  1. 通过 witnessClass() 方法确定当前插件与当前拦截到的目标类的版本是否匹配。若版本不匹配,则 define() 方法直接结束,当前插件类不会增强该类;若版本匹配,则继续后续逻辑。
  2. 进入 enhance() 方法执行增强逻辑。
  3. 设置插件增强标识。

witnessClass() 方法

很多开源组件和工具类库的功能会不断增加,架构也会随之重构,导致不同版本的兼容性得不到很好的保证。例如,MySQL 常用的版本有 5.6、5.7、8.0 多个版本,在使用 JDBC 连接 MySQL 时使用的 mysql-connector-java.jar 包也分为 5.x、6.x、8.x 等版本,对应的 JDBC 协议的版本也各不相同。

SkyWalking Agent 提供的 MySQL 插件本质上是增强 mysql-connector-java.jar 中的关键方法,例如 ConnectionImpl.getInstance() 方法,但在 mysql-connector-java.jar 的 5.x 版本和 8.x 版本中,ConnectionImpl 的包名不同,如下所示:

这仅仅是一个简单的示例,在有的开源组件或类库中,不同版本中同名类的功能和结构已经发生了翻天覆地的变化。要通过一个 SkyWalking Agent 插件完成对一个开源组件所有版本的增强,是非常难实现的,即使勉强能够实现,该插件的实现也会变的非常臃肿,扩展性也会成问题。

SkyWalking 怎么解决这个问题呢?回到 MySQL 示例,SkyWalking 为每个版本的 mysql-connector-java.jar 提供了不同版本的插件,这些插件的 witnessClass() 方法返回值不同,具体返回的是对应版本 mysql-connector-java.jar 所特有的一个类,如下表所示:

若当前类加载器无法扫描到插件 witnessClass() 方法指定的类,表示当前插件版本不合适,即使拦截到了目标类,也不能进行增强。AbstractClassEnhancePluginDefine.define() 方法中的相关片段如下:

String[] witnessClasses = witnessClasses(); 
if (witnessClasses != null) {
    for (String witnessClass : witnessClasses) {
        // 判断指定类加载器中是否存在witnessClasses()指定的类
        if (!WitnessClassFinder.INSTANCE.exist(witnessClass,
               classLoader)) { 
                return null// 若不存在则表示版本不匹配,直接返回
        }
    }
}

增强 static 静态方法

完成上述插件版本的匹配之后,开始进入 enhance() 方法对目标类进行增强。如下图所示, ClassEnhancePluginDefine 继承了 AbstractClassEnhancePluginDefine 抽象类:

在 ClassEnhancePluginDefine 实现的 enhance() 方法中,会分别完成对 static 静态方法以及实例方法的增强:

protected DynamicType.Builder<?> enhance(...) throws PluginException {
    // 增强static方法
    newClassBuilder = this.enhanceClass(typeDescription, 
            newClassBuilder, classLoader); 
    // 增强构造方法和实例方法
    newClassBuilder = this.enhanceInstance(typeDescription, 
            newClassBuilder, classLoader, context); 
    return newClassBuilder;
}

在增强静态方法时会使用到 StaticMethodsInterceptPoint 这个接口,它描述了当前插件要拦截目标类的哪些 static 静态方法,以及委托给哪个类去增强,其定义如下:

public interface StaticMethodsInterceptPoint {
    // 用于匹配目标静态方法
    ElementMatcher<MethodDescription> getMethodsMatcher(); 
    // 拦截到的静态方法交给哪个Interceptor来增强
    String getMethodsInterceptor();
    // 增强过程中是否需要修改参数
    boolean isOverrideArgs();
}

这里以 mysql-8.x-plugin 插件中的实现为例进行说明,其中ConnectionImplCreateInstrumentation 这个插件类的 enhanceClass() 方法如下:

protected ClassMatch enhanceClass() // 拦截目标类为ConnectionImpl
    return byName("com.mysql.cj.jdbc.ConnectionImpl");
}

其 getStaticMethodsInterceptPoints() 方法返回的下面这个 StaticMethodsInterceptPoint 实现(StaticMethodsInterceptPoint 接口的实现基本都是这种匿名内部类):

new StaticMethodsInterceptPoint[] {
    new StaticMethodsInterceptPoint() {
        @Override
        public ElementMatcher<MethodDescription> getMethodsMatcher() {
            return named("getInstance"); // 增强 getInstance()方法
        }

        @Override
        public String getMethodsInterceptor() { 
            // 委托给 ConnectionCreateInterceptor进行增强
            return “org.apache.skywalking.apm.plugin.jdbc
                       .mysql.v8.ConnectionCreateInterceptor”;
        }

        @Override
        public boolean isOverrideArgs() {
            return false; // 增强过程中无需修改方法参数
        }
    }
}

也就是说,ConnectionImplCreateInstrumentation 这个插件拦截的是  com.mysql.jdbc.ConnectionImpl.getInstance() 这个静态方法。

接下来回到 ClassEnhancePluginDefine.enhanceClass() 方法的具体实现:

private DynamicType.Builder<?> enhanceClass(TypeDescription typeDescription,
    DynamicType.Builder<?> newClassBuilder, ClassLoader classLoader) throws PluginException {
    // 获取当前插件的静态方法拦截点,如果该插件不增强静态方法,则该数组为空
    StaticMethodsInterceptPoint[] staticMethodsInterceptPoints = 
          getStaticMethodsInterceptPoints();
    String enhanceOriginClassName = typeDescription.getTypeName();
    for (StaticMethodsInterceptPoint staticMethodsInterceptPoint :
              staticMethodsInterceptPoints) {
        // 进行具体增强的Interceptor名称
        String interceptor = staticMethodsInterceptPoint
              .getMethodsInterceptor();
        // 在增强过程中,是否要修改参数。
        if (staticMethodsInterceptPoint.isOverrideArgs()) {
            // 前面介绍了 Byte Buddy 用法,这里也是一样的,通过method()方法
            // 指定拦截方法的条件
            newClassBuilder = newClassBuilder.method(isStatic()
              .and(staticMethodsInterceptPoint.getMethodsMatcher())) 
                .intercept( 
                    MethodDelegation.withDefaultConfiguration()
                        .withBinders( // 要用Morph注解,需要先绑定
Morph.Binder.install(OverrideCallable.class)
                        // StaticMethodsInterWithOverrideArgs后面展开说
                        ).to(new StaticMethodsInterWithOverrideArgs(interceptor))
                );
        } else { // 下面是不需要修改参数的增强
            newClassBuilder = newClassBuilder.method(isStatic()
             .and(staticMethodsInterceptPoint.getMethodsMatcher()))
                .intercept(MethodDelegation.withDefaultConfiguration()
                .to(new StaticMethodsInter(interceptor))
            );
        }
    }
    return newClassBuilder;
}

根据前文对 Byte Buddy API 的介绍,通过 method() 方法拦截到静态方法之后,如果需要修改方法参数,则会通过 StaticMethodsInterWithOverrideArgs 对象进行增强,其中的 intercept() 方法是其核心实现:

@RuntimeType
public Object intercept(@Origin Class<?> clazz, 
        @AllArguments Object[] allArguments, @Origin Method method,
           @Morph OverrideCallable zuper) throws Throwable {
    // 加载插件指定的StaticMethodsAroundInterceptor
    StaticMethodsAroundInterceptor interceptor = 
       InterceptorInstanceLoader
        .load(staticMethodsAroundInterceptorClassName,
              clazz.getClassLoader());
    MethodInterceptResult result = new MethodInterceptResult();
    // 调用 interceptor.before()做前置处理
    interceptor.beforeMethod(clazz, method, allArguments, 
        method.getParameterTypes(), result);
    Object ret = null;
    try {
        // 根据before()的处理结果判定是否调用目标方法
        if (!result.isContinue()) { 
            ret = result._ret();
        } else {
            // 注意:这里是需要传参的,这些参数我们是可以在before()方法中改动
            // 的,这就是OverrideArgs的意义
            ret = zuper.call(allArguments); 
        }
    } catch (Throwable t) {
        // 如果出现异常,会先通知interceptor中的
        // handleMethodException()方法进行处理
        interceptor.handleMethodException(clazz, method, allArguments, 
            method.getParameterTypes(), t);
        throw t;
    } finally { // 通过after()方法进行后置处理
        ret = interceptor.afterMethod(clazz, method, allArguments,
              method.getParameterTypes(), ret);
    }
    return ret;
}

如果不需要修改方法参数,则会通过 StaticMethodsInter 对象进行增强,其实现与 StaticMethodsInterWithOverrideArgs 类似,唯一区别在于调用目标方法时无法修改参数。

上面使用的 StaticMethodsAroundInterceptor 是个接口,其中定义了如下三个方法:

  • before():在目标方法之前调用。
  • after():在目标方法之后调用。
  • handleMethodException():在目标方法抛出异常时调用。

通过实现 StaticMethodsAroundInterceptor 接口,各个 Agent 插件就可以在静态方法前后添加自定义的逻辑了。

前面提到的 mysql-8.x-plugin 中的 ConnectionImplCreateInstrumentation 自然也实现了该接口。通过对 StaticMethodsInterWithOverrideArgs 以及 StaticMethodsAroundInterceptor 接口的介绍,我们会发现 Agent 插件对静态方法的增强逻辑与 Spring AOP 中环绕通知的逻辑非常类似。

设计模式 TIP
ClassEnhancePluginDefine 是个典型的模板方法模式的使用场景,其 enhanceClass() 方法只实现了增强静态方法的基本流程,真正的增强逻辑全部通过 getStaticMethodsInterceptPoints() 抽象方法推迟到子类实现。在后面增强对象的构造方法和实例方法时,同样会看到类似的实现。

增强实例对象

分析完增强 static 静态方法的相关逻辑之后,我们继续分析增强一个 Java 实例对象的相关逻辑 —— 入口是 enhanceInstance() 方法。enhanceInstance() 方法将分成三个部分来分析其实现:

  • 实现 EnhancedInstance 接口
  • 增强构造方法
  • 增强实例方法
实现 EnhancedInstance 接口

enhanceInstance() 方法首先会为目标类添加了一个字段,同时会让目标类实现 EnhancedInstance 接口,具体实现如下:

// EnhanceContext记录了整个增强过程中的上下文信息,里面就两个boolean值
if (!context.isObjectExtended()) { 
    newClassBuilder = newClassBuilder
        // 定义一个字段private volatile的字段,该字段为Object类型
        .defineField("_$EnhancedClassField_ws", Object.class, 
              ACC_PRIVATE | ACC_VOLATILE)
        // 实现EnhancedInstance接口的方式是读写新
        // 增的"_$EnhancedClassField_ws"字段
        .implement(EnhancedInstance.class) 
        .intercept(FieldAccessor.ofField(CONTEXT_ATTR_NAME));
    context.extendObjectCompleted(); // 标记一下上线文信息
}

EnhancedInstance 接口中定义了 getSkyWalkingDynamicField() 和setSkyWalkingDynamicField() 两个方法,分别读写新增的 _$EnhancedClassField_ws 字段。以前文 demo-webapp 示例中的 HelloWorldController 这个类为例,在 skywalking-agent/debugging/ 目录下可以看到增强后的类如下:

// 实现了EnhancedInstance接口
public class HelloWorldController implements EnhancedInstance {
    private volatile Object _$EnhancedClassField_ws; // 新增字段
    // 对EnhancedInstance的实现
    public Object getSkyWalkingDynamicField() { 
        return this._$EnhancedClassField_ws;
    }

    public void setSkyWalkingDynamicField(Object var1) {
        this._$EnhancedClassField_ws = var1;
    }
    … … // 省略其他业务逻辑相关的方法
}

增强构造方法

接下来,ehanceInstance() 方法会增强实例对象的构造方法,具体流程与增强 static 静态方法的流程类似,唯一区别是这里使用的是 ConstructorInterceptPoint,相关代码片段如下:

ConstructorInterceptPoint[] constructorInterceptPoints = 
   getConstructorsInterceptPoints();
for (ConstructorInterceptPoint constructorInterceptPoint : 
       constructorInterceptPoints) { 
    newClassBuilder = newClassBuilder.constructor(
      constructorInterceptPoint.getConstructorMatcher())
      // 这里对 SuperMethodCall的使用方式和介绍 Byte Buddy基础时说的一毛一样
      .intercept(SuperMethodCall.INSTANCE
          .andThen(MethodDelegation.withDefaultConfiguration() 
             .to(new ConstructorInter(constructorInterceptPoint
                .getConstructorInterceptor(), classLoader))
        )
    );
}

ConstructorInterceptPoint 中描述了插件要增强的构造方法以及增强的 Interceptor 类,与StaticMethodsInterceptPoint 类似,不再展开介绍。

ConstructorInter 与 StaticMethodsInter 类似(这里没有修改构造方法参数的 OverriderArgs 版本,因为此时的构造方法已经调用完成了),ConstructorInter.intercept() 方法的实现如下:

@RuntimeType
public void intercept(@This Object obj, 
        @AllArguments Object[] allArguments) {
    // 前面已经让该对象实现了EnhancedInstance接口,所以这里的类型转换是安全
    EnhancedInstance targetObject = (EnhancedInstance)obj;
    interceptor.onConstruct(targetObject, allArguments);
}

这里使用的 InstanceConstructorInterceptor 接口与前文介绍的 StaticMethodsAroundInterceptor 接口作用相同,都是留给各个插件去实现增强逻辑的。InstanceConstructorInterceptor 接口的定义如下:

public interface InstanceConstructorInterceptor {
    void onConstruct(EnhancedInstance objInst, Object[] allArguments);
}
mysql-8.x-plugin 插件对 ConnectionImpl 的增强

到这里你可能感觉实现逻辑有点乱,这里我将以 mysql-8.x-plugin 插件为例,把静态方法增强、构造方法增强等逻辑串起来。

首先来看 mysql-connector-java-8.x.jar 中  com.mysql.cj.jdbc.ConnectionImpl.getInstance() 方法,这是我们创建数据连接的最常用方法,具体实现:

public static JdbcConnection getInstance(HostInfo hostInfo)
       throws SQLException {
    return new ConnectionImpl(hostInfo);  // 创建 ConnectionImpl实例
}

先来看 mysql-8.x-plugin 模块的 skywalking-plugin.def 文件,其中定义了ConnectionInstrumentation 这个插件类,它会被 AgentClassLoader 加载,其 enhanceClass() 方法返回的 Matcher 拦截的目标类是 com.mysql.cj.jdbc.ConnectionImpl。

虽然 ConnectionInstrumentation 并不拦截构造方法(因为它的 getConstructorsInterceptPoints() 方法返回的是空数组),但是依然会修改 ConnectionImpl,为其添加 _$EnhancedClassField_ws 字段并实现 EnhanceInstance接口。

在 skywalking-plugin.def 文件中还定义了 ConnectionImplCreateInstrumentation 这个插件类,正如前面介绍的那样,它会拦截 com.mysql.cj.jdbc.ConnectionImpl 的 getInstance() 方法,并委托给 ConnectionCreateInterceptor 进行增强。ConnectionCreateInterceptor 中的 before() 和 handleMethodException() 方法都是空实现,其 after() 方法会记录新建 Connection 的一些信息,具体实现如下:

public Object afterMethod(Class clazz, Method method, 
      Object[] allArguments, Class<?>[] parameterTypes, Object ret) {
    if (ret instanceof EnhancedInstance) { // ConnectionImpl已经被增强了
        // ConnectionInfo中记录了DB名称、DB类型以及地址等等信息,具体构造过程省
        // 略,它会被记录到前面新增的 _$EnhancedClassField_ws 那个字段中
        ConnectionInfo connectionInfo = ...
        ((EnhancedInstance) ret).setSkyWalkingDynamicField(
            connectionInfo);
    }
    return ret;
}

另外,这里还会看到一个 AbstractMysqlInstrumentation 抽象类,继承关系如下图所示:

AbstractMysqlInstrumentation 实现了 witnessClasses() 方法以及 ClassEnhancePluginDefine 中的三个 get*InterceptPoints() 抽象方法(这三个方法都返回 null),其中 witnessClasses() 方法返回"com.mysql.cj.interceptors.QueryInterceptor"字符串,witnessClasses() 方法作用不再重复。

AbstractMysqlInstrumentation 的子类只需根据需求实现相应的 get*InterceptPoints() 方法即可,无需再提供其他剩余 get*InterceptPoints() 方法的空实现。在其他版本的 MySQL 插件中也有 AbstractMysqlInstrumentation 这个抽象类,功能相同,不再重复。

增强实例方法

最后,我们来看 enhanceInstance() 方法对实例方法的增强,其实和增强静态方法的套路一样,我们直接看代码吧:

InstanceMethodsInterceptPoint[] instanceMethodsInterceptPoints = 
    getInstanceMethodsInterceptPoints();
for (InstanceMethodsInterceptPoint instanceMethodsInterceptPoint : 
           instanceMethodsInterceptPoints) {
    String interceptor = instanceMethodsInterceptPoint
         .getMethodsInterceptor();
    // 目标方法的匹配条件
    ElementMatcher.Junction<MethodDescription> junction =
       not(isStatic()).and(instanceMethodsInterceptPoint
         .getMethodsMatcher()); 
    if (instanceMethodsInterceptPoint instanceof 
            DeclaredInstanceMethodsInterceptPoint) {
        // 目标方法必须定义在目标类中
        junction = junction.and(ElementMatchers.
             <MethodDescription>isDeclaredBy(typeDescription)); 
    }
    if (instanceMethodsInterceptPoint.isOverrideArgs()){ //修改方法参数
        newClassBuilder = newClassBuilder
           .method(junction) // 匹配目标方法
           .intercept(MethodDelegation.withDefaultConfiguration()
            // 使用@Morph注解之前,需要通过Morph.Binder绑定一下
            .withBinders(Morph.Binder.install(OverrideCallable.class))
            .to(new InstMethodsInterWithOverrideArgs(interceptor, 
                classLoader)));
    } else {
        // ...省略不需要重载参数的部分...
    }
}

增强实例方法过程中使用到的类,在增强静态方法中都有对应的类,如下表所示:

这些类的具体功能不再展开介绍了。

最后依然以 mysql-8.x-plugin 插件为例介绍一下它对实例方法的增强过程,其中  ConnectionInstrumentation.getInstanceMethodsInterceptPoints() 方法返回了 5 个 InstanceMethodsInterceptPoint 对象,这里只看其中的第一个对象:它负责拦截  ConnectionImpl 的 prepareStatement() 方法,并委托给 CreatePreparedStatementInterceptor(不修改方法参数),具体实现代码就不展示了。

在 CreatePreparedStatementInterceptor 中,before() 和 handleMethodException() 方法都是空实现,其 after() 方法实现如下:

public Object afterMethod(EnhancedInstance objInst, Method method, 
    Object[] allArguments, Class<?>[] argumentsTypes,
    Object ret) throws Throwable {
    if (ret instanceof EnhancedInstance) { // ConnectionImpl已被增强过
        // 更新_$EnhancedClassField_ws字段,StatementEnhanceInfos中不仅封
        // 装了原有的ConnectionInfo,还包含了具体执行的SQL语句和SQL参数等信息
        ((EnhancedInstance)ret).setSkyWalkingDynamicField(
          new StatementEnhanceInfos(
             (ConnectionInfo)objInst.getSkyWalkingDynamicField(),
                 (String)allArguments[0], "PreparedStatement"));
    }
    return ret;
}

InterceptorInstanceLoader

前面加载 Interceptpr 的 ClassLoader 并没有使用 AgentClassLoader 的默认实例或是Application ClassLoader,而是通过 InterceptorInstanceLoader 完成加载的。 在 InterceptorInstanceLoader 里面会维护一个 ClassLoader Cache,以及一个 Instance Cache,如下所示:

// 记录了 instanceKey与实例之间的映射关系,保证单例
static ConcurrentHashMap<String, Object> INSTANCE_CACHE = 
    new ConcurrentHashMap<String, Object>();

// 记录了 targetClassLoader以及其子 AgentClassLoader的对应关系
static Map<ClassLoader, ClassLoader> EXTEND_PLUGIN_CLASSLOADERS = 
     new HashMap<ClassLoader, ClassLoader>();

在通过 InterceptorInstanceLoader.load() 这个静态方法加载 Interceptor 类时的核心逻辑如下:

public static <T> load(String className, 
         ClassLoader targetClassLoader){
    if (targetClassLoader == null) {
          targetClassLoader = 
               InterceptorInstanceLoader.class.getClassLoader();
    }
    // 通过该 instanceKey保证该 Interceptor在一个 ClassLoader中只创建一次
    String instanceKey = className + "_OF_" + 
        targetClassLoader.getClass().getName() + "@" + 
           Integer.toHexString(targetClassLoader.hashCode());
    Object inst = INSTANCE_CACHE.get(instanceKey);
    if (inst == null) {
        // 查找targetClassLoader对应的子AgentClassLoader
        ClassLoader pluginLoader = 
                EXTEND_PLUGIN_CLASSLOADERS.get(targetClassLoader);
        if (pluginLoader == null) {
            // 为 targetClassLoader创建子AgentClassLoader
            pluginLoader = new AgentClassLoader(targetClassLoader);
            EXTEND_PLUGIN_CLASSLOADERS.put(targetClassLoader, 
              pluginLoader);
        }
        // 通过子AgentClassLoader加载Interceptor类
        inst = Class.forName(className, true, 
               pluginLoader).newInstance();
        if (inst != null) { // 记录Interceptor对象
            INSTANCE_CACHE.put(instanceKey, inst);
        }
    }
    return (T) inst;
}

以 demo-webapp 为例,其类加载器的结构如下图所示:

总结

本课时深入介绍了 Agent 插件增强目标类的实现,这是 Agent 最核心功能,其中深入分析了增强静态方法、构造方法、实例方法的原理,以及插件如何让目标实例对象实现 EnhanceInstance 接口,如何为目标实例对象添加新字段等。为了帮助你更好的理解,在分析的过程中还以 mysql-8.x-plugin 插件为例将上述核心逻辑串连起来。


Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐