skywalking源码解析系列一: agent插件加载原理

在2020年3月份开始接触skywalking到现在,使用skywalking已经一年时间,期间对内部代码进行了详细阅读,并且由于项目需要,我们已经对源码进行了二开,新增了各种个性化需求,可以说,我们对skywalking底层源码了解程度已经相对较高。
本来想通过笔记对这一年来的源码阅读及理解成果进行记录,无意中发现这篇文章写得相当的好,也懒得去写了,因此直接转载,后续该系列文章会夹杂着转载与原创,欢迎各位码友交流探讨

1 . 简介

本文涉及到的源码取自版本 : apache-skywalking-apm-7.0.0 ,不同版本实现差异可能会有一些区别,但是大体框架上没有变化的 , 一些地方为了方便理解,我拆分了 lamda 表达式,或者把一些写在一起的代码给做了拆分,但是整体逻辑是不变的

2. javaAgent

skywalking 是一个 分布式追踪系统 , 他可以帮助我们看到一个请求经过了多少个微服务,中途调用了多少数据库,redis,mq 等中间件, 要实现这样的功能,其实很简单只要在设计系统的时候, 每经过一次请求,每调用一次中间件,都把对应的日志给存起来, 然后提供一个 ui 服务,也同样能实现对应的功能. 但是如果这样做的话, 对代码就有很强的侵入性 , 每一个业务系统都需要修改,在业务代码里去加上对应的日志,对于喜欢偷懒的程序员来说,这是非常不舒服的.

所以 skywalking 使用 agent 的形式去接入 业务系统,这样就不需要在业务系统里添加任何的日志代码,也能记录到对应的调用数据库

那么agent 到底是什么嗯? 这里提一点, idea的破解插件, spring boot热更新插件, jacoco 都是基于 javaAgent 实现的

在JDK1.5以后,我们可以使用agent技术构建一个独立于应用程序的代理程序(即为Agent),用来协助监测、运行甚至替换其他JVM上的程序。使用它可以实现虚拟机级别的AOP功能。

划重点: 虚拟机级别的aop
这样的话,就能在 JVM加载class二进制文件的时候, 修改对应 业务系统的 class文件,动态在class文件里加上记录日志的代码.

小朋友你是否有很多问号,要如何去修改字节码嗯?
如果直接去修改字节码,是非常非常麻烦的,所以这里引入了一个字节码修改框架byte-buddy
应为篇幅的关系,这里就不去赘述了,不了解的同学可以去看看

  1. Byte Buddy 教程
  2. 在Java-Agent中使用使用ByteBuddy修改字节码

3. SkyWalkingAgent

如上面所说, 如果下面的内容想看着流畅,那么需要先去了解 javaAgent , ByteBuddy 以及 真正去使用过skywalking

看源码找一个入口很重要
加载命令为 -javaagent:/对应路径/skywalking-agent.jar,及把包 skywalking-agent.jar 当做一个 agent给加载进入虚拟机,根据agent 对应的规则,我们用 解压工具 打开 skywalking-agent.jar ,找到对应目录
在这里插入图片描述
在该文件中找到了 agent 的入口类 ,关注 对应key为 Premain-Class 的这一行配置

Manifest-Version: 1.0
Implementation-Title: apm-agent
Implementation-Version: 7.0.0
Built-By: bignosecat
Specification-Vendor: The Apache Software Foundation
Can-Redefine-Classes: true
Specification-Title: apm-agent
Implementation-Vendor-Id: org.apache.skywalking
Implementation-Vendor: The Apache Software Foundation
Premain-Class: org.apache.skywalking.apm.agent.SkyWalkingAgent
Can-Retransform-Classes: true
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_151
Specification-Version: 7.0
Implementation-URL: http://maven.apache.org

该类中确实有一个叫做 premain的方法 作为 agent 的入口方法,那么他会在 应用系统 启动的时候,去执行 SkyWalkingAgent#premain 方法,我们来看看这个方法里他做了什么

对应代码上都有注释, 需要深入讲解的 在注释后面标明了标题序号,可以快速定位

public static void premain(String agentArgs, Instrumentation instrumentation) throws PluginException {
        final PluginFinder pluginFinder;
        try {
            //初始化一些参数
            SnifferConfigInitializer.initialize(agentArgs);
            //去加载了所有的插件,具体看下面 <3.1> 的解析
            List<AbstractClassEnhancePluginDefine> abstractClassEnhancePluginDefines = new PluginBootstrap().loadPlugins();
            //把插件对象放入 PluginFinder 容器,在PluginFinder 里面还会给 AbstractClassEnhancePluginDefine 分类
            pluginFinder = new PluginFinder(abstractClassEnhancePluginDefines);

        } 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对象用于修改字节码
        final ByteBuddy byteBuddy = new ByteBuddy().with(TypeValidation.of(Config.Agent.IS_OPEN_DEBUGGING_CLASS));
        //去忽略一些不需要修改字节码的包
        //可以理解成设置了aop的切面
        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();
        //加载 Bootstrap 相关的插件
        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;
        }

        //从插件里找到哪一些类需要修改字节码,把匹配规则给找出来
        //详情看 <3.2>
        ElementMatcher<? super TypeDescription> elementMatcher = pluginFinder.buildMatch();
        //字节码的修改规则
        //详情看 <3.3>
        Transformer transformer = new Transformer(pluginFinder);

        //使用bytebuddy 去修改字节码
        agentBuilder.type(elementMatcher)
                    .transform(new Transformer(pluginFinder))
                    .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
                    .with(new Listener())
                    .installOn(instrumentation);

        try {
            ServiceManager.INSTANCE.boot();
        } catch (Exception e) {
            logger.error(e, "Skywalking agent boot failure.");
        }

        Runtime.getRuntime()
               .addShutdownHook(new Thread(ServiceManager.INSTANCE::shutdown, "skywalking service shutdown thread"));
    }

3.1 loadPlugins

因为有不同的web容器 , 中间件 ,记录的方式也非常的多, 所以SkyWalkingAgent 引入了插件机制,比如 你只想记录 连接数据相关的记录,那么就引入 jdbc 的对应插件即可.

那么 SkyWalkingAgent 是如何加载插件的嗯? 入口就是

List<AbstractClassEnhancePluginDefine> abstractClassEnhancePluginDefines = new PluginBootstrap().loadPlugins()

我们进入loadPlugins() 方法看看他都做了什么

public List<AbstractClassEnhancePluginDefine> loadPlugins() throws AgentPackageNotFoundException {
        AgentClassLoader.initDefaultLoader();
        //生成一个插件加载器
        PluginResourcesResolver resolver = new PluginResourcesResolver();
        //去指定的路径下去搜索 文件 skywalking-plugin.def
        //指定的路径 默认是 skywalking-agent.jar 同目录的 activations和 plugins 文件夹
        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>();
        }

        //去加载了 skywalking-plugin.def ,并且解析该文件
        for (URL pluginUrl : resources) {
            try {
                PluginCfg.INSTANCE.load(pluginUrl.openStream());
            } catch (Throwable t) {
                logger.error(t, "plugin file [{}] init failure.", pluginUrl);
            }
        }

        //skywalking-plugin.def 文件里指定的插件的 类给集中保存到容器 pluginClassList里
        List<PluginDefine> pluginClassList = PluginCfg.INSTANCE.getPluginClassList();

        List<AbstractClassEnhancePluginDefine> plugins = new ArrayList<AbstractClassEnhancePluginDefine>();
        //把上面 pluginClassList 中插件的类 全部给实例化了
        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-plugin.def 文件,然后把skywalking-plugin.def 文件里指定的插件给实例化,然后反回出去
这里我们看看 skywalking-plugin.def 里面都是什么内容

比如: spring-mvc 的插件中 skywalking-plugin.def 的内容如下

spring-mvc-annotation-5.x=org.apache.skywalking.apm.plugin.spring.mvc.v5.define.ControllerInstrumentation
spring-mvc-annotation-5.x=org.apache.skywalking.apm.plugin.spring.mvc.v5.define.RestControllerInstrumentation
spring-mvc-annotation-5.x=org.apache.skywalking.apm.plugin.spring.mvc.v5.define.HandlerMethodInstrumentation

加载到代码里如下所示
在这里插入图片描述
然后根据对应的类路径去实例化插件对象
在这里插入图片描述

最终把 容器 plugins 返回出去, loadPlugins() 方法就结束了

3.2 buildMatch

回到 premain 方法中, 并不是,所有的类都需要去修改字节码的,那么什么样的class才有资格去修改字节码,我们看方法 pluginFinder.buildMatch()
首先要先明确一点的是 pluginFinder 持有了所有插件对象的一个容器

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

上述我们可以看到, 对于有的插件, 他会调用 define.enhanceClass() 方法去获取匹配规则,
这里 我们可以随意打开一个 插件类 AbstractControllerInstrumentation,可以发现里面重写了方法 enhanceClass

@Override
    protected ClassMatch enhanceClass() {
        return ClassAnnotationMatch.byClassAnnotationMatch(getEnhanceAnnotations());
    }
	protected abstract String[] getEnhanceAnnotations();

篇幅关系,就不进去深究 byClassAnnotationMatch 的实现了,从名字可以看出,这是根据注解去匹配,而getEnhanceAnnotations 是一个抽象方法,他需要 插件开发者 去重写这个方法,
这里我们可以看到
在这里插入图片描述
ControllerInstrumentationAbstractControllerInstrumentation 的子类之一,

public class ControllerInstrumentation extends AbstractControllerInstrumentation {

    public static final String ENHANCE_ANNOTATION = "org.springframework.stereotype.Controller";

    @Override
    protected String[] getEnhanceAnnotations() {
        return new String[] {ENHANCE_ANNOTATION};
    }
}

及插件 ControllerInstrumentation 将会把所有打了注解 @Controller 的类修改字节码

3.3 Transformer

skywalking-agent.jar 到底对字节码做了什么,所有的答案就在 Transformer

被上述 3.2 buildMatch 命中的类都会执行一次 Transformer#transform 方法来修改class文件的内容

@Override
        public DynamicType.Builder<?> transform(final DynamicType.Builder<?> builder,
                                                final TypeDescription typeDescription,
                                                final ClassLoader classLoader,
                                                final JavaModule module) {
            //这里 typeDescription 是要被修改class的类
            //找到哪几个插件需要去修改对应的class
            List<AbstractClassEnhancePluginDefine> pluginDefines = pluginFinder.find(typeDescription);
            if (pluginDefines.size() > 0) {
                DynamicType.Builder<?> newBuilder = builder;
                //这个EnhanceContext 只是为了保证流程的一个记录器,比如执行了某个步骤后就会记录一下,防止重复操作
                EnhanceContext context = new EnhanceContext();
                for (AbstractClassEnhancePluginDefine define : pluginDefines) {
                    //真正去修改字节码的逻辑
                    DynamicType.Builder<?> possibleNewBuilder = define.define(
                        typeDescription, newBuilder, classLoader, context);
                    if (possibleNewBuilder != null) {
                        newBuilder = possibleNewBuilder;
                    }
                }
                if (context.isEnhanced()) {
                    logger.debug("Finish the prepare stage for {}.", typeDescription.getName());
                }

                //返回修改后的字节码,做替换
                return newBuilder;
            }

            logger.debug("Matched class {}, but ignore by finding mechanism.", typeDescription.getTypeName());
            return builder;
        }
    }
    

重点在于 DynamicType.Builder<?> possibleNewBuilder = define.define(ypeDescription, newBuilder, classLoader, context); .进入方法 define

 public DynamicType.Builder<?> define(TypeDescription typeDescription, DynamicType.Builder<?> builder,
        ClassLoader classLoader, EnhanceContext context) throws PluginException {
        //拿到插件的全路径
        String interceptorDefineClassName = this.getClass().getName();
        //拿到目标类的全路径
        String transformClassName = typeDescription.getTypeName();
        if (StringUtil.isEmpty(transformClassName)) {
            logger.warn("classname of being intercepted is not defined by {}.", interceptorDefineClassName);
            return null;
        }

        logger.debug("prepare to enhance class {} by {}.", transformClassName, interceptorDefineClassName);

        //一些插件需要依赖一些外部的类,他会找是否有对应依赖的类,如果没有就直接 return null,不再去修改字节码了
        String[] witnessClasses = witnessClasses();
        if (witnessClasses != null) {
            for (String witnessClass : witnessClasses) {
                if (!WitnessClassFinder.INSTANCE.exist(witnessClass, classLoader)) {
                    logger.warn("enhance class {} by plugin {} is not working. Because witness class {} is not existed.", transformClassName, interceptorDefineClassName, witnessClass);
                    return null;
                }
            }
        }
        
        //去修改原来的class,在对应的方法上添加一个拦截器
        DynamicType.Builder<?> newClassBuilder = this.enhance(typeDescription, builder, classLoader, context);

        context.initializationStageCompleted();
        logger.debug("enhance class {} by {} completely.", transformClassName, interceptorDefineClassName);

        return newClassBuilder;
    }

代码都很简单,接着进入 this.enhance(typeDescription, builder, classLoader, context) 去正真修改字节码

 protected DynamicType.Builder<?> enhance(TypeDescription typeDescription, DynamicType.Builder<?> newClassBuilder,
        ClassLoader classLoader, EnhanceContext context) throws PluginException {
        //去增强静态方法
        newClassBuilder = this.enhanceClass(typeDescription, newClassBuilder, classLoader);
        //去增强实例方法
        newClassBuilder = this.enhanceInstance(typeDescription, newClassBuilder, classLoader, context);

        return newClassBuilder;
    }

这里我们只看 this.enhanceInstance(typeDescription, newClassBuilder, classLoader, context) 2个方法其实都差不多

接下来终于到底,没有套娃了,因为方法比较长,我删减了部分代码,为了让读者能看懂主要逻辑

private DynamicType.Builder<?> enhanceInstance(TypeDescription typeDescription,
        DynamicType.Builder<?> newClassBuilder, ClassLoader classLoader,
        EnhanceContext context) throws PluginException {
        //目标类 在执行构造器之前,可能会去执行一些其他的方法,要执行的方法是什么,给找出来放入数组,其实就是 aop构造器
        ConstructorInterceptPoint[] constructorInterceptPoints = getConstructorsInterceptPoints();
        //目标类 在执行方法的时候,可能会去执行一些其他的方法,要执行的方法是什么,给找出来放入数组
        InstanceMethodsInterceptPoint[] instanceMethodsInterceptPoints = getInstanceMethodsInterceptPoints();
        //目标类 的全路径
        String enhanceOriginClassName = typeDescription.getTypeName();

        //下面都是设置一些标识,用来判断后续是否增强,比如上面的 constructorInterceptPoints和instanceMethodsInterceptPoints 都没有增强方法,那么就直接结束整个方法了
        boolean existedConstructorInterceptPoint = false;
        boolean existedMethodsInterceptPoints = false;
        if (constructorInterceptPoints != null && constructorInterceptPoints.length > 0) {
            existedConstructorInterceptPoint = true;
        }
        
        if (instanceMethodsInterceptPoints != null && instanceMethodsInterceptPoints.length > 0) {
            existedMethodsInterceptPoints = true;
        }

        /**
         * nothing need to be enhanced in class instance, maybe need enhance static methods.
         */
        if (!existedConstructorInterceptPoint && !existedMethodsInterceptPoints) {
            return newClassBuilder;
        }


        //会向 目标类里面 写入一个属性 叫做 _$EnhancedClassField_ws,类型Object
        //然后会让目标类去实现接口 EnhancedInstance, 实现这个接口的意义就是在于可以去获取 _$EnhancedClassField_ws 属性,可以理解成 getter/setter 了_$EnhancedClassField_ws
        //这个 context 前面提到过时用来 控制整个流程,防止流程多次执行,所以一个类只会写入一次 _$EnhancedClassField_ws 属性,就算有多个插件作用于了同一个 目标类, 也只会执行一次
        if (!context.isObjectExtended()) {
            newClassBuilder = newClassBuilder.defineField(CONTEXT_ATTR_NAME, Object.class, ACC_PRIVATE | ACC_VOLATILE)
                                             .implement(EnhancedInstance.class)
                                             .intercept(FieldAccessor.ofField(CONTEXT_ATTR_NAME));
            context.extendObjectCompleted();
        }

        //去增强 构造器,也就是把上面 constructorInterceptPoints 中定义好的 增强方法给代理到构造器上面
        if (existedConstructorInterceptPoint) {
            for (ConstructorInterceptPoint constructorInterceptPoint : constructorInterceptPoints) {
                //这里被我删减了,就是用 constructorInterceptPoint去操作 newClassBuilder 对象修改字节码
            }
        }

        //去增强 方法 把instanceMethodsInterceptPoints 中定义好的 增强方法给代理到指定方法上面
        if (existedMethodsInterceptPoints) {
            for (InstanceMethodsInterceptPoint instanceMethodsInterceptPoint : instanceMethodsInterceptPoints) {
                //这里被我删减了,就是用 instanceMethodsInterceptPoint去操作 newClassBuilder 对象修改字节码
            }
        }

        return newClassBuilder;
    }

其实说白了 上述 步骤就是 3步

  1. 向目标类写入一个属性 _$EnhancedClassField_ws ,然后让目标类实现接口EnhancedInstance 让他有set/get _$EnhancedClassField_ws属性的能力,具体这个属性有什么用,后面系列会讲到,这里就不做过多说明了,然后我们来验证一下是否真的写入了属性 _$EnhancedClassField_ws,
    我这里有一个非常简单的 目标类,我加载了spring-mvc 插件,所有打了 @Controller的类都会成为目标类
@Controller
public class TestController {

   @RequestMapping("/wx/chy")
   public String test(){
       return "hehe";
   }

}

然后我打上断点在 return "hehe" 访问 /wx/chy 进入断点
使用反射可以看到
在这里插入图片描述
确实有一个 叫做 _$EnhancedClassField_ws 的属性

  1. 给构造器设置代理方法,代理方法来自 getConstructorsInterceptPoints()
  2. 给实例方法设置代理方法,代理方法来自 getInstanceMethodsInterceptPoints()

这边我们大概看下 spring-mvc 插件的 getInstanceMethodsInterceptPoints() 方法

public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
        return new InstanceMethodsInterceptPoint[] {
            new DeclaredInstanceMethodsInterceptPoint() {
                @Override
                public ElementMatcher<MethodDescription> getMethodsMatcher() {
                    return byMethodInheritanceAnnotationMatcher(named("org.springframework.web.bind.annotation.RequestMapping"));
                }

                @Override
                public String getMethodsInterceptor() {
                    return "org.apache.skywalking.apm.plugin.spring.mvc.commons.interceptor.RequestMappingMethodInterceptor";
                }

                @Override
                public boolean isOverrideArgs() {
                    return false;
                }
            },
            new DeclaredInstanceMethodsInterceptPoint() {
                @Override
                public ElementMatcher<MethodDescription> getMethodsMatcher() {
                    return byMethodInheritanceAnnotationMatcher(named("org.springframework.web.bind.annotation.GetMapping"))
                        .or(byMethodInheritanceAnnotationMatcher(named("org.springframework.web.bind.annotation.PostMapping")))
                        .or(byMethodInheritanceAnnotationMatcher(named("org.springframework.web.bind.annotation.PutMapping")))
                        .or(byMethodInheritanceAnnotationMatcher(named("org.springframework.web.bind.annotation.DeleteMapping")))
                        .or(byMethodInheritanceAnnotationMatcher(named("org.springframework.web.bind.annotation.PatchMapping")));
                }

                @Override
                public String getMethodsInterceptor() {
                    return "org.apache.skywalking.apm.plugin.spring.mvc.commons.interceptor.RequestMappingMethodInterceptor";
                }

                @Override
                public boolean isOverrideArgs() {
                    return false;
                }
            }
        };
    }

整个方法非常的简单,就是把打了注解 @RequestMapping 的方法给加上一个代理方法 RequestMappingMethodInterceptor

由上可见,要自定义个skywalking 插件非常简单,只要继承抽象类 AbstractClassEnhancePluginDefine 然后去实现 里面的 getConstructorsInterceptPoints() , getInstanceMethodsInterceptPoints() 等方法就行

这里还是以 spring-mvc 插件为例
在这里插入图片描述

4 . 结尾

到此,整个 skywalkingAgent插件 的加载流程就结束,到此为止,仅仅是揭露了 skywalking 如何去加载插件,如何去修改应用的字节码. 但是和分布式链路追踪 好像还没有半毛钱关系,具体skywalkingAgent插件 如何去记录调用信息,如何跨进程传递数据,如何发送数据给skywalkingServer 请看下回分解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值