™技术博客

skywalking | Agent插件原理

2021年2月17日

字节码增强

JavaAgent

  • Java Agent是java命令的一个参数(即 -javaagent),参数之后需要指定一个jar包

  • 在Java虚拟机启动时,执行main()函数之前,虚拟机会先找到-javaagent命令指定jar包,然后执行 premain-class中的premain()方法。

  • 一句概括其功能的话就是:main()函数之前的一个拦截器。

  • 使用Java Agent的步骤

    • 定义一个MANIFEST.MF文件,在其中添加premain-class配置项。
    • 创建premain-class配置项指定的类,并在其中实现premain()方法。
    • 将MANIFEST.MF文件和premain-class指定的类一起打包成一个jar包。
    • 使用-javaagent指定该jar包的路径即可执行其中的premain()方法。
  • premain()方法第二个参数:Instrumentation。位于java.lang.instrument包中,通过这个工具包,可以编写一个强大的Java Agent程序,用来动态替换或是修改某些类的定义。

  • Instrumentation 中的核心 API 方法

    • addTransformer()/removeTransformer()方法:注册/注销一个ClassFileTransformer类的实例,该Transformer会在类加载的时候被调用,可用于修改类定义。
    • redefineClasses()方法:该方法针对的是已经加载的类,它会对传入的类进行重新定义。
    • getAllLoadedClasses()方法:返回当前JVM已加载的所有类。
    • getInitiatedClasses() 方法:返回当前JVM已经初始化的类。
    • getObjectSize()方法:获取参数指定的对象的大小。

ByteBuddy

  • Byte Buddy 通过编写简单的Java代码即可创建自定义的运行时类。
  • ByteBuddy类,任何一个由Byte Buddy创建/增强的类型都是通过ByteBuddy类的实例来完成的
  • Byte Buddy 动态增强代码总共有三种方式
    • subclass:对应 ByteBuddy.subclass()方法。就是为目标类(即被增强的类)生成一个子类,在子类方法中插入动态代码。
    • rebasing:对应ByteBuddy.rebasing()方法。当使用rebasing方式增强一个类时,Byte Buddy保存目标类中所有方法的实现,也就是说,当 Byte Buddy 遇到冲突的字段或方法时,会将原来的字段或方法实现复制到具有兼容签名的重新命名的私有方法中,而不会抛弃这些字段和方法实现。从而达到不丢失实现的目的。这些重命名的方法可以继续通过重命名后的名称进行调用。
    • redefinition:对应ByteBuddy.redefine()方法。当重定义一个类时,Byte Buddy可以对一个已有的类添加属性和方法,或者删除已经存在的方法实现。如果使用其他的方法实现替换已经存在的方法实现,则原来存在的方法实现就会消失。
  • Byte Buddy 提供了几种类加载策略,这些策略定义在ClassLoadingStrategy.Default中
    • WRAPPER策略:创建一个新的ClassLoader来加载动态生成的类型。
    • CHILD_FIRST策略:创建一个子类优先加载的ClassLoader,即打破了双亲委派模型。
    • INJECTION策略:使用反射将动态生成的类型直接注入到当前ClassLoader中。
  • Byte Buddy功能
    • intercept() 方法: 修改方法实现
    • defineMethod() 方法:新增方法
    • defineField() 方法:新增字段
    • Implement() 方法:实现一个接口

Skywalking探针

插件定义基础父类

  • AbstractClassEnhancePluginDefine是所有插件的父类,SkywalkingAgent.Transformer会通过其enhanceClass()方法返回的ClassMatch对象,匹配到要增强的目标类。在不同的插件实现类中,enhanceClass()方法返回的 ClassMatch 对象不同。
  • 完成目标类和插件类的匹配之后,会进入 define() 方法
  1. 通过witnessClass()方法确定当前插件与当前拦截到的目标类的版本是否匹配。若版本不匹配,则define()方法直接结束,当前插件类不会增强该类;若版本匹配,则继续后续逻辑。
  2. 进入 enhance() 方法执行增强逻辑。
  3. 设置插件增强标识。

静态方法增强逻辑

  • StaticMethodsInterceptPoint接口:描述了当前插件要拦截目标类的哪些static静态方法,以及委托给哪个类去增强
    • getMethodsMatcher():用于匹配目标静态方法
    • getMethodsInterceptor():拦截到的静态方法交给哪个Interceptor来增强
    • isOverrideArgs():增强过程中是否需要修改参数
  1. 通过 method() 方法拦截到静态方法之后,如果需要修改方法参数,则会通过StaticMethodsInterWithOverrideArgs对象进行增强
  2. 如果不需要修改方法参数,则会通过StaticMethodsInter对象进行增强。
  3. 加载插件指定的StaticMethodsAroundInterceptor
  4. 调用 interceptor.beforeMethod()做前置处理
  5. 根据before()的处理结果判定是否调用目标方法;如果需要传参,参数可以在beforeMethod()方法中改动,OverrideArgs的意义
  6. 如果出现异常,会先通知interceptor中的handleMethodException()方法进行处理
  7. 通过调用 interceptor.afterMethod()方法进行后置处理
  • StaticMethodsAroundInterceptor接口 相关API
    • before():在目标方法之前调用。
    • after():在目标方法之后调用。
    • handleMethodException():在目标方法抛出异常时调用。

ClassEnhancePluginDefine 是个典型的模板方法模式的使用场景,其enhanceClass()方法只实现了增强静态方法的基本流程,真正的增强逻辑全部通过getStaticMethodsInterceptPoints()抽象方法推迟到子类实现。

构造及普通实例方法增强逻辑

  • 实现EnhancedInstance接口: 为目标类添加了一个字段,让目标类实现EnhancedInstance接口

    • 定义了getSkyWalkingDynamicField()和setSkyWalkingDynamicField()两个方法,分别读写新增的_$EnhancedClassField_ws字段
  • 增强构造方法: 使用的是 ConstructorInterceptPoint

    • 通过ConstructorInter对象进行增强。
    • 加载插件指定的InstanceConstructorInterceptor
    • InstanceConstructorInterceptor相关API,onConstruct()方法
  • 增强实例方法:

    • InstanceMethodsInterceptPoint接口:描述了当前插件要拦截目标类的哪些实例方法,以及委托给哪个类去增强;API与增强静态方法类似
    • 需要修改方法参数,则会通过InstMethodsInterWithOverrideArgs对象进行增强;不需要修改方法参数,则会通过InstMethodsInter对象进行增强。
    • intercept() 方法 拦截逻辑 同 拦截静态方法类似
    • 加载插件指定的InstanceMethodsAroundInterceptor

增强实例方法和静态方法对比

拦截器Loader

  • 在 InterceptorInstanceLoader里面会维护一个ClassLoader Cache,以及一个Instance Cache;动态加载Interceptpr
    • INSTANCE_CACHE:记录了instanceKey与实例之间的映射关系,保证单例
    • EXTEND_PLUGIN_CLASSLOADERS:记录了targetClassLoader以及其子AgentClassLoader的对应关系
  • 通过 InterceptorInstanceLoader.load()这个静态方法加载Interceptor类
  1. 通过该instanceKey保证该Interceptor在一个ClassLoader中只创建一次;
  2. 查找targetClassLoader对应的子AgentClassLoader
  3. 如果pluginLoader不存在,为targetClassLoader创建子AgentClassLoader;
  4. 通过子AgentClassLoader加载Interceptor类
  5. 记录Interceptor对象到INSTANCE_CACHE

扫描二维码,分享此文章