Skywalking Agent原理剖析

Skywalking Agent原理剖析

  • Skywalking Agent原理剖析
    • 1 agent原理
      • 1.1 Java Agent
      • 1.2 定义自己的agent
      • 1.3 自定义方法耗时统计
    • 2 Byte Buddy
      • 2.1 Byte Buddy应用场景
      • 2.2 Byte Buddy学习
        • 2.2.1 ByteBuddy语法
        • 2.2.2 ByteBuddy创建代理
          • 2.2.3 ByteBuddy程序中的应用
    • 3 Skywalking源码导入
      • 31 源码环境搭建
      • 3.2 模块分析
    • 4 Skywalking Agent启动流程剖析
      • 4.1 Skywalking Agent架构
      • 4.2 Skywalking Agent启动流程
    • 5 Skywalking Agent源码剖析
      • 5.1 配置初始化
      • 5.2 插件加载
      • 5.3 解析插件
        • 5.3.1 PluginResourcesResolver
        • 5.3.2 PluginFinder
        • 5.3.3 AgentBuilder

Skywalking Agent原理剖析

1 agent原理

使用Skywalking的时候,并没有修改程序中任何一行 Java 代码,这里便使用到了 Java Agent 技术,我们接下来展开对Java Agent 技术的学习。

1.1 Java Agent

Java Agent 是从 JDK1.5 开始引入的,算是一个比较老的技术了。作为 Java 的开发工程师,我们常用的命令之一就是 java 命令,而 Java Agent 本身就是 java 命令的一个参数(即 -javaagent)。正如上一课时接入 SkyWalking Agent 那样,-javaagent 参数之后需要指定一个 jar 包,这个 jar 包需要同时满足下面两个条件:

  1. 在 META-INF 目录下的 MANIFEST.MF 文件中必须指定 premain-class 配置项。
  2. premain-class 配置项指定的类必须提供了 premain() 方法。在 Java 虚拟机启动时,执行 main() 函数之前,虚拟机会先找到 -javaagent 命令指定 jar 包,然后执行premain-class 中的 premain() 方法。用一句概括其功能的话就是:main() 函数之前的一个拦截器。

使用 Java Agent 的步骤大致如下:
3. 定义一个 MANIFEST.MF 文件,在其中添加 premain-class 配置项。
4. 创建 premain-class 配置项指定的类,并在其中实现 premain() 方法,方法签名如下:

public static void premain(String agentArgs, Instrumentation inst){ 
    //... 
}
  1. 将 MANIFEST.MF 文件和 premain-class 指定的类一起打包成一个 jar 包。
  2. 使用 -javaagent 指定该 jar 包的路径即可执行其中的 premain() 方法。

1.2 定义自己的agent

1)探针工程
创建工程 hailtaxi-agent 用来编写agent包,该类需要用 maven-assembly-plugin 打包,我们先引入该插件:


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>com.itheimagroupId>
    <artifactId>hailtaxi-agentartifactId>
    <version>1.0-SNAPSHOTversion>

    <properties>
        <maven.compiler.source>8maven.compiler.source>
        <maven.compiler.target>8maven.compiler.target>
    properties>

    <dependencies>
        <dependency>
            <groupId>net.bytebuddygroupId>
            <artifactId>byte-buddyartifactId>
            <version>1.9.2version>
        dependency>
        <dependency>
            <groupId>net.bytebuddygroupId>
            <artifactId>byte-buddy-agentartifactId>
            <version>1.9.2version>
        dependency>
    dependencies>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-pluginartifactId>
                <configuration>
                    <appendAssemblyId>falseappendAssemblyId>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependenciesdescriptorRef>
                    descriptorRefs>
                    <archive> 
                        <manifest>
                            
                            <addDefaultImplementationEntries>trueaddDefaultImplementationEntries>
                            <addDefaultSpecificationEntries>trueaddDefaultSpecificationEntries>
                        manifest>
                        
                        <manifestEntries>
                            <Premain-Class>com.itheima.LoginAgentPremain-Class>
                            
                        manifestEntries>
                    archive>
                configuration>
                <executions>
                    <execution>
                        <id>make-assemblyid>
                        <phase>packagephase>
                        <goals>
                            <goal>singlegoal>
                        goals>
                    execution>
                executions>
            plugin>
        plugins>
    build>
project>

在该工程中编写一个类 com.itheima.LoginAgent :

public class LoginAgent {

    /***
     * 执行方法拦截
     * @param agentArgs:-javaagent 命令携带的参数。在前面介绍 SkyWalking Agent 接入时提到
     *                 agent.service_name 这个配置项的默认值有三种覆盖方式,
     *                 其中,使用探针配置进行覆盖,探针配置的值就是通过该参数传入的。
     * @param inst:java.lang.instrumen.Instrumentation 是 Instrumention 包中定义的一个接口,它提供了操作类定义的相关方法。
     */
    public static void premain(String agentArgs, Instrumentation inst){
        System.out.println("参数:" + agentArgs);
    }
}

再把该工程打包,在 D:/project/skywalking/hailtaxi-agent/target/hailtaxi-agent-1.0-SNAPSHOT.jar 生成了该探针包。
此时我们把jar包解压, MANIFEST.MF 内容如下:

Manifest-Version: 1.0 
Archiver-Version: Plexus Archiver 
Created-By: Apache Maven 
Built-By: admin 
Build-Jdk: 1.8.0_91 
Specification-Title: hailtaxi-agent 
Specification-Version: 1.0-SNAPSHOT 
Implementation-Title: hailtaxi-agent 
Implementation-Version: 1.0-SNAPSHOT 
Implementation-Vendor-Id: com.itheima 
Premain-Class: com.itheima.LoginAgent 

2)普通工程
我们在创建一个普通工程 hailtaxi-user ,在该工程中创建一个普通类com.itheima.agent.UserInfo 并编写main方法:

public class UserInfo {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("张三是个中国人!");
        //调用say()方法
        say();
        TimeUnit.SECONDS.sleep(2);
    }

    /***
     * 测试时间
     * @throws InterruptedException
     */
    public static void say() throws InterruptedException {
        System.out.println("hello!");
        TimeUnit.SECONDS.sleep(5);
    }
}

我们再将如下参数配置到IDEA中:

-javaagent:D:/project/skywalking/hailtaxi-agent/target/hailtaxi-agent-1.0-SNAPSHOT.jar=hailtaxi-user

如果是多个参数,可以这么写 -javaagent:/xxx.jar=option1=value1,option2=value2 。

此时运行效果如下:
Skywalking Agent原理剖析_第1张图片

1.3 自定义方法耗时统计

Java Agent 能做的事情非常多,而刚才打印一句日志只是一个能功能展示。要想使用 java agent 做更多事,这里需要关注一下 premain() 方法中的第二个参数:Instrumentation 。Instrumentation 位于 java.lang.instrument 包中,通过这个工具包,我们可以编写一个强大的Java Agent 程序。
下面先来简单介绍一下 Instrumentation 中的核心 API 方法:
addTransformer()/removeTransformer() 方法:注册/注销一个 ClassFileTransformer 类的实例,该 Transformer 会在类加载的时候被调用,可用于修改类定义。redefineClasses() 方法:该方法针对的是已经加载的类,它会对传入的类进行重新定义。
**getAllLoadedClasses()**方法:返回当前 JVM 已加载的所有类。
getInitiatedClasses() 方法:返回当前 JVM 已经初始化的类。
**getObjectSize()**方法:获取参数指定的对象的大小。
我们要想实现更复杂的功能,需要先学习下 Byte Buddy ,我们接下来学习下byte buddy并且基于bytebuddy写出更多复杂应用。

1)Byte Buddy介绍
Byte Buddy 是一个开源 Java 库,其主要功能是帮助用户屏蔽字节码操作,以及复杂的
Instrumentation API 。Byte Buddy 提供了一套类型安全的 API 和注解,我们可以直接使用这些 API 和注解轻松实现复杂的字节码操作。另外,Byte Buddy 提供了针对 Java Agent 的额外 API,帮助开发人员在 Java Agent 场景轻松增强已有代码。
学习完上面方法后,我们基于java agent写一个统计方法耗时流程,此时我们需要将 Java Agent 与Byte Buddy 结合使用,统计 com.itheima.agent包下所有方法的耗时。

2)引入依赖
在 hailtaxi-agent 中引入byte buddy依赖:

   <dependency>
            <groupId>net.bytebuddygroupId>
            <artifactId>byte-buddyartifactId>
            <version>1.9.2version>
        dependency>
        <dependency>
            <groupId>net.bytebuddygroupId>
            <artifactId>byte-buddy-agentartifactId>
            <version>1.9.2version>
        dependency>

3)创建统计拦截器
创建 com.itheima.TimeInterceptor 实现统计拦截,代码如下:

import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;

import java.lang.reflect.Method;
import java.util.concurrent.Callable;

public class TimeInterceptor {

    /***
     * 拦截方法
     * @RuntimeType:返回类型绑定,让返回结果和被调用的原对象方法返回结果类型保持一致
     * @Origin:原方法参数类型绑定
     * @SuperCall:绑定被调用对象的代理对象
     * @param method:拦截的方法
     * @param callable:调用对象的代理对象
     * @return
     * @throws Exception
     */
    @RuntimeType
    public static Object intercept(@Origin Method method,
                                   @SuperCall Callable<?> callable) throws Exception {
        //时间统计开始
        long start = System.currentTimeMillis();
        // 执行原函数
        Object result = callable.call();
        //执行时间统计
        System.out.println(method.getName() + ":" + (System.currentTimeMillis() - start) + "ms");
        return result;
    }
}

这里整体实现类似动态代理执行过程,也类似SpringAop中的环绕通知,其中几个注解我们一起来学习一下:

@RuntimeType 注解:告诉 Byte Buddy 不要进行严格的参数类型检测,在参数匹配失败时,尝试使用类型转换方式(runtime type casting)进行类型转换,匹配相应方法。 
@Origin 注解:注入目标方法对应的 Method 对象。如果拦截的是字段的话,该注解应该标注到 Field 类型参数。 
@SuperCall:这个注解比较特殊,我们要在 intercept() 方法中调用目标方法的话,需要通过这种方式注入,与 Spring AOP 中的 ProceedingJoinPoint.proceed() 方法有点类似,需要注意的是, 这里不能修改调用参数,从上面的示例的调用也能看出来,参数不用单独传递,都包含在其中了。另外, 
@SuperCall 注解还可以修饰 Runnable 类型的参数,只不过目标方法的返回值就拿不到了。

4)agent拦截配置
创建Java Agent和Byte Buddy结合处理方法拦截配置流程,创建 com.itheima.AgentByteBuddy ,在该类中配置拦截的类和方法:

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.FixedValue;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;

import java.lang.instrument.Instrumentation;

public class AgentByteBuddy {

    /***
     * 执行方法拦截
     * @param agentArgs:-javaagent 命令携带的参数。在前面介绍 SkyWalking Agent 接入时提到
     *                 agent.service_name 这个配置项的默认值有三种覆盖方式,
     *                 其中,使用探针配置进行覆盖,探针配置的值就是通过该参数传入的。
     * @param inst:java.lang.instrumen.Instrumentation 是 Instrumention 包中定义的一个接口,它提供了操作类定义的相关方法。
     */
    public static void premain(String agentArgs, Instrumentation inst) throws IllegalAccessException, InstantiationException {
        //动态构建操作,根据transformer规则执行拦截操作
        AgentBuilder.Transformer transformer = new AgentBuilder.Transformer() {
            @Override
            public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
                                                    TypeDescription typeDescription,
                                                    ClassLoader classLoader,
                                                    JavaModule javaModule) {
                //构建拦截规则
                return builder
                        //method()指定哪些方法需要被拦截,ElementMatchers.any()表示拦截所有方法
                        .method(ElementMatchers.<MethodDescription>any())
                        //intercept()指定拦截上述方法的拦截器
                        .intercept(MethodDelegation.to(TimeInterceptor.class));
            }
        };

        //采用Byte Buddy的AgentBuilder结合Java Agent处理程序
        new AgentBuilder
                //采用ByteBuddy作为默认的Agent实例
                .Default()
                //拦截匹配方式:类以com.itheima开始(其实即使com.itheima包下的所有类)
                .type(ElementMatchers.nameStartsWith("com.itheima"))
                //拦截到的类由transformer处理
                .transform(transformer)
                //安装到 Instrumentation
                .installOn(inst);


        // 创建ByteBuddy对象
        String str = new ByteBuddy()
                // subclass增强方式
                .subclass(Object.class)
                // 新类型的类名
                .name("com.itheima.Type")
                // 拦截其中的toString()方法
                .method(ElementMatchers.named("toString"))
                // 让toString()方法返回固定值
                .intercept(FixedValue.value("Hello World!"))
                .make()
                // 加载新类型,默认WRAPPER策略
                .load(ByteBuddy.class.getClassLoader())
                .getLoaded()
                // 通过 Java反射创建 com.xxx.Type实例
                .newInstance()
                // 调用 toString()方法
                .toString();

        // 指定方法名称
        ElementMatchers.named("toString")
                // 指定方法的返回值
                .and(ElementMatchers.returns(String.class))
                // 指定方法参数
                .and(ElementMatchers.takesArguments(0));
    }
}

同时将pom.xml中的premain-class替换成 AgentByteBuddy

 <manifestEntries>
                            <Premain-Class>com.itheima.AgentByteBuddyPremain-Class>
                        manifestEntries>

修改 hailtaxi-user 中的 UserInfo 添加测试方法:

public class UserInfo {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("张三是个中国人!");
        //调用say()方法
        say();
        TimeUnit.SECONDS.sleep(2);
    }

    /***
     * 测试时间
     * @throws InterruptedException
     */
    public static void say() throws InterruptedException {
        System.out.println("hello!");
        TimeUnit.SECONDS.sleep(5);
    }
}

测试效果如下:

张三是个中国人! 
hello! 
say:5000ms 
main:7002ms

2 Byte Buddy

在前面学习 Java Agent 技术时,结合 Byte Buddy 技术实现了统计方法执行时间的功能。 Byte Buddy在Skywalking中被广泛使用,我们接下来继续学习Byte Buddy,为后续分析 SkyWalking Agent打下基础。

2.1 Byte Buddy应用场景

Java 是一种强类型的编程语言,即要求所有变量和对象都有一个确定的类型,如果在赋值操作中出现类型不兼容的情况,就会抛出异常。强类型检查在大多数情况下是可行的,然而在某些特殊场景下,强类型检查则成了巨大的障碍。
我们在做一些通用工具封装的时候,类型检查就成了很大障碍。比如我们编写一个通用的Dao实现数据操作,我们根本不知道用户要调用的方法会传几个参数、每个参数是什么类型、需求变更又会出现什么类型,几乎没法在方法中引用用户方法中定义的任何类型。我们绝大多数通用工具封装都采用了反射机制,通过反射可以知道用户调用的方法或字段,但是Java反射有很多缺陷:

1:反射性能很差 
2:反射能绕开类型安全检查,不安全,比如权限暴力破解

学完agent后,我们可以基于agent做出一些改变,运行时代码生成在 Java 应用启动之后再动态生成一些类定义,这样就可以模拟一些只有使用动态编程语言编程才有的特性,同时也不丢失 Java 的强类型检查。在运行时生成代码需要特别注意的是 Java 类型被 JVM 加载之后,一般不会被垃圾被回收,因此不应该过度使用代码生成。
java编程语言代码生成库不止 Byte Buddy 一个,以下代码生成库在 Java 中也很流行:

Java Proxy
Java Proxy 是 JDK 自带的一个代理工具,它允许为实现了一系列接口的类生成代理类。Java Proxy 要求目标类必须实现接口是一个非常大限制,例如,在某些场景中,目标类没有实现任何接口且无法修改目标类的代码实现,Java Proxy 就无法对其进行扩展和增强了。
CGLIB
CGLIB 诞生于 Java 初期,但不幸的是没有跟上 Java 平台的发展。虽然 CGLIB 本身是一个相当强大的库,但也变得越来越复杂。鉴于此,导致许多用户放弃了 CGLIB 。
Javassist
Javassist 的使用对 Java 开发者来说是非常友好的,它使用Java 源代码字符串和Javassist 提供的一些简单 API ,共同拼凑出用户想要的 Java 类,Javassist 自带一个编译器,拼凑好的 Java 类在程序运行时会被编译成为字节码并加载到 JVM 中。Javassist 库简单易用,而且使用 Java 语法构建类与平时写 Java 代码类似,但是 Javassist 编译器在性能上比不了 Javac 编译器,而且在动态组合字符串以实现比较复杂的逻辑时容易出错。
Byte Buddy
Byte Buddy 提供了一种非常灵活且强大的领域特定语言,通过编写简单的 Java 代码即可创建自定义的运行时类。与此同时,Byte Buddy 还具有非常开放的定制性,能够应付不同复杂度的需求。

上面所有代码生成技术中,我们推荐使用Byte Buddy,因为Byte Buddy代码生成可的性能最高,ByteBuddy 的主要侧重点在于生成更快速的代码,如下图:
Skywalking Agent原理剖析_第2张图片

2.2 Byte Buddy学习

我们接下来详细讲解一下Byte Buddy Api,对重要的方法和类进行深度剖析。

2.2.1 ByteBuddy语法

任何一个由 Byte Buddy 创建/增强的类型都是通过 ByteBuddy 类的实例来完成的,我们先来学习一下ByteBuddy类,如下代码:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy() 

// 生成 Object的子类 

.subclass(Object.class) 

// 生成类的名称为"com.itheima.Type" 

.name("com.itheima.Type") 

.make(); 

Byte Buddy 动态增强代码总共有三种方式:

subclass:对应 ByteBuddy.subclass() 方法。这种方式比较好理解,就是为目标类(即被增强的类)生成一个子类,在子类方法中插入动态代码。 
rebasing:对应 ByteBuddy.rebasing() 方法。当使用 rebasing 方式增强一个类时,Byte Buddy 保存目标类中所有方法的实现,也就是说,当 Byte Buddy 遇到冲突的字段或方法时,会将原来的字段或方法实现复制到具有兼容签名的重新命名的私有方法中,而不会抛弃这些字段和方法实现。从而达到不丢失实现的目的。这些重命名的方法可以继续通过重命名后的名称进行调用。
redefinition:对应 ByteBuddy.redefine() 方法。当重定义一个类时,Byte Buddy 可以对一个已有的类添加属性和方法,或者删除已经存在的方法实现。如果使用其他的方法实现替换已经存在的方法实现,则原来存在的方法实现就会消失。 

通过上述三种方式完成类的增强之后,我们得到的是 DynamicType.Unloaded 对象,表示的是一个未加载的类型,我们可以使用 ClassLoadingStrategy 加载此类型。ByteBuddy 提供了几种类加载策略,
这些策略定义在 ClassLoadingStrategy.Default 中,其中:
WRAPPER 策略:创建一个新的 ClassLoader 来加载动态生成的类型。
CHILD_FIRST 策略:创建一个子类优先加载的 ClassLoader,即打破了双亲委派模型。
INJECTION 策略:使用反射将动态生成的类型直接注入到当前 ClassLoader 中。
实现如下:

Class<?> dynamicClazz = new ByteBuddy() 

// 生成 Object的子类 

.subclass(Object.class) 

// 生成类的名称为"com.itheima.Type" 

.name("com.itheima.Type") 

.make() 

.load(Demo.class.getClassLoader(), 

//使用WRAPPER 策略加载生成的动态类型 

ClassLoadingStrategy.Default.WRAPPER) 

.getLoaded(); 

前面动态生成的 com.itheima.Type 类型只是简单的继承了 Object 类,在实际应用中动态生成新类型的一般目的就是为了增强原始的方法,下面通过一个示例展示 Byte Buddy 如何增强 toString() 方法:

// 创建ByteBuddy对象 

String str = new ByteBuddy() 

// subclass增强方式 

.subclass(Object.class) 

// 新类型的类名 

.name("com.itheima.Type") 

// 拦截其中的toString()方法

.method(ElementMatchers.named("toString")) 

// 让toString()方法返回固定值 

.intercept(FixedValue.value("Hello World!")) 

.make() 

// 加载新类型,默认WRAPPER策略 

.load(ByteBuddy.class.getClassLoader()) 

.getLoaded() 

// 通过 Java反射创建 com.xxx.Type实例 

.newInstance() 

// 调用 toString()方法 

.toString(); 

首先需要关注这里的 method() 方法,method() 方法可以通过传入的 ElementMatchers 参数匹配多个需要修改的方法,这里的 ElementMatchers.named(“toString”) 即为按照方法名匹配 toString() 方法。如果同时存在多个重载方法,则可以使用 ElementMatchers 其他 API 描述方法的签名,如下所示:

// 指定方法名称 

ElementMatchers.named("toString") 

// 指定方法的返回值 

.and(ElementMatchers.returns(String.class)) 

// 指定方法参数 

.and(ElementMatchers.takesArguments(0)); 

接下来需要关注的是 intercept() 方法,通过 method() 方法拦截到的所有方法会由Intercept()方法指定的 Implementation 对象决定如何增强。这里的 FixValue.value() 会将方法的实现修改为固定值,上例中就是固定返回 “Hello World!” 字符串。
Byte Buddy 中可以设置多个 method() 和 Intercept() 方法进行拦截和修改, Byte Buddy 会按照栈的顺序来进行拦截。

2.2.2 ByteBuddy创建代理

我们先创建一个普通类,再为该类创建代理类,创建代理对方法进行拦截做处理。
1)普通类
创建 com.itheima.service.UserService

public class UserService { 

//方法1 

public String username(){ 

return "张三"; 

}

//方法2 

public String address(String username){ 

return username+"来自 【湖北省武汉市】"; 

}

//方法3 

public String address(String username,String city){ 

return username+"来自 【湖北省"+city+"】"; 

} 

}

2)代理创建

public static void main(String[]args)throws Exception{

//创建ByteBuddy 

        UserService userService=new ByteBuddy()

//指定创建UserServiceImpl对象的子类 

        .subclass(UserService.class)

//匹配方法,所有方法均被拦截 

        .method(ElementMatchers.isDeclaredBy(UserService.class))

//任何拦截都返回一个固定值 

        .intercept(FixedValue.value("被拦截了吧!"))

//为特定方法添加拦截 

        .method(ElementMatchers.named("address").and(ElementMatchers.takesArguments(1)))

//返回固定值 

        .intercept(FixedValue.value("被拦截了吧!address()方法被特别关照了!"))

//创建动态对象 

        .make()

        .load(ByteBuddy.class.getClassLoader(),

        ClassLoadingStrategy.Default.INJECTION)

        .getLoaded()

        .newInstance();

//会被拦截,返回固定值:被拦截了吧! 

        System.out.println(userService.username());

        System.out.println(userService.address("王五","武汉"));

//会被拦截,返回固定值:被拦截了吧!address()方法被特别关照了! 

        System.out.println(userService.address("张三"));

        }

此时我们运行,结果如下:

被拦截了吧! 
被拦截了吧! 
被拦截了吧!
address()方法被特别关照了!

在这里插入图片描述

2.2.3 ByteBuddy程序中的应用

上面我们创建代理的案例中,把返回值设置成了固定值,但在真实程序汇总通常是要做特定业务流程处理,比如事务、日志、权限校验等,此时我们需要用到ByteBuddy的MethodDelegation对象,它可以将拦截的目标方法委托给其他对象处理,这里有几个注解我们先进行说明:
@RuntimeType:不进行严格的参数类型检测,在参数匹配失败时,尝试使用类型转换方式(runtime type casting)进行类型转换,匹配相应方法。
@This:注入被拦截的目标对象。
@AllArguments:注入目标方法的全部参数。
@Origin:注入目标方法对应的 Method 对象。如果拦截的是字段的话,该注解应该标注到 Field类型参数。
@Super:注入目标对象。通过该对象可以调用目标对象的所有方法。
@SuperCall:这个注解比较特殊,我们要在 intercept() 方法中调用目标方法的话,需要通过这种方式注入,与 Spring AOP 中的 ProceedingJoinPoint.proceed() 方法有点类似,需要注意的是,这里不能修改调用参数,从上面的示例的调用也能看出来,参数不用单独传递,都包含在其中了。
另外,@SuperCall 注解还可以修饰 Runnable 类型的参数,只不过目标方法的返回值就拿不到了。

1)修改案例方法
我们修改一下之前写的案例类 com.itheima.service.UserService 添加日志打印:

public class UserService {

    //方法1
    public String username(){
        System.out.println("com.itheima.service.UserService.username.....");
        return "张三";
    }

    //方法2
    public String address(String username){
        System.out.println("com.itheima.service.UserService.address(String username).....");
        return username+"来自 【湖北省武汉市】";
    }

    //方法3
    public String address(String username,String city){
        System.out.println("com.itheima.service.UserService.address(String username,String city).....");
        return username+"来自 【湖北省"+city+"】";
    }


}

2)创建代理
编写动态创建对象代理的方法:

public class ByteBuddyLogAspect {

    public static void main(String[] args) throws Exception {
        //创建ByteBuddy
        UserService userService = new ByteBuddy()
                //指定创建UserServiceImpl对象的子类
                .subclass(UserService.class)
                //匹配方法,所有方法均被拦截
                .method(ElementMatchers.isDeclaredBy(UserService.class))
                //任何拦截都返回一个固定值
                //.intercept(MethodDelegation.to(new AspectLog()))
                //修改参数
                .intercept(MethodDelegation.withDefaultConfiguration().withBinders(
                        Morph.Binder.install(String.class)
                ).to(new AspectLog()))
                //创建动态对象
                .make()
                .load(ByteBuddy.class.getClassLoader(),
                        ClassLoadingStrategy.Default.INJECTION)
                .getLoaded()
                .newInstance();

        userService.username();
        userService.address("王五","武汉");
        userService.address("张三");
    }
}

此时测试运行结果如下:

准备执行Method=username 
com.itheima.service.UserService.username..... 
方法执行完成Method=username 
准备执行Method=address 
com.itheima.service.UserService.address(String username,String city)..... 
方法执行完成Method=address 
准备执行Method=address 
com.itheima.service.UserService.address(String username)..... 
方法执行完成Method=address

3 Skywalking源码导入

我们已经学习了Skywalking的应用,接下来我们将剖析Skywalking源码,深度学习Skywalking agent。

31 源码环境搭建

当前最新版本是8.3.0,我们首先找到8.3.0的版本,然后下载并导入到IDEA,下载地址https://github.com/apache/skywalking/tags,我们直接用git克隆到本地。

1)下载工程

Skywalking Agent原理剖析_第3张图片
这个过程比较耗时间,需要大家耐心等待,如果想提升下载速度,可以把github仓库地址导入到码云中,再下载,速度将会变得非常快。

2)切换版本
将Skywalking工程加入到Maven工程中,我们用的是当前最新版本8.3.0,因此需要切换版本:
Skywalking Agent原理剖析_第4张图片
项目导入IDEA后,会从指定路径加载项目,我们需要在skywalking的pom.xml中配置项目路径,添加
如下properties配置即可:
D:/project/skywalking/skywalking- 8.3.0/
将 ${COVERALLS_REPO_TOKEN} 注释掉,我们只用于本地测试,不需要提交相关报告。
Skywalking Agent原理剖析_第5张图片
我们接下来生成一些需要用到的类,需要在工程中执行如下命令:

git submodule init 
git submodule update

此时会生成一些类 skywalking\apm-protocol\apm-network\target\generated- sources\protobuf\java\org\apache\skywalking\apm\network\common\v3 目录下的类如下图:
在这里插入图片描述
Skywalking Agent原理剖析_第6张图片
除了上面这里,还有很多个地方都需要这么操作,我们执行 OAPServerStartUp 的main方法启动Skywalking,只要执行找不到类,就找下有没有任何编译后生成的类没在类路径下,都把他们设置为类路径即可。
Skywalking依赖的插件特别多,因此依赖的包也特别多,我们把Skywalking安装到本地,会耗费很长时间,但不要担心,因为迟早会安装完成,如下图:

3.2 模块分析

apm-application-toolkit:常用的工具工程,例如:log4j、log4j2、logback 等常见日志框架的接入接口,Kafka轮询调用注解,apm-application-toolkit 模块类似于暴露 API 定义,对应的处理逻辑在apm-sniffer/apm-toolkit-activation 模块中实现,如下图:
Skywalking Agent原理剖析_第7张图片
apm-commons:SkyWalking 的公共组件和工具类。如下图所示,其中包含两个子模块,apmdatacarrier 模块提供了一个生产者-消费者模式的缓存组(DataCarrier),无论是在 Agent 端还是OAP 端都依赖该组件。apm-util 模块则提供了一些常用的工具类,例如,字符串处理工具类(StringUtil)、占位符处理的工具类(PropertyPlaceholderHelper、PlaceholderConfifigurerSupport)等等。
apache-skywalking-apm:SkyWalking 打包后使用的命令文件都在此目录中,例如,前文启动 OAP和 SkyWalking Rocketbot 使用的 startup.sh 文件。
apm-protocol:该模块中只有一个 apm-network 模块,我们需要关注的是其中定义的 .proto 文件,定义 Agent 与后端 OAP 使用 gRPC 交互时的协议。
apm-sniffffer:agent核心功能以及agent依赖插件,模块比较多:

apm-agent:只有一个类SkyWalkingAgent,是Skywalking的agent入口。 
apm-agent-core:看名字我们就知道它是Skywalking agent核心模块。 
apm-sdk-plugin:该模块下包含了 SkyWalking Agent 的全部插件。 
apm-toolkit-activation:apm-application-toolkit 模块的具体实现。 
apm-test-tools:Skywalking的测试功能。 
bootstrap-plugins:该插件主要提供了Http和多线程相关的功能支持,它里面有2个子工程。 
optional-plugins:可选插件,例如对spring支持、对kotlin支持等,它下面有多个插件工程实现。 
optional-reporter-plugins:该工程插件主要提供一些数据报告,集成了Kafka功能。

apm-webapp:SkyWalking Rocketbot 对应的后端。
oap-server:opa主程序,该工程中有多个模块,我们对核心模块进行说明:

analyzer:数据分析工程,例如对内存分析、仪表盘分析报告等,它下面有2个子工程。 

exporter:导出数据功能。oal-grammar:操作适配语法,例如SQL语法。 

oal-rt:操作解析器,上面提供了语法,该工程提供对操作解析功能。 

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

server-cluster-plugin: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 的前端。

4 Skywalking Agent启动流程剖析

我们已经学习了Skywalking常用操作,并且讲解了Java Agent,而且Skywalking Agent就是基于JavaAgent研发而来,我们接下来深入学习Skywalking Agent架构、原理、常用组件。

4.1 Skywalking Agent架构

我们在学习Skywalking之前,先了解一下微内核架构,如下图:
Skywalking Agent原理剖析_第8张图片
微内核架构(Microkernel Architecture),也被成为插件化架构(Plug-in Architecture),是一种面向功能进行拆分的可扩展性架构,通常用于实现基于产品(原文为product-based,指存在多个版本,需要下载安装才能使用,与web-based想对应)的应用。

微内核架构的好处:

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

微内核的核心系统设计的关键技术有:插件管理,插件连接和插件通信。
SkyWalking Agent 采用了微内核架构(Microkernel Architecture),是一种面向功能进行拆分的可扩展性架构。
apm-agent-core:是Skywalking Agent的核心模块
apm-sdk-plugin:是Skywalking需要的各个插件模块
Skywalking Agent原理剖析_第9张图片

4.2 Skywalking Agent启动流程

1)启动OAP
我们接下来启动Skywalking oap,我们在 oap-server\server-starter 或者 oap-server\server- starter-es7 中找到 OAPServerStartUp 类,执行该类的main方法即可启动,但默认用的是H2存储,如果希望用elasticsearch存储,需要修改被调用的服务 server-bootstrap 的配置文件application.yml 配置elasticsearch位置:

storage:
 # selector: ${SW_STORAGE:h2}
  selector: ${SW_STORAGE:elasticsearch7}
  elasticsearch:
    nameSpace: ${SW_NAMESPACE:""}
    clusterNodes: ${SW_STORAGE_ES_CLUSTER_NODES:localhost:9200}
    protocol: ${SW_STORAGE_ES_HTTP_PROTOCOL:"http"}
    user: ${SW_ES_USER:""}
    password: ${SW_ES_PASSWORD:""}
    trustStorePath: ${SW_STORAGE_ES_SSL_JKS_PATH:""}
    trustStorePass: ${SW_STORAGE_ES_SSL_JKS_PASS:""}

执行 OAPServerStartUp 的main方法不报错就没问题。

2)启动SkyWalking Rocketbot
apm-webapp 是 Spring Boot 的 Web项目,执行 ApplicationStartUp 中的 main() 方法。正常启动之后,访问 localhost:8080,看到 SkyWalking Rocketbot 的 UI 界面即为启动成功。
如果修改启动端口,可以直接修改application.yml即可。

3)直接使用源码中的Agent
项目打包会生成 skywalking-agent.jar ,如下图:
Skywalking Agent原理剖析_第10张图片
我们来使用一下前面源码工程中打包生成的 skywalking-agent.jar
Skywalking Agent原理剖析_第11张图片

public class SkyWalkingAgent {
    private static ILog LOGGER = LogManager.getLogger(SkyWalkingAgent.class);

    /**
     * Main entrance. Use byte-buddy transform to enhance all classes, which define in plugins.
     */
    public static void premain(String agentArgs, Instrumentation instrumentation) throws PluginException {
        final PluginFinder pluginFinder;
        try {
        //初始化加载 agent.config 配置文件,其中会检测 Java Agent 参数以及环境变量是否覆 盖了相应配置项
                    SnifferConfigInitializer.initializeCoreConfig(agentArgs);
        } catch (Exception e) {
            // try to resolve a new logger, and use the new logger to write the error log here
            LogManager.getLogger(SkyWalkingAgent.class)
                    .error(e, "SkyWalking agent initialized failure. Shutting down.");
            return;
        } finally {
            // refresh logger again after initialization finishes
            LOGGER = LogManager.getLogger(SkyWalkingAgent.class);
        }

        try {
        //管理插件            
        pluginFinder = new PluginFinder(new PluginBootstrap().loadPlugins());
        } catch (AgentPackageNotFoundException ape) {
            LOGGER.error(ape, "Locate agent.jar failure. Shutting down.");
            return;
        } catch (Exception e) {
            LOGGER.error(e, "SkyWalking agent initialized failure. Shutting down.");
            return;
        }
        //使用ByteBuddy创建AgentBuilder 
         final ByteBuddy byteBuddy = new ByteBuddy().with(TypeValidation.of(Config.Agent.IS_OPEN_DEBUGGING_CLASS));
        //忽略拦截配置
        AgentBuilder agentBuilder = new AgentBuilder.Default(byteBuddy).ignore(
                nameStartsWith("net.bytebuddy.")
                        .or(nameStartsWith("org.slf4j."))
                        .or(nameStartsWith("org.groovy."))
                        .or(nameContains("javassist"))
                        .or(nameContains(".asm."))
                        .or(nameContains(".reflectasm."))
                        .or(nameStartsWith("sun.reflect"))
                        .or(allSkyWalkingAgentExcludeToolkit())
                        .or(ElementMatchers.isSynthetic()));

        JDK9ModuleExporter.EdgeClasses edgeClasses = new JDK9ModuleExporter.EdgeClasses();
        try {
            agentBuilder = BootstrapInstrumentBoost.inject(pluginFinder, instrumentation, agentBuilder, edgeClasses);
        } catch (Exception e) {
            LOGGER.error(e, "SkyWalking agent inject bootstrap instrumentation failure. Shutting down.");
            return;
        }

        try {
            agentBuilder = JDK9ModuleExporter.openReadEdge(instrumentation, agentBuilder, edgeClasses);
        } catch (Exception e) {
            LOGGER.error(e, "SkyWalking agent open read edge in JDK 9+ failure. Shutting down.");
            return;
        }

        if (Config.Agent.IS_CACHE_ENHANCED_CLASS) {
            try {
                agentBuilder = agentBuilder.with(new CacheableTransformerDecorator(Config.Agent.CLASS_CACHE_MODE));
                LOGGER.info("SkyWalking agent class cache [{}] activated.", Config.Agent.CLASS_CACHE_MODE);
            } catch (Exception e) {
                LOGGER.error(e, "SkyWalking agent can't active class cache.");
            }
        }
       //Java Agent创建代理流程
        agentBuilder.type(pluginFinder.buildMatch())
                    .transform(new Transformer(pluginFinder))
                    .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
                    .with(new Listener())
                    .installOn(instrumentation);

        try {
        //使用 JDK SPI加载的方式并启动 BootService 服务。
            ServiceManager.INSTANCE.boot();
        } catch (Exception e) {
            LOGGER.error(e, "Skywalking agent boot failure.");
        }
        //添加一个JVM钩子
        Runtime.getRuntime()
                .addShutdownHook(new Thread(ServiceManager.INSTANCE::shutdown, "skywalking service shutdown thread"));
    }

我们总结一下Skywalking 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 服务。 

5 Skywalking Agent源码剖析

前面我们对Skywalking Agent启动流程源码进行了剖析,接下来我们对启动流程中每个步骤源码进行剖析。

5.1 配置初始化

-javaagent:D:/project/skywalking/skywalking/apm-sniffer/apm-agent/target/skywalking-agent.jar 
-Dskywalking_config=D:/project/skywalking/hailtaxi-parent/hailtaxi-driver/src/main/resources/agent.config 
-Dskywalking.collector.backend_service=127.0.0.1:11800

启动driver服务的时候,会指定skywalking-agent.jar路径,同时会指定 agent.config 配置文件路径,如上配置,此时需要初始化加载该文件,加载流程可以从启动类 SkyWalkingAgent.premain() 方法找答案。
Skywalking Agent原理剖析_第12张图片
加载解析文件的时候,permain()方法会调用initializeCoreConfig(String agentOptions)方法,并解析agent.config文件,并将文件内容存入到Properties中,此时加载是按照${配置项名称:默认值}的格式解析各个配置,如下图:
Skywalking Agent原理剖析_第13张图片
loadConfig() 方法会优先根据环境变量(skywalking_config)指定的 agent.config 文件路径加载。若环境变量未指定 skywalking_ config 配置,则到 skywalking-agent.jar 同级的 config 目录下查找agent.confg 配置文件。
Skywalking Agent原理剖析_第14张图片
解析前后的数据也是不一致的,如下图:
在这里插入图片描述
overrideConfigBySystemProp() 方法中会遍历环境变量(即 System.getProperties() 集合),如果环境变 是以 “skywalking.” 开头的,则认为是 SkyWalking 的配置,同样会填充到 Config 类中,以覆盖agent.config 中的默认值。如下图:
Skywalking Agent原理剖析_第15张图片
ConfigInitializer 工具类,将配置信息填充到 Config 中的静态字段中,SkyWalking Agent 启动所需的全部配置都已经填充到 Config 中,后续使用配置信息时直接访问 Config 中的相应静态字段即可。
Skywalking Agent原理剖析_第16张图片
Config结构:
Skywalking Agent原理剖析_第17张图片

Config中Agent类的 SERVICE_NAME 对应agent.config中的agent.service_name= x x x C o n f i g 中 C o l l e c t o r 类 的 B A C K E N D S E R V I C E 对 应 a g e n t . c o n f i g 中 的 a g e n t . b a c k e n d s e r v i c e = {xxx} Config中Collector类的 BACKEND_SERVICE 对应agent.config中的agent.backend_service= xxxConfigCollectorBACKENDSERVICEagent.configagent.backendservice={xxx}

5.2 插件加载

加载插件执行流程:

1:new PluginBootstrap() 

2:PluginBootstrap().loadPlugins() 

3:AgentClassLoader.initDefaultLoader(); 没有指定类加载器的时候使用 PluginBootstrap.ClassLoader 

4:创建PluginResourcesResolver插件加载解析器 

5:将解析的插件存到List pluginClassList,此时只存储了插件的名字和类路径 

6:创建插件实例 

7:将所有插件添加到Skywalking内核中 

插件加载流程如下:
在 SkyWalkingAgent.premain() 方法中会执行插件加载,如下代码:

pluginFinder = new PluginFinder(new PluginBootstrap().loadPlugins());

加载插件的全部详细代码如下

public class PluginBootstrap {
    private static final ILog LOGGER = LogManager.getLogger(PluginBootstrap.class);

    /**
     * load all plugins.
     *
     * @return plugin definition list.
     */
    public List<AbstractClassEnhancePluginDefine> loadPlugins() throws AgentPackageNotFoundException {
        //初始化AgentClassLoader
        AgentClassLoader.initDefaultLoader();
        //创建PluginResourcesResolver插件加载解析器
        PluginResourcesResolver resolver = new PluginResourcesResolver();
        
        List<URL> resources = resolver.getResources();

        if (resources == null || resources.size() == 0) {
            LOGGER.info("no plugin files (skywalking-plugin.def) found, continue to start application.");
            return new ArrayList<AbstractClassEnhancePluginDefine>();
        }
        //循环加载插件路径
        for (URL pluginUrl : resources) {
            try {
            //插件会存到List pluginClassList,PluginDefine中只有 插件名字和插件类路径
                PluginCfg.INSTANCE.load(pluginUrl.openStream());
            } catch (Throwable t) {
                LOGGER.error(t, "plugin file [{}] init failure.", pluginUrl);
            }
        }
         //获取解析的插件集合
         List<PluginDefine> pluginClassList = PluginCfg.INSTANCE.getPluginClassList();

        List<AbstractClassEnhancePluginDefine> plugins = new ArrayList<AbstractClassEnhancePluginDefine>();
        //循环所有插件
        for (PluginDefine pluginDefine : pluginClassList) {
            try {
                LOGGER.debug("loading plugin class {}.", pluginDefine.getDefineClass());
                //创建插件实例(加载插件)
                AbstractClassEnhancePluginDefine plugin = (AbstractClassEnhancePluginDefine) Class.forName(pluginDefine.getDefineClass(), true, AgentClassLoader
                    .getDefault()).newInstance();
                plugins.add(plugin);
            } catch (Throwable t) {
                LOGGER.error(t, "load plugin [{}] failure.", pluginDefine.getDefineClass());
            }
        }

        //将插件添加到内核中
        plugins.addAll(DynamicPluginLoader.INSTANCE.load(AgentClassLoader.getDefault()));

        return plugins;

    }

SkyWalking Agent 加载插件时使用到一个自定义的 ClassLoader ——AgentClassLoader,之所以自定义类加载器,目的是不在应用的 Classpath 中引入 SkyWalking 的插件 jar 包,这样就可以让应用无依赖、无感知的插件。
AgentClassLoader 作为一个类加载器,主要工作还是从其 Classpath 下加载类(或资源文件),对应的就是其 findClass() 方法和 findResource() 方法:
我们来看一下findClass,主要根据类名获取它的Class:

  @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    //扫描classpath所有的jar包
        List<Jar> allJars = getAllJars();
        //把包替换成路径,最后加上.class
        String path = name.replace('.', '/').concat(".class");
        //循环查找所有的jar包
        for (Jar jar : allJars) {
        //加载jar包的信息
            JarEntry entry = jar.jarFile.getJarEntry(path);
            if (entry == null) {
                continue;
            }
            try {
            //定位当前jar包位置
                URL classFileUrl = new URL("jar:file:" + jar.sourceFile.getAbsolutePath() + "!/" + path);
                //加载jar包
                byte[] data;
                try (final BufferedInputStream is = new BufferedInputStream(
                    classFileUrl.openStream()); final ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                    int ch;
                    while ((ch = is.read()) != -1) {
                        baos.write(ch);
                    }
                    data = baos.toByteArray();
                }
                return processLoadedClass(defineClass(name, data, 0, data.length));
            } catch (IOException e) {
                LOGGER.error(e, "find class fail.");
            }
        }
        throw new ClassNotFoundException("Can't find " + name);
    }

findResource()方法主要获取文件路径,换句话理解,就是获取插件路径,我们来看下方法:

 @Override
    protected URL findResource(String name) {
    //扫描classpath所有的jar包
        List<Jar> allJars = getAllJars();
        //循环查找所有的jar包
        for (Jar jar : allJars) {
        //加载jar包的信息
            JarEntry entry = jar.jarFile.getJarEntry(name);
            if (entry != null) {
                try {
                //获取jar包的路径
                    return new URL("jar:file:" + jar.sourceFile.getAbsolutePath() + "!/" + name);
                } catch (MalformedURLException ignored) {
                }
            }
        }
        return null;
    }

5.3 解析插件

我们在学习插件解析之前,先看看插件是如何定义的。我们可以打开 apm-sniffer/apm-sdk-plugin ,它里面都是要用到的插件集合:
Skywalking Agent原理剖析_第18张图片
我们看看 mysql-5.x-plugin ,在resources(也就是classpath)中定义skywalking-plugin.def文件,在该文件中定义加载插件需要解析的类,而插件类以key=value形式定义,如下图:
Skywalking Agent原理剖析_第19张图片

5.3.1 PluginResourcesResolver

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

拿到全部插件的 skywalking-plugin.def 文件之后,PluginCfg 会逐行进行解析,转换成
PluginDefine 对象。PluginDefine 中有两个字段,分别对应 skywalking-plugin.def 中的key和value,解析流程如下:
Skywalking Agent原理剖析_第20张图片
接下来会遍历全部 PluginDefine 对象,通过反射将其中 defineClass 字段中记录的插件类实例化,核心逻辑如下:
Skywalking Agent原理剖析_第21张图片
AbstractClassEnhancePluginDefine 抽象类是所有 Agent 插件类的顶级父类,其中定义了四个核心方法,决定了一个插件类应该增强哪些目标类、应该如何增强、具体插入哪些逻辑,如下所示:
Skywalking Agent原理剖析_第22张图片
enhanceClass() 方法:返回的 ClassMatch,用于匹配当前插件要增强的目标类。
defifine() 方法:插件类增强逻辑的入口,底层会调用下面的 enhance() 方法和 witnessClass() 方法。
enhance() 方法:真正执行增强逻辑的地方。
witnessClass() 方法:一个开源组件可能有多个版本,插件会通过该方法识别组件的不同版本,防止对不兼容的版本进行增强。
ClassMatch
enhanceClass() 方法决定了一个插件类要增强的目标类,返回值为 ClassMatch 类型对象。
ClassMatch 类似于一个过滤器,可以通过多种方式匹配到目标类,ClassMatch 接口的实现如下:
Skywalking Agent原理剖析_第23张图片
NameMatch:根据其 className 字段(String 类型)匹配目标类的名称。

IndirectMatch:子接口中定义了两个方法。

public interface IndirectMatch extends ClassMatch { 

//Junction是Byte Buddy中的类,可以通过and、or等操作串联多个ElementMatcher,进行匹配

ElementMatcher.Junction buildJunction(); 

//用于检测传入的类型是否匹配该Match 

boolean isMatch(TypeDescription typeDescription); 

} 

MultiClassNameMatch:其中会指定一个 matchClassNames 集合,该集合内的类即为目标类。

ClassAnnotationMatch:根据标注在类上的注解匹配目标类。

MethodAnnotationMatch:根据标注在方法上的注解匹配目标类。

HierarchyMatch:根据父类或是接口匹配目标类。

我们来分析一下ClassAnnotationMatch的buildJunction()方法和isMatch()方法:

@Override
    public ElementMatcher.Junction buildJunction() {
        ElementMatcher.Junction junction = null;
        //annotations:指定了该 ClassAnnotationMatch 对象需要检查的注解 
        //遍历该对象需要检查的所有注解
        for (final IndirectMatch indirectMatch : indirectMatches) {
            if (junction == null) {
            //检测类是否标注了指定注解
                junction = indirectMatch.buildJunction();
            } else {
            //使用 and 方式将所有Junction对象连接起来
                junction = junction.or(indirectMatch.buildJunction());
            }
        }
       // 排除接口
        return junction;
    }

isMatch()方法如下:

@Override 

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()); 

}

// 如果全部删除,则匹配成功 

return annotationList.isEmpty(); 

}
5.3.2 PluginFinder

PluginFinder 是 AbstractClassEnhancePluginDefine 查找器,可以根据给定的类查找用于增强的AbstractClassEnhancePluginDefine 集合。
在 PluginFinder 的构造函数中会遍历前面课程已经实例化的AbstractClassEnhancePluginDefine ,并根据 enhanceClass() 方法返回的 ClassMatcher 类型进行分类,得到如下两个集合:

public class PluginFinder {
//pluginFinder将插件分类保存在两个集合中,分别是:按名字分类和按其他辅助信息分类
    private final Map<String, LinkedList<AbstractClassEnhancePluginDefine>> nameMatchDefine = new HashMap<String, LinkedList<AbstractClassEnhancePluginDefine>>();
    private final List<AbstractClassEnhancePluginDefine> signatureMatchDefine = new ArrayList<AbstractClassEnhancePluginDefine>();
    private final List<AbstractClassEnhancePluginDefine> bootstrapClassMatchDefine = new ArrayList<AbstractClassEnhancePluginDefine>();
   //构造方法
    public PluginFinder(List<AbstractClassEnhancePluginDefine> plugins) {
    //抽象方法enhanceClass方法定义在插件的抽象基类 AbstractClassEnhancePluginDefine中,每一个插件必须去实现这个类中的方法
        for (AbstractClassEnhancePluginDefine plugin : plugins) {
        //故enhanceClass是每个插件 都会自己去实现的方法,指定需要增强的类
            ClassMatch match = plugin.enhanceClass();

            if (match == null) {
                continue;
            }

            if (match instanceof NameMatch) {
                NameMatch nameMatch = (NameMatch) match;
                LinkedList<AbstractClassEnhancePluginDefine> pluginDefines = nameMatchDefine.get(nameMatch.getClassName());
                if (pluginDefines == null) {
                    pluginDefines = new LinkedList<AbstractClassEnhancePluginDefine>();
                    nameMatchDefine.put(nameMatch.getClassName(), pluginDefines);
                }
                pluginDefines.add(plugin);
            } else {
                signatureMatchDefine.add(plugin);
            }

            if (plugin.isBootstrapInstrumentation()) {
                bootstrapClassMatchDefine.add(plugin);
            }
        }
    }
//typeDescription是bytebuddy的内置接口,是对类的完整描述,包含了类的全类名 
//传入typeDescription,返回可以运用于typeDescription的类的插件

    public List<AbstractClassEnhancePluginDefine> find(TypeDescription typeDescription) {
        List<AbstractClassEnhancePluginDefine> matchedPlugins = new LinkedList<AbstractClassEnhancePluginDefine>();
        //根据名字信息匹配查找
        String typeName = typeDescription.getTypeName();
        if (nameMatchDefine.containsKey(typeName)) {
            matchedPlugins.addAll(nameMatchDefine.get(typeName));
        }
        //通过除了名字之外的辅助信息,在signatureMatchDefine集合中查找
        for (AbstractClassEnhancePluginDefine pluginDefine : signatureMatchDefine) {
            IndirectMatch match = (IndirectMatch) pluginDefine.enhanceClass();
            if (match.isMatch(typeDescription)) {
                matchedPlugins.add(pluginDefine);
            }
        }

        return matchedPlugins;
    }

    public ElementMatcher<? super TypeDescription> buildMatch() {
    //设置匹配的规则,名字是否相同,通过名字直接匹配
        ElementMatcher.Junction judge = new AbstractJunction<NamedElement>() {
            @Override
            public boolean matches(NamedElement target) {
                return nameMatchDefine.containsKey(target.getActualName());
            }
        };
        //接口不增强,排除掉
        //如果无法确定类的全限定名,则通过注解、回调信息等辅助方法间接匹配
        judge = judge.and(not(isInterface()));
        for (AbstractClassEnhancePluginDefine define : signatureMatchDefine) {
            ClassMatch match = define.enhanceClass();
            if (match instanceof IndirectMatch) {
                judge = judge.or(((IndirectMatch) match).buildJunction());
            }
        }
        return new ProtectiveShieldMatcher(judge);
    }

    public List<AbstractClassEnhancePluginDefine> getBootstrapClassMatchDefine() {
        return bootstrapClassMatchDefine;
    }
}
5.3.3 AgentBuilder

利用bytebuddy的API生成一个代理,并执行transform方法和监听器Listener(主要是日志相关)。
在premain中,通过链式调用,被builderMatch()匹配到的类都会执行transform方法,transform定义了字节码增强的逻辑:

//使用ByteBuddy创建AgentBuilder 
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 是 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)

上面代码中有些方法我们需要理解一下:
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 查找目标类匹配的插件(即AbstractClassEnhancePluginDefifine 对象),然后交由 AbstractClassEnhancePluginDefifine 完成增强,核心实现如下:

public DynamicType.Builder<?> transform(DynamicType.Builder<?>builder, 
TypeDescription typeDescription, // 被拦截的目标类1:实际插件演示
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; 
}

你可能感兴趣的:(互联网微服务前沿技术栈进阶,spring,java,skywalking,agent,skywalking源码)