1. 概述
做中间件的埋点插件,需要通过对源码梳理,找到关键拦截点,如什么类的什么方法,通过其参数、返回值、对象属性等获取构建Span
的数据信息。这节内容记录一下开发插件过程中一些零碎的事项。
通过继承ClassInstanceMethodsEnhancePluginDefine
,在子类的实现方法中完成以下逻辑:
- 指定待增强的类
@Override
protected ClassMatch enhanceClass() {
IndirectMatch hierarchyMatch = byHierarchyMatch(new String[]{ENHANCE_CLASS});
PrefixMatch prefixMatch = nameStartsWith(PAJK_PACKAGE_PREFIX);
return LogicalMatchOperation.and(hierarchyMatch, prefixMatch);
}
- 在构造函数中添加增强逻辑,可通过类名(构造方法同名),方法参数信息指定构造函数。
//这种方式,不做构造函数增强
@Override
public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
return new ConstructorInterceptPoint[0];
}
//指定构造方法
@Override
public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
return new ConstructorInterceptPoint[]{
new ConstructorInterceptPoint() {
@Override
public ElementMatcher getConstructorMatcher() {
//第二个参数必须是String
return takesArgumentWithType(2, "java.lang.String");
}
@Override
public String getConstructorInterceptor() {
return INTERCEPT_CLASS;//指定类名(构造方法名)
}
}
};
}
- 增强实例方法,通过
InstanceMethodsInterceptPoint
匹配将增强的方法
@Override
public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
return new InstanceMethodsInterceptPoint[]{
new InstanceMethodsInterceptPoint() {
@Override
public ElementMatcher getMethodsMatcher() {
return named(ENHANCE_METHOD);
}
@Override
public String getMethodsInterceptor() {
return INTERCEPTOR_CLASS;
}
@Override
public boolean isOverrideArgs() {
return false;
}
}
};
}
- 增强类(静态)方法,通过
StaticMethodsInterceptPoint
匹配将增强的类方法
@Override
public StaticMethodsInterceptPoint[] getStaticMethodsInterceptPoints() {
return new StaticMethodsInterceptPoint[0];
}
2. xxxMatch
匹配的技巧
在以上代码中可以看到许多xxxMatch,大致是2类:
-
ClassMatch
:用于匹配类 -
ElementMatcher
:用于匹配方法
2.1 ClassMatch
在源码中可看到ClassMatch
有如下这些实现类:
ClassAnnotationMatch
EitherInterfaceMatch
FailedCallbackMatch
HierarchyMatch
IndirectMatch
ListenableFutureCallbackMatch
LogicalAndMatch
LogicalOrMatch
MethodAnnotationMatch
MultiClassNameMatch
NameMatch
PrefixMatch
RegexMatch
SuccessCallbackMatch
这些匹配有按照类名字匹配、前缀匹配、继承关系、实现接口、组合与匹配、组合或匹配等等;可通过在源码中查找其应用来进一步理解其作用;另外需要注意,除了默认提供的这些,还是可以根据自己的需求来额外定制(通过实现接口IndirectMatch
)
2.2 ElementMatcher
系统api
在ElementMatchers
中已经定义了很多用于方法匹配的方法,因其方法太多,不在此列举,自行查看自定义matcher
return new ElementMatcher() {
@Override
public boolean matches(MethodDescription target) {
//自己编排逻辑
if(xxx) {
return true;
}
return false;
}
3. 上下文数据的传递
插件埋点所需要的信息,通常是需要再多个方法中分别捕获,我们需要通过某种上下文机制将这些分散的信息搜集整合起来;从线程角度说通常分为同步请求(相同线程内完成一次执行)和异步请求(不同的线程内完成一次请求)。
3.1 同步请求的上下文数据
- ThreadLocal方式,在Skywalking中已为我们提供了这种途径
ContextManager.getRuntimeContext()
;
- 存储数据
ContextManager.getRuntimeContext().put(key, data)
- 检查数据
ContextManager.getRuntimeContext().get(key)
,判断是否存在 - 清理数据
需要留意做善后清理数据ContextManager.getRuntimeContext().remove(key)
- Skywalking的扩展字段,Skywalking会在被增强的类中添加一个sw专用的属性,同时这个类会被修改,实现了接口
EnhancedInstance
,通过此接口中的2个方法来读写这个扩展属性
public interface EnhancedInstance {
Object getSkyWalkingDynamicField();
void setSkyWalkingDynamicField(Object value);
}
这个sw专用字段,就是一个普通的Object,可根据自己的需求给其赋值。
如这样一些使用场景:
- 拦截目标类的构造方法,在构造房中new 一个自定义类,通过
setSkyWalkingDynamicField
赋值给扩展字段 - 在其他方法中,捕获不同的数据,填充到这个对象的扩展属性里。
- 读取扩展属性中的数据,构造、填充
Span
3.2 异步请求的上下文数据
异步请求的场景下,因为跨越了线程,所以上下文需要在多个线程中传递,那么常规的ThreadLocal
方式就不可用了;通过SW扩展属性来承载数据,在多个线程中传递的方式非常方便了。
异步请求时 需要借助AsyncSpan#prepareForAsync
和 AsyncSpan#asyncFinish
来完成span的异步关闭,这里一个问题,异步跨线程的情况下,另外的线程里如何拿到这个span实例呢? 寻找在多个线程中都存在的对象,如果这个对象本身有承载额外数据的能力最好,如果没有,则需要增强这个类,借助sw的扩展字段来承载这个span,进而在异步结果处理中完成span的关闭。
异步请求的上下文的实例,通过在源码中搜索prepareForAsync
方法的使用来加深理解,这里用ExitSpan方式暂记一下关键逻辑和步骤:
- 发送请求的线程a,创建ExitSpan 实例 exitSpan,会对exitSpan做一些赋值
- 线程b中会获取当前请求的执行结果。
- 线程a 和线程b 之间一定会有一个对象,在两个线程之间都可访问,暂叫 externObj;
- 线程a,在发起异步请求之前,调用exitSpan的
prepareForAsync
方法,并把exitSpan装入externObj中;如果externObj没有合适的属性去承载exitSpan数据,就扩展这个externObj的类,比如使用skywalking的类增强或者继承externObj的类,达到增加属性字段的效果 - 线程b,在拿到响应结果后,从externObj的取出exitSpan,根据返回值 给 exitSpan做赋值,最后调用
asyncFinish
异步关闭span
4. SpanStack
1. EntrySpan 的SpanStack
服务接收请求时,创建EntrySpan
;tomcat和SpringMVC的插件都是创建 EntrySpan
,这样就重复创建了EntySpan
,这样没有意义,在sw中,只要最外层的EntrySpan
,通过一个计数器(stackDepth)和栈的结构来实现,大致流程如下:
- 请求进入1处,创建
EntrySpan
暂叫tomEntrySpan,stackDepth=1,记录开始时间 - 请求进入2处,创建
EntrySpan
时,stackDepth+1=2,会复用上一步创建的tomEntrySpan对象,而不是new 一个新的;同时会清理此span上的layer、logs和tags;保留springMVC层的layer、logs和tags。 - 请求进入3处,执行finish方法将stackDepth-- = 1;
- 请求进入4处,执行finish方法将stackDepth--=0,关闭tomEntrySpan,记录结束时间
2. ExitSpan 的SpanStack
假设上图tomcat
是作为调用外部请求的,创建ExitSpan
(这里只是借图举例)其逻辑流程却是这样:
- 请求进入1处,创建
ExitSpan
实例 tomExitSpan,stackDepth=1,记录开始时间;在这一层才可以记录layer、logs和tags。 - 请求进入2处,创建
ExitSpan
时,stackDepth+1=2,会复用上一步创建的tomExitSpan对象,而不是new 一个新的;这里layer、logs和tags设置无效。 - 请求进入3处,执行finish方法将stackDepth-- = 1;
- 请求进入4处,执行finish方法将stackDepth--=0,关闭tomExitSpan,记录结束时间。
3. 方法的重入
比如在ExitSpan
场景下中,A类的put1和put2方法都被拦截,并且put1方法 调用了put2方法,在上述的SpanStack的机制里,由于put2方法中stackDepth=2,其内所处理的layer、logs和tags是无效的;对于这种情况,大概有2中方法处理:
- 当stackDepth!=1 的时候,通过上下文记录layerlayer、logs和tags的信息,等stackDepth=1时,从上下文中取出layerlayer、logs和tags的信息赋值给span。
- 控制span的创建时机,自行在上下文中增加stackDepth的计数控制,当stackDepth>1时,不在创建ExitSpan。即put1中有一个ExitSpan,put2中不创建
ExitSpan
,那么stackDepth一直=1,layerlayer、logs和tags的信息可以随时赋值给span