暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

skywalking 居然对我的代码做了这样的事

未来技术站 2020-05-21
416


作者 | 曹恒源


为了防止这个世界被破坏,为了保护世界的和平,贯彻爱与真实的邪恶,可爱又迷人的程序员。




1 . 简介

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

2. javaAgent

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

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

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

在 JDK 1.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
 是一个抽象方法,他需要 插件开发者
 去重写这个方法, 这里我们可以看到 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步

  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
 请看下回分解。


全文完



以下文章您可能也会感兴趣:



我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。



文章转载自未来技术站,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论