1. 概述
做中間件的埋點插件,需要通過對源碼梳理浪册,找到關(guān)鍵攔截點扫腺,如什么類的什么方法,通過其參數(shù)村象、返回值笆环、對象屬性等獲取構(gòu)建Span
的數(shù)據(jù)信息。這節(jié)內(nèi)容記錄一下開發(fā)插件過程中一些零碎的事項厚者。
通過繼承ClassInstanceMethodsEnhancePluginDefine
躁劣,在子類的實現(xiàn)方法中完成以下邏輯:
- 指定待增強的類
@Override
protected ClassMatch enhanceClass() {
IndirectMatch hierarchyMatch = byHierarchyMatch(new String[]{ENHANCE_CLASS});
PrefixMatch prefixMatch = nameStartsWith(PAJK_PACKAGE_PREFIX);
return LogicalMatchOperation.and(hierarchyMatch, prefixMatch);
}
- 在構(gòu)造函數(shù)中添加增強邏輯,可通過類名(構(gòu)造方法同名)籍救,方法參數(shù)信息指定構(gòu)造函數(shù)习绢。
//這種方式,不做構(gòu)造函數(shù)增強
@Override
public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
return new ConstructorInterceptPoint[0];
}
//指定構(gòu)造方法
@Override
public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
return new ConstructorInterceptPoint[]{
new ConstructorInterceptPoint() {
@Override
public ElementMatcher<MethodDescription> getConstructorMatcher() {
//第二個參數(shù)必須是String
return takesArgumentWithType(2, "java.lang.String");
}
@Override
public String getConstructorInterceptor() {
return INTERCEPT_CLASS;//指定類名(構(gòu)造方法名)
}
}
};
}
- 增強實例方法,通過
InstanceMethodsInterceptPoint
匹配將增強的方法
@Override
public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
return new InstanceMethodsInterceptPoint[]{
new InstanceMethodsInterceptPoint() {
@Override
public ElementMatcher<MethodDescription> getMethodsMatcher() {
return named(ENHANCE_METHOD);
}
@Override
public String getMethodsInterceptor() {
return INTERCEPTOR_CLASS;
}
@Override
public boolean isOverrideArgs() {
return false;
}
}
};
}
- 增強類(靜態(tài))方法闪萄,通過
StaticMethodsInterceptPoint
匹配將增強的類方法
@Override
public StaticMethodsInterceptPoint[] getStaticMethodsInterceptPoints() {
return new StaticMethodsInterceptPoint[0];
}
2. xxxMatch
匹配的技巧
在以上代碼中可以看到許多xxxMatch梧却,大致是2類:
-
ClassMatch
:用于匹配類 -
ElementMatcher
:用于匹配方法
2.1 ClassMatch
在源碼中可看到ClassMatch
有如下這些實現(xiàn)類:
ClassAnnotationMatch
EitherInterfaceMatch
FailedCallbackMatch
HierarchyMatch
IndirectMatch
ListenableFutureCallbackMatch
LogicalAndMatch
LogicalOrMatch
MethodAnnotationMatch
MultiClassNameMatch
NameMatch
PrefixMatch
RegexMatch
SuccessCallbackMatch
這些匹配有按照類名字匹配、前綴匹配败去、繼承關(guān)系放航、實現(xiàn)接口、組合與匹配圆裕、組合或匹配等等广鳍;可通過在源碼中查找其應(yīng)用來進一步理解其作用;另外需要注意吓妆,除了默認(rèn)提供的這些赊时,還是可以根據(jù)自己的需求來額外定制(通過實現(xiàn)接口IndirectMatch
)
2.2 ElementMatcher
系統(tǒng)api
在ElementMatchers
中已經(jīng)定義了很多用于方法匹配的方法,因其方法太多行拢,不在此列舉祖秒,自行查看自定義matcher
return new ElementMatcher<MethodDescription>() {
@Override
public boolean matches(MethodDescription target) {
//自己編排邏輯
if(xxx) {
return true;
}
return false;
}
3. 上下文數(shù)據(jù)的傳遞
插件埋點所需要的信息,通常是需要再多個方法中分別捕獲舟奠,我們需要通過某種上下文機制將這些分散的信息搜集整合起來竭缝;從線程角度說通常分為同步請求(相同線程內(nèi)完成一次執(zhí)行)和異步請求(不同的線程內(nèi)完成一次請求)。
3.1 同步請求的上下文數(shù)據(jù)
- ThreadLocal方式沼瘫,在Skywalking中已為我們提供了這種途徑
ContextManager.getRuntimeContext()
抬纸;
- 存儲數(shù)據(jù)
ContextManager.getRuntimeContext().put(key, data)
- 檢查數(shù)據(jù)
ContextManager.getRuntimeContext().get(key)
,判斷是否存在 - 清理數(shù)據(jù)
需要留意做善后清理數(shù)據(jù)ContextManager.getRuntimeContext().remove(key)
- Skywalking的擴展字段,Skywalking會在被增強的類中添加一個sw專用的屬性,同時這個類會被修改耿戚,實現(xiàn)了接口
EnhancedInstance
湿故,通過此接口中的2個方法來讀寫這個擴展屬性
public interface EnhancedInstance {
Object getSkyWalkingDynamicField();
void setSkyWalkingDynamicField(Object value);
}
這個sw專用字段,就是一個普通的Object溅话,可根據(jù)自己的需求給其賦值晓锻。
如這樣一些使用場景:
- 攔截目標(biāo)類的構(gòu)造方法,在構(gòu)造房中new 一個自定義類飞几,通過
setSkyWalkingDynamicField
賦值給擴展字段 - 在其他方法中砚哆,捕獲不同的數(shù)據(jù),填充到這個對象的擴展屬性里屑墨。
- 讀取擴展屬性中的數(shù)據(jù)躁锁,構(gòu)造、填充
Span
3.2 異步請求的上下文數(shù)據(jù)
異步請求的場景下卵史,因為跨越了線程战转,所以上下文需要在多個線程中傳遞,那么常規(guī)的ThreadLocal
方式就不可用了以躯;通過SW擴展屬性來承載數(shù)據(jù)槐秧,在多個線程中傳遞的方式非常方便了啄踊。
異步請求時 需要借助AsyncSpan#prepareForAsync
和 AsyncSpan#asyncFinish
來完成span的異步關(guān)閉,這里一個問題刁标,異步跨線程的情況下颠通,另外的線程里如何拿到這個span實例呢? 尋找在多個線程中都存在的對象膀懈,如果這個對象本身有承載額外數(shù)據(jù)的能力最好顿锰,如果沒有,則需要增強這個類启搂,借助sw的擴展字段來承載這個span硼控,進而在異步結(jié)果處理中完成span的關(guān)閉。
異步請求的上下文的實例胳赌,通過在源碼中搜索prepareForAsync
方法的使用來加深理解,這里用ExitSpan方式暫記一下關(guān)鍵邏輯和步驟:
- 發(fā)送請求的線程a牢撼,創(chuàng)建ExitSpan 實例 exitSpan,會對exitSpan做一些賦值
- 線程b中會獲取當(dāng)前請求的執(zhí)行結(jié)果匈织。
- 線程a 和線程b 之間一定會有一個對象浪默,在兩個線程之間都可訪問牡直,暫叫 externObj;
- 線程a缀匕,在發(fā)起異步請求之前,調(diào)用exitSpan的
prepareForAsync
方法碰逸,并把exitSpan裝入externObj中乡小;如果externObj沒有合適的屬性去承載exitSpan數(shù)據(jù),就擴展這個externObj的類饵史,比如使用skywalking的類增強或者繼承externObj的類满钟,達到增加屬性字段的效果 - 線程b,在拿到響應(yīng)結(jié)果后胳喷,從externObj的取出exitSpan湃番,根據(jù)返回值 給 exitSpan做賦值,最后調(diào)用
asyncFinish
異步關(guān)閉span
4. SpanStack
1. EntrySpan 的SpanStack
服務(wù)接收請求時吭露,創(chuàng)建EntrySpan
;tomcat和SpringMVC的插件都是創(chuàng)建 EntrySpan
吠撮,這樣就重復(fù)創(chuàng)建了EntySpan
,這樣沒有意義,在sw中讲竿,只要最外層的EntrySpan
,通過一個計數(shù)器(stackDepth)和棧的結(jié)構(gòu)來實現(xiàn)泥兰,大致流程如下:
- 請求進入1處,創(chuàng)建
EntrySpan
暫叫tomEntrySpan题禀,stackDepth=1,記錄開始時間 - 請求進入2處鞋诗,創(chuàng)建
EntrySpan
時,stackDepth+1=2迈嘹,會復(fù)用上一步創(chuàng)建的tomEntrySpan對象削彬,而不是new 一個新的;同時會清理此span上的layer、logs和tags融痛;保留springMVC層的layer糕篇、logs和tags。 - 請求進入3處酌心,執(zhí)行finish方法將stackDepth-- = 1;
- 請求進入4處拌消,執(zhí)行finish方法將stackDepth--=0,關(guān)閉tomEntrySpan安券,記錄結(jié)束時間
2. ExitSpan 的SpanStack
假設(shè)上圖tomcat
是作為調(diào)用外部請求的墩崩,創(chuàng)建ExitSpan
(這里只是借圖舉例)其邏輯流程卻是這樣:
- 請求進入1處,創(chuàng)建
ExitSpan
實例 tomExitSpan侯勉,stackDepth=1鹦筹,記錄開始時間;在這一層才可以記錄layer、logs和tags址貌。 - 請求進入2處铐拐,創(chuàng)建
ExitSpan
時,stackDepth+1=2练对,會復(fù)用上一步創(chuàng)建的tomExitSpan對象遍蟋,而不是new 一個新的;這里layer螟凭、logs和tags設(shè)置無效虚青。 - 請求進入3處,執(zhí)行finish方法將stackDepth-- = 1;
- 請求進入4處螺男,執(zhí)行finish方法將stackDepth--=0棒厘,關(guān)閉tomExitSpan,記錄結(jié)束時間。
3. 方法的重入
比如在ExitSpan
場景下中下隧,A類的put1和put2方法都被攔截奢人,并且put1方法 調(diào)用了put2方法,在上述的SpanStack的機制里淆院,由于put2方法中stackDepth=2何乎,其內(nèi)所處理的layer、logs和tags是無效的迫筑;對于這種情況宪赶,大概有2中方法處理:
- 當(dāng)stackDepth!=1 的時候脯燃,通過上下文記錄layerlayer搂妻、logs和tags的信息,等stackDepth=1時辕棚,從上下文中取出layerlayer欲主、logs和tags的信息賦值給span邓厕。
- 控制span的創(chuàng)建時機,自行在上下文中增加stackDepth的計數(shù)控制扁瓢,當(dāng)stackDepth>1時详恼,不在創(chuàng)建ExitSpan。即put1中有一個ExitSpan引几,put2中不創(chuàng)建
ExitSpan
昧互,那么stackDepth一直=1,layerlayer伟桅、logs和tags的信息可以隨時賦值給span