手写SkyWalking-Agent模块(抽象可拔插Agent模块、组件及插件的抽象)
  hzIe5RkCbU7L 2023年12月15日 40 0

(目录)


※ 抽象Agent模块 (一条指令挂载所有plugins 和 通用的类与方法的字节码增强框架)

※ 可插拔式插件加载实现要点分析

  1. 怎么做到只指定一个-javaagent参数
  2. 怎么加载多个plugin?
  3. 怎么把typeDescription和要拦截的method对应起来
  4. 怎么把typeDescription和要拦截的method的拦截器Interceptor对应起来

接下来我们依次解决这些问题代码思路借鉴SkyWalking-Agent模块的源码

构建以下目录结构的项目↓↓

image-20230909161426755


实现只指定一个-javaagent参数,实现挂载所有jar

SkyWalking源码plugins目录下有成千上百个插件,都通过-javaagent名命一个一个挂载肯定不现实

怎么去解决这个问题,想一下思路?

只需要一个留 PreMain 入口类就行

image-20230909161455755


怎么加载多个plugin?

最简单的想法:type()中把需要拦截的都写上

	public static void premain(String args, Instrumentation instrumentation) {

        log.info("进入到premain,args:{}",args);

        AgentBuilder builder =  new AgentBuilder.Default()
                .type(
                        // springmvc
                        isAnnotatedWith(named(CONTROLLER_NAME).or(named(REST_CONTROLLER_NAME)))
                        // mysql
                        .or(named(CLIENT_PS_NAME).or(named(SERVER_PS_NAME)))
                        // es
                        .or(xx))
                .transform(new Transformer())
                .installOn(instrumentation);
    }

怎么把typeDescription和要拦截的method对应起来

我们想一下之前拦截method是不是都是指定对应的规则去进行拦截,那我们可不可以使用 or 把条件联系起来?

eg:如下所示,答案是肯定不行,具体原因的话自己想一下

@Override
    public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
                                            TypeDescription typeDescription,
                                            // 加载 typeDescription这个类的类加载器
                                            ClassLoader classLoader,
                                            JavaModule module) {
        log.info("actualName to transform:{}", typeDescription.getActualName());
        DynamicType.Builder.MethodDefinition.ReceiverTypeDefinition<?> intercept = builder
                .method(
                        // mysal 拦截方法
                        named("execute").or(named("executeUpdate")).or(named("executeQuery"))
                        // mvc 拦截方法
                        .or(not(isStatic()).and(isAnnotatedWith(
                                nameStartsWith(MAPPING_PKG_PREFIX).and(nameEndsWith(MAPPING_SUFFIX))
                        )))
                        // es 拦截方法
                        .or(xx)
                )
        return intercept;
    }

所以我们需要想其他办法去抽象出一个组件,形成自定义通用的匹配规则


怎么把typeDescription和要拦截的method的拦截器Interceptor对应起来

同上,我们怎么去选择 我们想要的拦截器:

目前是无法去自定义的选择我们需要的Interceptor的

eg:bytebuddy的Api只能指定一拦截器的实现

    @Override
    public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
                                            TypeDescription typeDescription,
                                            // 加载 typeDescription这个类的类加载器
                                            ClassLoader classLoader,
                                            JavaModule module) {
        log.info("actualName to transform:{}", typeDescription.getActualName());
        DynamicType.Builder.MethodDefinition.ReceiverTypeDefinition<?> intercept = builder
                .method(
                        // mysal 拦截方法
                        named("execute").or(named("executeUpdate")).or(named("executeQuery"))
                        // mvc
                        .or(not(isStatic()).and(isAnnotatedWith(
                                nameStartsWith(MAPPING_PKG_PREFIX).and(nameEndsWith(MAPPING_SUFFIX))
                        )))
                        // es
                        .or(xx)
                )
                /*
                    指定拦截器时 , 需要选择 指定的 Interceptor
                    // springmvc
                    new SpringMvcInterceptor()
                    // mysql
                    new MysqlInterceptor()
                    // es
                    new EsInterceptor()
                  */
                .intercept(MethodDelegation.to(new MysqlInterceptor()));
        return intercept;
    }

以上的问题,我们目前是无法依靠现有的api去完成可插拔式插件的实现的

只有一个办法了抽象组件

研究SkyWalking-Agent模块抽取出其中主要实现,来按照下面思路一步一步的实现一下


※ 组件的抽象 ※

通过上面的分析,我们现在的核心问题

把 typeDescription 、 method 、lnterceptor 对应起来

点进type()的源码,查看他的接收参数类型是 ElementMatcher<? super TypeDescription> typeMatcher

image-20230909165117136

点进method()的源码,查看接收参数:ElementMatcher<? super MethodDescription> matcher

image-20230909172329502


通过上面信息抽象出以下的组件

  1. AbstractClassEnhancePluginDefine 所有插件的顶级父类 , 只要是插件必须直接或间接的集成这个类

  2. ClassMatch 获取当前插件 需要拦截的类

  3. ConstructorMethodsInterceptorPoint 构造方法拦截点

  4. InstanceMethodsInterceptorPoint 实例方法的拦截点

  5. StaticMethodsInterceptorPoint 静态方法拦截点


AbstractClassEnhancePluginDefine 所有插件的顶级父类

具体代码:

AbstractClassEnhancePluginDefine所有插件的顶级父类

/**
 * @Description: 所有插件的顶级父类 , 只要是插件必须直接或间接的集成这个类
 * @Author: Perceus
 * Date: 2023-09-09
 * Time: 16:46
 */
public abstract class AbstractClassEnhancePluginDefine {

    /**
     * 获取当前插件 需要拦截的类
     * @return
     */
    protected abstract ClassMatch enhanceClass();

    /**
     * 获取当前插件,需要拦截的 实例方法 拦截点
     * 一个插件可以多个拦截点
     * @return
     */
    protected abstract InstanceMethodsInterceptorPoint[] getInstanceMethodsInterceptorPoints();

    /**
     * 获取当前插件,需要拦截的 构造方法 拦截点
     * 一个插件可以多个拦截点
     * @return
     */
    protected abstract ConstructorMethodsInterceptorPoint[] getConstructorMethodsInterceptorPoints();

    /**
     * 获取当前插件,需要拦截的 静态方法 拦截点
     * 一个插件可以多个拦截点
     * @return
     */
    protected abstract StaticMethodsInterceptorPoint[] getStaticMethodsInterceptorPoints();



}

MethodsInterceptorPoint (构造、静态、实例方法拦截点)

ConstructorMethodsInterceptorPoint构造方法拦截点

/**
 * @Description: 构造方法拦截点
 * @Author: Perceus
 * Date: 2023-09-09
 * Time: 17:30
 */
public interface ConstructorMethodsInterceptorPoint {

    /**
     *  要拦截哪些方法
     * @return 作为method()方法的参数
     */
    ElementMatcher<? super MethodDescription> getConstructorMatcher();

    /**
     *  获取被增强方法对应的拦截器
     * @return 拦截器路径
     */
    String getConstructorInterceptor();

}

InstanceMethodsInterceptorPoint实例方法的拦截点*

/**
 * @Description: 实例方法的拦截点
 * @Author: Perceus
 * Date: 2023-09-09
 * Time: 17:20
 */
public interface InstanceMethodsInterceptorPoint {

    /**
     *  要拦截哪些方法
     * @return 作为method()方法的参数
     */
    ElementMatcher<? super MethodDescription> getMethodsMatcher();

    /**
     *  获取被增强方法对应的拦截器
     * @return 拦截器路径
     */
    String getMethodsInterceptor();

}

StaticMethodsInterceptorPoint 静态方法拦截点

/**
 * @Description: 静态方法拦截点
 * @Author: Perceus
 * Date: 2023-09-09
 * Time: 17:31
 */
public interface StaticMethodsInterceptorPoint {

    /**
     *  要拦截哪些方法
     * @return 作为method()方法的参数
     */
    ElementMatcher<? super MethodDescription> getStaticMatcher();

    /**
     *  获取被增强方法对应的拦截器
     * @return 拦截器路径
     */
    String getStaticInterceptor();

}

我们现在重新去写插件,现在我们肯定不能向之前那种写单独的premain去进行命令的挂载了

一、MySQL 插件的抽象

  1. 定义 MySQL 插件 , 继承AbstractClassEnhancePluginDefine
  2. 重写getInstanceMethodsInterceptorPoints()的方法实现方法拦截表达式、Interceptor路径
  3. 编写 Match规则实现,构造类的拦截条件

1. MysqlInstrumentation 定义MySQL插件

MysqlInstrumentation类 代码实现:

/**
 * @Description: 定义MySQL插件 , 继承AbstractClassEnhancePluginDefine
 * @Author: Perceus
 * Date: 2023-09-09
 * Time: 17:44
 */
public class MysqlInstrumentation extends AbstractClassEnhancePluginDefine {

    private static final String CLIENT_PS_NAME = "com.mysql.cj.jdbc.ClientPreparedStatement";
    private static final String SERVER_PS_NAME = "com.mysql.cj.jdbc.ServerPreparedStatement";
    private static final String INTERCEPTOR = "com.roadjava.skywalking.agent.demoapm.plugins.mysql.interceptor.MysqlInterceptor";


    @Override
    protected ClassMatch enhanceClass() {
        // 构造多个类名的拦截条件
        // named(CLIENT_PS_NAME).or(named(SERVER_PS_NAME)) 条件
        return MultiClassNameMatch.byMultiClassMatch(CLIENT_PS_NAME, SERVER_PS_NAME);
    }

    // 这个插件拦截的方法只用 拦截实例方法
    @Override
    protected InstanceMethodsInterceptorPoint[] getInstanceMethodsInterceptorPoints() {
        return new InstanceMethodsInterceptorPoint[]{
                new InstanceMethodsInterceptorPoint() {
                    /**
                     *  方法拦截表达式
                     */
                    @Override
                    public ElementMatcher<? super MethodDescription> getMethodsMatcher() {
                        return named("execute")
                                .or(named("executeUpdate"))
                                .or(named("executeQuery"));
                    }

                    /**
                     *  Interceptor拦截器路径
                     * @return
                     */
                    @Override
                    public String getMethodsInterceptor() {
                        return INTERCEPTOR;
                    }
                }
        };
    }

    @Override
    protected ConstructorMethodsInterceptorPoint[] getConstructorMethodsInterceptorPoints() {
        return null;
    }

    @Override
    protected StaticMethodsInterceptorPoint[] getStaticMethodsInterceptorPoints() {
        return null;
    }
}

2. ClassMatch 表示要匹配类的 最顶层接口(两类:NameMatch、IndirectMatch)

ClassMatch

/**
 * @Description: 表示要匹配类的 最顶层接口,有两类
 *              - NameMatch
 *              - IndirectMatch
 * @Author: Perceus
 * Date: 2023-09-09
 * Time: 17:16
 */
public interface ClassMatch {

}

NameMatch:专门用于 类名称=xxx , 仅适用于 named(xxx)

/**
 * @Description: 专门用于 类名称=xxx , 仅适用于 named(xxx)
 *              eg:.type(named(xxx))
 * @Author: Perceus
 * Date: 2023-09-09
 * Time: 18:40
 */
public class NameMatch implements ClassMatch{
}

3. IndirectMatch 所有非NameMatch的情况都要实现 IndirectMatch

/**
 * @Description: 所有非NameMatch的情况都要实现IndirectMatch
 * @Author: Perceus
 * Date: 2023-09-09
 * Time: 18:42
 */
public interface IndirectMatch extends ClassMatch{
    /**
     * 用于构造type()的参数
     * 比如构建 -> named(CLIENT_PS_NAME).or(named(SERVER_PS_NAME))
     */
    ElementMatcher.Junction<? super TypeDescription> buildJunction();

    /**
     * 用于判断typeDescription是否满足当前匹配器(IndirectMatch的实现类)的条件
     * @param typeDescription 待判断的类
     * @return true:匹配 false:不匹配
     */
     boolean isMatch(TypeDescription typeDescription);
}

4. MultiClassNameMatch 多个类型条件相等的匹配器

/**
 * @Description: 多个类型条件相等的匹配器
 * @Author: Perceus
 * Date: 2023-09-09
 * Time: 18:44
 */
public class MultiClassNameMatch implements IndirectMatch {

    /**
     * 要匹配的类名称
     */
    private List<String> needMatchClassNames;

    private MultiClassNameMatch(String[] classNames) {
        if (classNames == null || classNames.length == 0) {
            throw new IllegalArgumentException("needMatchClassNames can not be null");
        }
        this.needMatchClassNames = Arrays.asList(classNames);
    }

    /**
     * 多个类在这里 要求是 or的关系
     * @return
     */
    @Override
    public ElementMatcher.Junction<? super TypeDescription> buildJunction() {
        ElementMatcher.Junction<? super TypeDescription> junction = null;
        // 构造类拦截条件
        for(String needMatchClassName:needMatchClassNames){
            if (junction == null) {
                junction = named(needMatchClassName);
            }else {
                junction = junction.or(named(needMatchClassName));
            }
        }
        return null;
    }

    public static IndirectMatch byMultiClassMatch(String... classNames) {
        return new MultiClassNameMatch(classNames);
    }

}

二、MVC 插件的抽象

步骤还是类似上面,只不过细节有一些不一样

1. ClassAnnotationNameMatch 某个类要同时含有某几个注解匹配器

我现在所写的Match规则:

是某个类同时含有某几个注解的匹配器,关系是and

代码如下:

/**
 * @Description: 某个类要同时含有某几个注解匹配器
 * @Author: Perceus
 * Date: 2023-09-09
 * Time: 19:10
 */
public class ClassAnnotationNameMatch implements IndirectMatch {
    /**
     * 要包含的注解列表
     */
    private List<String> annotations;

    private ClassAnnotationNameMatch(String[] annotations) {
        if (annotations == null || annotations.length == 0) {
            throw new IllegalArgumentException("annotations can not be null");
        }
        this.annotations = Arrays.asList(annotations);
    }

    /**
     * 多个注解要求是and的关系
     * @return
     */
    @Override
    public ElementMatcher.Junction<? super TypeDescription> buildJunction() {
        ElementMatcher.Junction<? super TypeDescription> junction = null;
        for (String anno : annotations) {
            if (junction == null) {
                junction = buildEachAnno(anno);
            } else {
                junction = junction.and(buildEachAnno(anno));
            }
        }
        return junction;
    }

    private ElementMatcher.Junction<TypeDescription> buildEachAnno(String anno) {
        return isAnnotatedWith(named(anno));
    }

    public static IndirectMatch byClassAnnotationMatch(String... annotations) {
        return new ClassAnnotationNameMatch(annotations);
    }
}

我所需要的是:

MVC拦截的类包含 @Controller 或者 @RestController 注解的类,关系是or

所以像Mysql那种一次性调用就不行,代码如下:

    @Override
    protected ClassMatch enhanceClass() {
        // 构造 isAnnotatedWith(named(CONTROLLER_NAME).or(named(REST_CONTROLLER_NAME)))条件
        // 但是 ClassAnnotationNameMatch中的条件是and , 所以不能这么构建
        // 把这个类抽象成抽象类 , 让子类 去实现这个逻辑
        return ClassAnnotationNameMatch.byClassAnnotationMatch(CONTROLLER_NAME, REST_CONTROLLER_NAME);
    }

2. SpringmvcCommonInstrumentation抽象类 (SpringMVC插件公共类)

现在抽象出一个SpringmvcCommonInstrumentation抽象类enhanceClass()方法逻辑由子类构造

/**
 * @Description: SpringMVC插件公共类 , 继承AbstractClassEnhancePluginDefine
 * @Author: Perceus
 * Date: 2023-09-09
 * Time: 17:44
 */
public abstract class SpringmvcCommonInstrumentation extends AbstractClassEnhancePluginDefine {
    private static final String INTERCEPTOR = "com.roadjava.skywalking.agent.demo.apm.plugins.springmvc.interceptor.SpringmvcInterceptor";
    private static final String MAPPING_PKG_PREFIX = "org.springframework.web.bind.annotation";
    private static final String MAPPING_SUFFIX = "Mapping";

    /*
    @Override
    protected ClassMatch enhanceClass() {
        // 构造 isAnnotatedWith(named(CONTROLLER_NAME).or(named(REST_CONTROLLER_NAME)))条件
        // 但是 ClassAnnotationNameMatch中的条件是and , 所以不能这么构建
        // 把这个类抽象成抽象类 , 让子类 去实现这个逻辑
        return ClassAnnotationNameMatch.byClassAnnotationMatch(CONTROLLER_NAME, REST_CONTROLLER_NAME);
    }
    */

    // 这个插件拦截的方法只用 拦截实例方法
    @Override
    protected InstanceMethodsInterceptorPoint[] getInstanceMethodsInterceptorPoints() {
        return new InstanceMethodsInterceptorPoint[]{
                new InstanceMethodsInterceptorPoint() {
                    /**
                     *  方法拦截表达式
                     *   not(isStatic()).and(isAnnotatedWith(
                     *        nameStartsWith(MAPPING_PKG_PREFIX).and(nameEndsWith(MAPPING_SUFFIX))
                     *   ))
                     */
                    @Override
                    public ElementMatcher<? super MethodDescription> getMethodsMatcher() {
                        return not(isStatic()).and(isAnnotatedWith(
                                nameStartsWith(MAPPING_PKG_PREFIX).and(nameEndsWith(MAPPING_SUFFIX))
                        ));
                    }

                    /**
                     *  Interceptor拦截器路径
                     * @return
                     */
                    @Override
                    public String getMethodsInterceptor() {
                        return INTERCEPTOR;
                    }
                }
        };
    }

    @Override
    protected ConstructorMethodsInterceptorPoint[] getConstructorMethodsInterceptorPoints() {
        return new ConstructorMethodsInterceptorPoint[0];
    }

    @Override
    protected StaticMethodsInterceptorPoint[] getStaticMethodsInterceptorPoints() {
        return new StaticMethodsInterceptorPoint[0];
    }
}

3. ControllerInstrumentation 抽象类实现 拦截@Controller注解

拦截带有**@Controller注解的SpringMVC**插件 , 继承SpringmvcCommonInstrumentation

/**
 * @Description: 拦截带有@Controller注解的SpringMVC插件 , 继承SpringmvcCommonInstrumentation
 * @Author: Perceus
 * Date: 2023-09-09
 * Time: 17:44
 */
public class ControllerInstrumentation extends SpringmvcCommonInstrumentation {
    // @Controller注解的路径
    private static final String CONTROLLER_NAME = "org.springframework.stereotype.Controller";

    @Override
    protected ClassMatch enhanceClass() {
        // 构造 isAnnotatedWith((named(CONTROLLER_NAME))条件
        return ClassAnnotationNameMatch.byClassAnnotationMatch(CONTROLLER_NAME);
    }

}

4. RestControllerInstrumentation 抽象类实现 拦截带有**@RestController**注解

拦截带有@RestController注解的SpringMVC插件继承SpringmvcCommonInstrumentation

/**
 * @Description: 拦截带有@RestController注解的SpringMVC插件 , 继承SpringmvcCommonInstrumentation
 * @Author: Perceus
 * Date: 2023-09-09
 * Time: 17:44
 */
public class RestControllerInstrumentation extends SpringmvcCommonInstrumentation {

    // @RestController注解的路径
    private static final String REST_CONTROLLER_NAME = "org.springframework.web.bind.annotation.RestController";

    @Override
    protected ClassMatch enhanceClass() {
        // 构造 isAnnotatedWith((named(REST_CONTROLLER_NAME))条件
        return ClassAnnotationNameMatch.byClassAnnotationMatch(REST_CONTROLLER_NAME);
    }

}

三、maven-antrun-plugin 创建指定目录并拷贝jar包到指定目录

apm-agent包下pom中引入maven插件

            <!--  创建指定目录并拷贝jar包到指定目录    -->
            <plugin>
                <artifactId>maven-antrun-plugin</artifactId>
                <version>1.8</version>
                <executions>
                    <!-- 在clean阶段删除dist目录  -->
                    <execution>
                        <id>clean</id>
                        <phase>clean</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                        <configuration>
                            <target>
                                <delete dir="${project.basedir}/../../dist" />
                            </target>
                        </configuration>
                    </execution>
                    <!-- 在package阶段创建/dist、/dist/plugins、拷贝agent.jar到/dist下  -->
                    <execution>
                        <id>package</id>
                        <phase>package</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                        <configuration>
                            <target>
                                <mkdir dir="${project.basedir}/../../dist" />
                                <copy file="${project.build.directory}/apm-agent-1.0-SNAPSHOT-jar-with-dependencies.jar" tofile="${project.basedir}/../../dist/apm-agent-1.0-SNAPSHOT-jar-with-dependencies.jar" overwrite="true" />
                                <mkdir dir="${project.basedir}/../../dist/plugins" />
                            </target>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

运行打包命令,根目录下生成一个dist文件夹,里面拷贝了一个jar包和创建了一个plugins文件夹

image-20230909201709283


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年12月15日 0

暂无评论

hzIe5RkCbU7L