Skip to content

第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() 方法中的相关片段如下:

java
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 静态方法以及实例方法的增强:

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

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

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

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

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

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

java
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() 方法的具体实现:

java
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() 方法是其核心实现:

java
@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 接口,具体实现如下:

java
// 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/ 目录下可以看到增强后的类如下:

java
// 实现了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,相关代码片段如下:

java
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() 方法的实现如下:

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

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

java
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() 方法,这是我们创建数据连接的最常用方法,具体实现:

java
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 的一些信息,具体实现如下:

java
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() 方法对实例方法的增强,其实和增强静态方法的套路一样,我们直接看代码吧:

java
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() 方法实现如下:

java
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,如下所示:

java
// 记录了 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 类时的核心逻辑如下:

java
public static <T> 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 插件为例将上述核心逻辑串连起来。