在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
应为篇幅的关系,这里就不去赘述了,不了解的同学可以去看看
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
是一个抽象方法,他需要 插件开发者
去重写这个方法,
这里我们可以看到
ControllerInstrumentation
是 AbstractControllerInstrumentation
的子类之一,
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步
- 向目标类写入一个属性
_$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
的属性
- 给构造器设置代理方法,代理方法来自
getConstructorsInterceptPoints()
- 给实例方法设置代理方法,代理方法来自
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
请看下回分解