首页 > 编程语言 >Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析

时间:2022-10-07 22:06:22浏览次数:69  
标签:插件 Agent 源码 agent apm new Skywalking skywalking

3 Skywalking源码导入

接上文,已经学习了Skywalking的应用,接下来我们将剖析Skywalking源码,深度学习Skywalking Agent。

3.1 源码环境搭建

当前最新版本是8.3.0,我们首先找到8.3.0的版本,然后下载并导入到IDEA,下载地址

​github.com/apache/skyw…​

1、推荐大家将github仓库拷贝到码云上,以提升下载速度

2、为了避免clone过程出错,可以设置git的全局参数:​​git config --global core.longpaths true​​​避免出现​​Filename too long的报错信息​

1)下载工程

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_git

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_微服务_02

这个过程比较耗时间,需要大家耐心等待!

2)切换版本

将Skywalking工程加入到Maven工程中,我们用的是当前最新版本8.3.0,因此需要切换版本:

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_java_03

项目导入IDEA后,会从指定路径加载项目,我们需要在skywalking的pom.xml中配置项目的工路径,添加如下properties配置即可:

<maven.multiModuleProjectDirectory>C:\developer\WorkSpace\skywalking</maven.multiModuleProjectDirectory>

pom中有一个插件​​maven-enforcer-plugin​​要求的maven的版本是3.6以上,需要注意!!!

我们接下来获取skywalking子模块的源码,需要在工程中执行如下命令:

git submodule init
git submodule update

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_git_04

该步骤非常重要,不完整执行成功,后续的编译会失败。git submodule update执行很慢,还可能中途中断

编译项目,此时会生成一些类​​skywalking\apm-protocol\apm-network\target\generated-sources\protobuf\java\org\apache\skywalking\apm\network\common\v3​​目录下的类如下图:

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_git_05

接下来把生成的文件添加到类路径下,如下图:

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_git_06

除了上面这里,还有很多个地方都需要这么操作,我们执行​​OAPServerStartUp​​的main方法启动Skywalking,只要执行找不到类,就找下有没有任何编译后生成的类没在类路径下,都把他们设置为类路径即可。

安装项目,Skywalking依赖的插件特别多,因此依赖的包也特别多,我们把Skywalking安装到本地,会耗费很长时间,但不要担心,因为迟早会安装完成,如下图:

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_maven_07

如果通过以上方式实在构建不了源码,也可尝试通过如下方式来,

官方提供了关于如何构建的步骤可以参阅:

​github.com/apache/skyw…​

社区中文版:​​skyapm.github.io/document-cn…​

1、通过命令拉取源码

git clone -b v8.3.0 --recurse-submodules https://gitee.com/giteets/skywalking.git

构建过程中遇到的最大问题是:git submodule 子模块的源码构建不出来,整体项目拉取下来后也可通过如下命令再次拉取子模块源码

git submodule init git submodule update

如果实在不行:在项目下有个​​.gitmodules​​文件,定义了子模块的仓库地址和应该安装到什么目录下

[submodule "apm-protocol/apm-network/src/main/proto"] path = apm-protocol/apm-network/src/main/proto url = https://github.com/apache/skywalking-data-collect-protocol.git [submodule "oap-server/server-query-plugin/query-graphql-plugin/src/main/resources/query-protocol"] path = oap-server/server-query-plugin/query-graphql-plugin/src/main/resources/query-protocol url = https://github.com/apache/skywalking-query-protocol.git [submodule "skywalking-ui"] path = skywalking-ui url = https://github.com/apache/skywalking-rocketbot-ui.git [submodule "test/e2e/e2e-protocol/src/main/proto"] path = test/e2e/e2e-protocol/src/main/proto url = https://github.com/apache/skywalking-data-collect-protocol.git

实在不行,就手动将这四个子模块分别手动下载到指定的path目录下,注意版本

2、将项目导入到idea,要求jkd8,maven3.6

3、在项目的​​pom.xml​​​中添加​​properties​

<maven.multiModuleProjectDirectory>C:\developer\WorkSpace\skywalking</maven.multiModuleProjectDirectory>

4、​​clean​​​,​​package​​​,​​install​​,注意跳过测试

5、参考社区文档,设置idea,将生成的源代标记成​​Sources Root​

设置 生成的源代码(Generated Source Code)目录.

  • apm-protocol/apm-network/target/generated-sources/protobuf目录下的​​grpc-java​​​ 和​​java​​ 目录
  • oap-server/server-core/target/generated-sources/protobuf目录下的​​grpc-java​​​ 和​​java​​ 目录
  • oap-server/server-receiver-plugin/receiver-proto/target/generated-sources/protobuf目录下的​​grpc-java​​​ 和​​java​
  • oap-server/exporter/target/generated-sources/protobuf目录下的​​grpc-java​​​ 和​​java​
  • oap-server/server-configuration/grpc-configuration-sync/target/generated-sources/protobuf目录下的​​grpc-java​​​ 和​​java​

3.2 模块分析

apm-application-toolkit:常用的工具工程,例如:log4j、log4j2、logback 等常见日志框架的接入接口,Kafka轮询调用注解,apm-application-toolkit 模块类似于暴露 API 定义,对应的处理逻辑在 apm-sniffer/apm-toolkit-activation 模块中实现,如下图:

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_git_08

apm-commons:SkyWalking 的公共组件和工具类。如下图所示,其中包含两个子模块,apm-datacarrier 模块提供了一个生产者-消费者模式的缓存组件(DataCarrier),无论是在 Agent 端还是 OAP 端都依赖该组件。apm-util 模块则提供了一些常用的工具类,例如,字符串处理工具类(StringUtil)、占位符处理的工具类(PropertyPlaceholderHelper、PlaceholderConfigurerSupport)等等。

apache-skywalking-apm:SkyWalking 打包后使用的命令文件都在此目录中,例如,前文启动 OAP 和 SkyWalking Rocketbot 使用的 startup.sh 文件。

apm-protocol:该模块中只有一个 apm-network 模块,我们需要关注的是其中定义的 .proto 文件,定义 Agent 与后端 OAP 使用 gRPC 交互时的协议。

apm-sniffer:agent核心功能以及agent依赖插件,模块比较多:

apm-agent:只有一个类SkyWalkingAgent,是Skywalking的agent入口。

apm-agent-core:看名字我们就知道它是Skywalking agent核心模块。

apm-sdk-plugin:该模块下包含了 SkyWalking Agent 的全部插件。

apm-toolkit-activation:apm-application-toolkit 模块的具体实现。

apm-test-tools:Skywalking的测试功能。

bootstrap-plugins:该插件主要提供了Http和多线程相关的功能支持,它里面有2个子工程。

optional-plugins:可选插件,例如对spring支持、对kotlin支持等,它下面有多个插件工程实现。

optional-reporter-plugins:该工程插件主要提供一些数据报告,集成了Kafka功能。

apm-webapp:SkyWalking Rocketbot 对应的后端。

oap-server:oap 主程序,该工程中有多个模块,我们对核心模块进行说明:

analyzer:数据分析工程,例如对内存分析、仪表盘分析报告等,它下面有2个子工程。

exporter:导出数据功能。

oal-grammar:操作适配语法,例如SQL语法。

oal-rt:操作解析器,上面提供了语法,该工程提供对操作解析功能。

server-alarm-plugin:负责实现 SkyWalking 的告警功能。

server-cluster-plugin:OAP集群管理功能,提供了很多第三方介入的组件。

server-configuration:负责管理 OAP 的配置信息,也提供了接入多种配置管理组件的相关插件。

server-core:SkyWalking OAP的核心实现都在该模块中。

server-library:OAP 以及 OAP 各个插件依赖的公共模块,其中提供了双队列 Buffer、请求远端的 Client 等工具类,这些模块都是对立于 SkyWalking OAP 体系之外的类库,我们可以直接拿着使用。

server-query-plugin:SkyWalking Rocketbot 发送的请求首先由该模块接收处理,目前该模块只支持 GraphQL 查询。

server-receiver-plugin:SkyWalking Agent 发送来的 Metrics、Trace 以及 Register 等写入请求都是首先由该模块接收处理的,不仅如此,该模块还提供了多种接收其他格式写入请求的插件。

server-starter:OAP 服务启动的入口。

server-storage-plugin:OAP 服务底层可以使用多种存储来保存 Metrics 数据以及Trace 数据,该模块中包含了接入相关存储的插件。

skywalking-agent:SkyWalking Agent 编译后生成的 jar 包都会放到该目录中。

skywalking-ui:SkyWalking Rocketbot 的前端。

4 Skywalking Agent 启动流程剖析

我们已经学习了Skywalking常用操作,并且讲解了Java Agent,而且Skywalking Agent就是基于Java Agent研发而来,我们接下来深入学习Skywalking Agent架构、原理、常用组件。

4.1 Skywalking Agent架构

我们在学习Skywalking之前,先了解一下微内核架构,如下图:

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_maven_09

微内核架构(Microkernel Architecture),也被成为插件化架构(Plug-in Architecture),是一种面向功能进行拆分的可扩展性架构,通常用于实现基于产品(原文为product-based,指存在多个版本,需要下载安装才能使用,与web-based想对应)的应用。

微内核架构的好处:

1:测试成本下降。从软件工程的角度看,微内核架构将变化的部分和不变的部分拆分,降低了测试的成本,符合设计模式中的开放封闭原则。
2:稳定性。由于每个插件模块相对独立,即使其中一个插件有问题,也可以保证内核系统以及其他插件的稳定性。
3:可扩展性。在增加新功能或接入新业务的时候,只需要新增相应插件模块即可;在进行历史功能下线时,也只需删除相应插件模块即可。

微内核的核心系统设计的关键技术有:插件管理,插件连接 和 插件通信。

SkyWalking Agent 采用了微内核架构(Microkernel Architecture),是一种面向功能进行拆分的可扩展性架构。

apm-agent-core:是Skywalking Agent的核心模块
apm-sdk-plugin:是Skywalking需要的各个插件模块

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_maven_10

4.2 Skywalking Agent启动流程

1)启动OAP

我们接下来启动Skywalking oap,我们在​​oap-server\server-starter​​​或者​​oap-server\server-starter-es7​​​中找到​​OAPServerStartUp​​​类,执行该类的main方法即可启动,但默认用的是H2存储,如果希望用elasticsearch存储,需要修改被调用的服务​​server-bootstrap​​​的配置文件​​application.yml​​配置elasticsearch位置:

storage:
#selector: ${SW_STORAGE:h2}
selector: ${SW_STORAGE:elasticsearch7}
elasticsearch7:
nameSpace: ${SW_NAMESPACE:""}
#clusterNodes: ${SW_STORAGE_ES_CLUSTER_NODES:localhost:9200}
clusterNodes: ${SW_STORAGE_ES_CLUSTER_NODES:192.168.211.145:9200}
........略

存储直接使用上一次课准备好的es7的存储即可。

执行​​OAPServerStartUp​​的main方法不报错就没问题。

2)启动SkyWalking Rocketbot

apm-webapp 是 Spring Boot 的 Web项目,执行 ApplicationStartUp 中的 main() 方法。正常启动之后,

访问 ​​http://localhost:8080,看到​​ SkyWalking Rocketbot 的 UI 界面即为启动成功。

如果修改启动端口,可以直接修改application.yml即可。

3)直接使用源码中的Agent

项目打包会生成​​skywalking-agent​​​目录,里面有​​skywalking-agent.jar​​,如下图:

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_微服务_11

我们来使用一下前面源码工程中打包生成的​​skywalking-agent.jar​​,复制该jar包的路径

找到​​hailtaxi-parent​​​项目,修改​​-javaagent​​参数如下

​hailtaxi-gateway​

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_java_12

-javaagent:C:\developer\WorkSpace\sources\skywalking\skywalking-agent\skywalking-agent.jar
-Dskywalking_config=C:\developer\WorkSpace\sources\skywalking\skywalking-agent\config\agent.config
-Dskywalking.agent.service_name=hailtaxi-gateway

​hailtaxi-driver​​​和​​hailtaxi-order​​进行相同配置即可!

全都启动后,查看​​Skywalking Rocketbot​​ :本地启动,需要等待一定的时间

5 Skywalking Agent源码剖析

1、创建​​sw-agent-debugger​​项目:一个普通的springboot项目即可

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_git_13

2、添加启动​​-javaagent​​参数

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_Java_14

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_Java_15

启动的整个方法执行流程如下:

public static void premain(String agentArgs, Instrumentation instrumentation) throws PluginException {
final PluginFinder pluginFinder;
try {
//初始化加载 agent.config 配置文件,其中会检测 Java Agent 参数以及环境变量是否覆盖了相应配置项
SnifferConfigInitializer.initializeCoreConfig(agentArgs);
} catch (Exception e) {
// try to resolve a new logger, and use the new logger to write the error log here
LogManager.getLogger(SkyWalkingAgent.class)
.error(e, "SkyWalking agent initialized failure. Shutting down.");
return;
} finally {
// refresh logger again after initialization finishes
LOGGER = LogManager.getLogger(SkyWalkingAgent.class);
}

try {
//管理插件
pluginFinder = new PluginFinder(new PluginBootstrap().loadPlugins());
} 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创建AgentBuilder
final ByteBuddy byteBuddy = new ByteBuddy().with(TypeValidation.of(Config.Agent.IS_OPEN_DEBUGGING_CLASS));

//忽略拦截配置
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();
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;
}

if (Config.Agent.IS_CACHE_ENHANCED_CLASS) {
try {
agentBuilder = agentBuilder.with(new CacheableTransformerDecorator(Config.Agent.CLASS_CACHE_MODE));
LOGGER.info("SkyWalking agent class cache [{}] activated.", Config.Agent.CLASS_CACHE_MODE);
} catch (Exception e) {
LOGGER.error(e, "SkyWalking agent can't active class cache.");
}
}

//Java Agent创建代理流程
agentBuilder.type(pluginFinder.buildMatch())
.transform(new Transformer(pluginFinder))
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with(new Listener())
.installOn(instrumentation);

try {
//使用 JDK SPI加载的方式并启动 BootService 服务。
ServiceManager.INSTANCE.boot();
} catch (Exception e) {
LOGGER.error(e, "Skywalking agent boot failure.");
}
//添加一个JVM钩子
Runtime.getRuntime()
.addShutdownHook(new Thread(ServiceManager.INSTANCE::shutdown, "skywalking service shutdown thread"));
}

我们总结一下Skywalking Agent启动流程:

1:初始化配置信息。该步骤中会加载 agent.config 配置文件,其中会检测 Java Agent 参数以及环境变量是否覆盖了相应配置项。
2:查找并解析 skywalking-plugin.def 插件文件。
3:AgentClassLoader 加载插件。
4:PluginFinder 对插件进行分类管理。
5:使用 Byte Buddy 库创建 AgentBuilder。这里会根据已加载的插件动态增强目标类,插入埋点逻辑。
6:使用 JDK SPI 加载并启动 BootService 服务。
7:添加一个 JVM 钩子,在 JVM 退出时关闭所有 BootService 服务。

这是​​org.apache.skywalking.apm.agent.SkyWalkingAgent#premain​​的主体工作流程

5.1 配置初始化

-javaagent:D:/project/skywalking/skywalking/apm-sniffer/apm-agent/target/skywalking-agent.jar
-Dskywalking_config=D:/project/skywalking/hailtaxi-parent/hailtaxi-driver/src/main/resources/agent.config
-Dskywalking.collector.backend_service=127.0.0.1:11800

启动driver服务的时候,会指定skywalking-agent.jar路径,同时会指定​​agent.config​​​配置文件路径,如上配置,此时需要初始化加载该文件,加载流程可以从启动类​​SkyWalkingAgent.premain()​​方法找答案。

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_maven_16

加载解析文件的时候,permain()方法会调用initializeCoreConfig(String agentOptions)方法,并解析agent.config文件,并将文件内容存入到Properties中,此时加载是按照${配置项名称:默认值}的格式解析各个配置,如下图:

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_Java_17

loadConfig() 方法会优先根据环境变量(skywalking_config)指定的 agent.config 文件路径加载。若环境变量未指定 skywalking_ config 配置,则到 skywalking-agent.jar 同级的 config 目录下查找 agent.confg 配置文件。

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_git_18

解析前后的数据也是不一致的,如下图:

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_Java_19

overrideConfigBySystemProp() 方法中会遍历环境变量(即 System.getProperties() 集合),如果环境变 是以 "skywalking." 开头的,则认为是 SkyWalking 的配置,同样会填充到 Config 类中,以覆盖 agent.config 中的默认值。如下图:

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_maven_20

ConfigInitializer 工具类,将配置信息填充到 Config 中的静态字段中,SkyWalking Agent 启动所需的全部配置都已经填充到 Config 中,后续使用配置信息时直接访问 Config 中的相应静态字段即可。

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_java_21

Config结构:

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_maven_22

Config中Agent类的​​SERVICE_NAME​​对应agent.config中的agent.service_name=${xxx}

Config中Collector类的​​BACKEND_SERVICE​​对应agent.config中的agent.backend_service=${xxx}

5.2 插件加载

加载插件执行流程:

1:new PluginBootstrap()
2:PluginBootstrap().loadPlugins()
3:AgentClassLoader.initDefaultLoader(); 没有指定类加载器的时候使用PluginBootstrap.ClassLoader
4:创建PluginResourcesResolver插件加载解析器
5:将解析的插件存到List<PluginDefine> pluginClassList,此时只存储了插件的名字和类路径
6:创建插件实例
7:将所有插件添加到Skywalking内核中

插件加载流程如下:

在​​SkyWalkingAgent.premain()​​方法中会执行插件加载,如下代码:

pluginFinder = new PluginFinder(new PluginBootstrap().loadPlugins());

加载插件的全部详细代码如下:

public class PluginBootstrap {
private static final ILog LOGGER = LogManager.getLogger(PluginBootstrap.class);

/**
* 加载所有插件
* load all plugins.
* @return
public List<AbstractClassEnhancePluginDefine> loadPlugins() throws AgentPackageNotFoundException {
//初始化AgentClassLoader
AgentClassLoader.initDefaultLoader();

//创建PluginResourcesResolver插件加载解析器
PluginResourcesResolver resolver = new PluginResourcesResolver();
//获取插件路径
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>();
}

//循环加载插件路径
for (URL pluginUrl : resources) {
try {
//插件会存到List<PluginDefine> pluginClassList,PluginDefine中只有插件名字和插件类路径
PluginCfg.INSTANCE.load(pluginUrl.openStream());
} catch (Throwable t) {
LOGGER.error(t, "plugin file [{}] init failure.", pluginUrl);
}
}
//获取解析的插件集合
List<PluginDefine> pluginClassList = PluginCfg.INSTANCE.getPluginClassList();

List<AbstractClassEnhancePluginDefine> plugins = new ArrayList<AbstractClassEnhancePluginDefine>();
//循环所有插件
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

SkyWalking Agent 加载插件时使用到一个自定义的 ClassLoader —— AgentClassLoader,之所以自定义类加载器,目的是不在应用的 Classpath 中引入 SkyWalking 的插件 jar 包,这样就可以让应用无依赖、无感知的插件。

AgentClassLoader 作为一个类加载器,主要工作还是从其 Classpath 下加载类(或资源文件),对应的就是其 findClass() 方法和 findResource() 方法:

我们来看一下findClass,主要根据类名获取它的Class:

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//扫描classpath所有的jar包
List<Jar> allJars = getAllJars();
//把包替换成路径,最后加上.class
String path = name.replace('.', '/').concat(".class");

//循环查找所有的jar包
for (Jar jar : allJars) {
//加载jar包的信息
JarEntry entry = jar.jarFile.getJarEntry(path);
if (entry == null) {
continue;
}
try {
//定位当前jar包位置
URL classFileUrl = new URL("jar:file:" + jar.sourceFile.getAbsolutePath() + "!/" + path);
//加载jar包
byte[] data;
try (final BufferedInputStream is = new BufferedInputStream(
classFileUrl.openStream()); final ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
int ch;
while ((ch = is.read()) != -1) {
baos.write(ch);
}
data = baos.toByteArray();
}
//返回当前对象的Class
return processLoadedClass(defineClass(name, data, 0, data.length));
} catch (IOException e) {
LOGGER.error(e, "find class fail.");
}
}
throw new ClassNotFoundException("Can't find "

findResource()方法主要获取文件路径,换句话理解,就是获取插件路径,我们来看下方法:

@Override
protected URL findResource(String name) {
//扫描classpath所有的jar包
List<Jar> allJars = getAllJars();
//循环查找所有的jar包
for (Jar jar : allJars) {
//加载jar包的信息
JarEntry entry = jar.jarFile.getJarEntry(name);
if (entry != null) {
try {
//获取jar包的路径
return new URL("jar:file:" + jar.sourceFile.getAbsolutePath() + "!/" + name);
} catch (MalformedURLException ignored) {
}
}
}
return null;
}

5.3 解析插件

我们在学习插件解析之前,先看看插件是如何定义的。我们可以打开​​apm-sniffer/apm-sdk-plugin​​,它里面都是要用到的插件集合:

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_java_23

我们看看​​mysql-5.x-plugin​​,在resources(也就是classpath)中定义skywalking-plugin.def文件,在该文件中定义加载插件需要解析的类,而插件类以key=value形式定义,如下图:

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_Java_24

5.3.1 PluginResourcesResolver

在​​loadPlugins()​​​方法中使用了​​PluginResourcesResolver​​​,​​PluginResourcesResolver​​​是 Agent 插件的资源解析器,会通过 ​​AgentClassLoader​​​ 中的 ​​findResource()​​​ 方法读取所有 ​​Agent​​​ 插件中的 ​​skywalking-plugin.def​​ 文件。

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_java_25

拿到全部插件的 ​​skywalking-plugin.def​​​ 文件之后,PluginCfg 会逐行进行解析,转换成 PluginDefine 对象。PluginDefine 中有两个字段,分别对应​​skywalking-plugin.def​​中的key和value,解析流程如下:

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_Java_26

接下来会遍历全部 PluginDefine 对象,通过反射将其中 defineClass 字段中记录的插件类实例化,核心逻辑如下:

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_微服务_27

​AbstractClassEnhancePluginDefine​​ 抽象类是所有 Agent 插件类的顶级父类,其中定义了四个核心方法,决定了一个插件类应该增强哪些目标类、应该如何增强、具体插入哪些逻辑,如下所示:

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_java_28

  • enhanceClass()方法:返回的 ClassMatch,用于匹配当前插件要增强的目标类。
  • define()方法:插件类增强逻辑的入口,底层会调用下面的 enhance() 方法和 witnessClass() 方法。
  • enhance()方法:真正执行增强逻辑的地方。
  • witnessClass()方法:一个开源组件可能有多个版本,插件会通过该方法识别组件的不同版本,防止对不兼容的版本进行增强。

ClassMatch

enhanceClass() 方法决定了一个插件类要增强的目标类,返回值为 ClassMatch 类型对象。ClassMatch 类似于一个过滤器,可以通过多种方式匹配到目标类,ClassMatch 接口的实现如下:

Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析_maven_29

  • **NameMatch:**根据其 className 字段(String 类型)匹配目标类的名称。
  • IndirectMatch:子接口中定义了两个方法。
public interface IndirectMatch extends ClassMatch {
//Junction是Byte Buddy中的类,可以通过and、or等操作串联多个ElementMatcher,进行匹配
ElementMatcher.Junction buildJunction();
//用于检测传入的类型是否匹配该Match
boolean isMatch(TypeDescription typeDescription);
}
  • MultiClassNameMatch:其中会指定一个 matchClassNames 集合,该集合内的类即为目标类。
  • ClassAnnotationMatch:根据标注在类上的注解匹配目标类。
  • MethodAnnotationMatch:根据标注在方法上的注解匹配目标类。
  • HierarchyMatch:根据父类或是接口匹配目标类。

我们来分析一下ClassAnnotationMatch的buildJunction()方法和isMatch()方法:

@Override
public ElementMatcher.Junction buildJunction() {
ElementMatcher.Junction junction = null;
//annotations:指定了该 ClassAnnotationMatch 对象需要检查的注解
//遍历该对象需要检查的所有注解
for (String annotation : annotations) {
if (junction == null) {
//检测类是否标注了指定注解
junction = buildEachAnnotation(annotation);
} else {
//使用 and 方式将所有Junction对象连接起来
junction = junction.and(buildEachAnnotation(annotation));
}
}
// 排除接口
junction = junction.and(not(isInterface()));
return

isMatch()方法如下:

@Override
public boolean isMatch(TypeDescription typeDescription) {
List<String> annotationList = new ArrayList<String>(Arrays.asList(annotations));
// 获取该类上的注解
AnnotationList declaredAnnotations = typeDescription.getDeclaredAnnotations();
// 匹配一个删除一个
for (AnnotationDescription annotation : declaredAnnotations) {
annotationList.remove(annotation.getAnnotationType().getActualName());
}
// 如果全部删除,则匹配成功
return

5.3.2 PluginFinder

PluginFinder 是 AbstractClassEnhancePluginDefine 查找器,可以根据给定的类查找用于增强的 AbstractClassEnhancePluginDefine 集合。

在 PluginFinder 的构造函数中会遍历前面课程已经实例化的 AbstractClassEnhancePluginDefine ,并根据 enhanceClass() 方法返回的 ClassMatcher 类型进行分类,得到如下两个集合:

//定义了集合
//pluginFinder将插件分类保存在两个集合中,分别是:按名字分类和按其他辅助信息分类
private final Map<String, LinkedList<AbstractClassEnhancePluginDefine>> nameMatchDefine = new HashMap<String, LinkedList<AbstractClassEnhancePluginDefine>>();
private final List<AbstractClassEnhancePluginDefine> signatureMatchDefine = new ArrayList<AbstractClassEnhancePluginDefine>();
private final List<AbstractClassEnhancePluginDefine> bootstrapClassMatchDefine = new ArrayList<AbstractClassEnhancePluginDefine>();

//构造方法
public PluginFinder(List<AbstractClassEnhancePluginDefine> plugins) {
for (AbstractClassEnhancePluginDefine plugin : plugins) {
//抽象方法enhanceClass方法定义在插件的抽象基类AbstractClassEnhancePluginDefine中,每一个插件必须去实现这个类中的方法
ClassMatch match = plugin.enhanceClass(); //故enhanceClass是每个插件都会自己去实现的方法,指定需要增强的类

if (match == null) {
continue;
}

if (match instanceof NameMatch) {
NameMatch nameMatch = (NameMatch) match;
LinkedList<AbstractClassEnhancePluginDefine> pluginDefines = nameMatchDefine.get(nameMatch.getClassName());
if (pluginDefines == null) {
pluginDefines = new LinkedList<AbstractClassEnhancePluginDefine>();
nameMatchDefine.put(nameMatch.getClassName(), pluginDefines);
}
pluginDefines.add(plugin);
} else {
signatureMatchDefine.add(plugin);
}

if (plugin.isBootstrapInstrumentation()) {
bootstrapClassMatchDefine.add(plugin);
}
}
}

//typeDescription是bytebuddy的内置接口,是对类的完整描述,包含了类的全类名
//传入typeDescription,返回可以运用于typeDescription的类的插件
public List<AbstractClassEnhancePluginDefine> find(TypeDescription typeDescription) {
List<AbstractClassEnhancePluginDefine> matchedPlugins = new LinkedList<AbstractClassEnhancePluginDefine>();
String typeName = typeDescription.getTypeName();
//根据名字信息匹配查找
if (nameMatchDefine.containsKey(typeName)) {
matchedPlugins.addAll(nameMatchDefine.get(typeName));
}
//通过除了名字之外的辅助信息,在signatureMatchDefine集合中查找
for (AbstractClassEnhancePluginDefine pluginDefine : signatureMatchDefine) {
IndirectMatch match = (IndirectMatch) pluginDefine.enhanceClass();
if (match.isMatch(typeDescription)) {
matchedPlugins.add(pluginDefine);
}
}

return matchedPlugins;
}

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

5.3.3 AgentBuilder

利用bytebuddy的API生成一个代理,并执行transform方法和监听器Listener(主要是日志相关)。

在premain中,通过链式调用,被builderMatch()匹配到的类都会执行transform方法,transform定义了字节码增强的逻辑:

//使用ByteBuddy创建AgentBuilder
final ByteBuddy byteBuddy = new ByteBuddy().with(TypeValidation.of(Config.Agent.IS_OPEN_DEBUGGING_CLASS));

Config.Agent.IS_OPEN_DEBUGGING_CLASS 在 agent.config 中对应配置​​agent.is_open_debugging_class​

如果将其配置为 true,则会将动态生成的类输出到 debugging 目录中。

AgentBuilder 是 Byte Buddy 库专门用来支持 Java Agent 的一个 API,如下所示:

new AgentBuilder.Default(byteBuddy) // 设置使用的ByteBuddy对象
.ignore(nameStartsWith("net.bytebuddy.")// 不会拦截下列包中的类
.or(nameStartsWith("org.slf4j."))
.or(nameStartsWith("org.apache.logging."))
.or(nameStartsWith("org.groovy."))
.or(nameContains("javassist"))
.or(nameContains(".asm."))
.or(nameStartsWith("sun.reflect"))
.or(allSkyWalkingAgentExcludeToolkit()) // 处理 Skywalking 的类
// synthetic类和方法是由编译器生成的,这种类也需要忽略
.or(ElementMatchers.<TypeDescription>isSynthetic()))
.type(pluginFinder.buildMatch())// 拦截
.transform(new Transformer(pluginFinder)) // 设置Transform
.with(new Listener()) // 设置Listener

上面代码中有些方法我们需要理解一下:

  • ignore() 方法:忽略指定包中的类,对这些类不会进行拦截增强。
  • type() 方法:在类加载时根据传入的 ElementMatcher 进行拦截,拦截到的目标类将会被 transform() 方法中指定的 Transformer 进行增强。
  • transform() 方法:这里指定的 Transformer 会对前面拦截到的类进行增强。
  • with() 方法:添加一个 Listener 用来监听 AgentBuilder 触发的事件。

首先, PluginFInder.buildMatch() 方法返回的 ElementMatcher 对象会将全部插件的匹配规则(即插件的 enhanceClass() 方法返回的 ClassMatch)用 OR 的方式连接起来,这样,所有插件能匹配到的所有类都会交给 Transformer 处理。

再来看 with() 方法中添加的监听器 —— SkywalkingAgent.Listener,它继承了 AgentBuilder.Listener 接口,当监听到 Transformation 事件时,会根据 IS_OPEN_DEBUGGING_CLASS 配置决定是否将增强之后的类持久化成 class 文件保存到指定的 log 目录中。注意,该操作是需要加锁的,会影响系统的性能,一般只在测试环境中开启,在生产环境中不会开启。

Skywalking.Transformer实现了 AgentBuilder.Transformer 接口,其 transform() 方法是插件增强目标类的入口。Skywalking.Transformer 会通过 PluginFinder 查找目标类匹配的插件(即 AbstractClassEnhancePluginDefine 对象),然后交由 AbstractClassEnhancePluginDefine 完成增强,核心实现如下:

public DynamicType.Builder<?> transform(DynamicType.Builder<?>builder,
TypeDescription typeDescription, // 被拦截的目标类
ClassLoader classLoader, // 加载目标类的ClassLoader
JavaModule module) {
// 从PluginFinder中查找匹配该目标类的插件,PluginFinder的查找逻辑不再重复
List<AbstractClassEnhancePluginDefine> pluginDefines =
pluginFinder.find(typeDescription);
if (pluginDefines.size() >0){
DynamicType.Builder<?>newBuilder = builder;
EnhanceContext context = new EnhanceContext();
for (AbstractClassEnhancePluginDefinedefine : pluginDefines) {
// AbstractClassEnhancePluginDefine.define()方法是插件入口,
// 在其中完成了对目标类的增强
DynamicType.Builder<?>possibleNewBuilder =
define.define(typeDescription,
newBuilder, classLoader,context);
if (possibleNewBuilder != null) {
// 注意这里,如果匹配了多个插件,会被增强多次
newBuilder = possibleNewBuilder;
}
}
return newBuilder;
}
return

思考:

1:如何自定义Skywalking插件

2:如何使用插件

标签:插件,Agent,源码,agent,apm,new,Skywalking,skywalking
From: https://blog.51cto.com/boxuegu/5735127

相关文章