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

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


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值