<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
關於skywalking請看我上一篇博文,skywalking分散式服務呼叫鏈路追蹤APM應用監控 其使用javaAgent技術,使得應用接入監控0耦合。今天在分析skywaking過程中,對javaAgent技術有了更深入的瞭解。skywalking使用的javaAgent工具ByteBuddy是一個比ASM更上層的針對java位元組碼操作的封裝,基於ByteBuddy,我們可以快速方便的對java位元組碼進行增強處理,更高效的開發javaAgent應用。
Byte Buddy官網:https://bytebuddy.net/
github專案地址:https://github.com/raphw/byte-buddy
本文共分三個部分,分別為skywalking的agent模組原始碼分析,javaAgent技術應用,ByteBuddy工具應用
agent的入口方法premain在apm-sniffer模組的SkyWalkingAgent類中,整個agent邏輯如下:
載入Agentjar包所在錄入的/config/agent.config檔案,引數載入的優先順序分別為:系統環境變數 > VM引數(-D) > /config/agent.config中的設定。所以Agent安裝包的目錄別輕易改動,相關的讀取設定在程式碼裡寫死了的
外掛程式碼在apm-sck-plugin模組下,目前共有24個外掛支援,包含主流Rpc如(dubbo,motan,grpc)等,下面以dubbo的Agent外掛列,看skywalking如何開發相關套件的。
分兩步:
public class DubboInstrumentation extends ClassInstanceMethodsEnhancePluginDefine { private static final String ENHANCE_CLASS = "com.alibaba.dubbo.monitor.support.MonitorFilter"; private static final String INTERCEPT_CLASS = "org.skywalking.apm.plugin.dubbo.DubboInterceptor"; @Override protected ClassMatch enhanceClass() { return byName(ENHANCE_CLASS); } @Override protected ConstructorInterceptPoint[] getConstructorsInterceptPoints() { return null; } @Override protected InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() { return new InstanceMethodsInterceptPoint[] { new InstanceMethodsInterceptPoint() { @Override public ElementMatchergetMethodsMatcher() { return named("invoke"); } @Override public String getMethodsInterceptor() { return INTERCEPT_CLASS; } @Override public boolean isOverrideArgs() { return false; } } }; } }
通過java的spi機制ServiceLoader.load(BootService.class)載入agen端的所需服務,Agent端共有七個基礎service服務,分別如下
程式碼如下
new AgentBuilder.Default().type(pluginFinder.buildMatch()).transform(new AgentBuilder.Transformer() { @Override public DynamicType.Builder transform(DynamicType.Builder builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module) { ListpluginDefines = pluginFinder.find(typeDescription, classLoader); if (pluginDefines.size() > 0) { DynamicType.Builder newBuilder = builder; EnhanceContext context = new EnhanceContext(); for (AbstractClassEnhancePluginDefine define : pluginDefines) { DynamicType.Builder possibleNewBuilder = define.define(typeDescription.getTypeName(), 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; } }).with(new AgentBuilder.Listener() { @Override public void onDiscovery(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) { } @Override public void onTransformation(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded, DynamicType dynamicType) { if (logger.isDebugEnable()) { logger.debug("On Transformation class {}.", typeDescription.getName()); } } @Override public void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded) { } @Override public void onError(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded, Throwable throwable) { logger.error("Failed to enhance class " + typeName, throwable); } @Override public void onComplete(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) { } }).installOn(instrumentation);
使用ByteBuddy程式碼位元組碼增強特別簡單,開發agent應用不用再操作instrumentation的相關介面了
Java agent是在另外一個Java應用(“目標”應用)啟動之前要執行的Java程式,這樣agent就有機會修改目標應用或者應用所執行的環境。在本文中,我們將會從基礎內容開始,逐漸增強其功能,藉助位元組碼操作工具Byte Buddy,使其成為高階的agent實現。
在最基本的用例中,Java agent會用來設定應用屬性或者設定特定的環境狀態,agent能夠作為可重用和可插入的元件。如下的樣例描述了這樣的一個agent,它設定了一個系統屬性,在實際的程式中就可以使用該屬性了:
public class Agent { public static void premain(String arg) { System.setProperty("my-property", 「foo」); } }
如上面的程式碼所述,Java agent的定義與其他的Java程式類似,只不過它使用premain方法替代main方法作為入口點。顧名思義,這個方法能夠在目標應用的main方法之前執行。相對於其他的Java程式,編寫agent並沒有特定的規則。有一個很小的區別在於,Java agent接受一個可選的引數,而不是包含零個或更多引數的陣列。
如果要使用這個agent,必須要將agent類和資源打包到jar中,並且在jar的manifest中要將Agent-Class屬性設定為包含premain方法的agent類。(agent必須要打包到jar檔案中,它不能通過拆解的格式進行指定。)接下來,我們需要啟動應用程式,並且在命令列中通過javaagent引數來參照jar檔案的位置:
java -javaagent:myAgent.jar -jar myProgram.jar
通過重複使用javaagent命令,能夠新增多個agent。
但是,Java agent的功能並不侷限於修改應用程式環境的狀態,Java agent能夠存取Java instrumentation API,這樣的話,agent就能修改目標應用程式的程式碼。Java虛擬機器器中這個鮮為人知的特性提供了一個強大的工具,有助於實現面向切面的程式設計。
如果要對Java程式進行這種修改,我們需要在agent的premain方法上新增型別為Instrumentation的第二個引數。Instrumentation引數可以用來執行一系列的任務,比如確定物件以位元組為單位的精確大小以及通過註冊ClassFileTransformers實際修改類的實現。ClassFileTransformers註冊之後,當類載入器(class loader)載入類的時候都會呼叫它。當它被呼叫時,在類檔案所代表的類載入之前,類檔案transformer有機會改變或完全替換這個類檔案。按照這種方式,在類使用之前,我們能夠增強或修改類的行為,如下面的樣例所示:
public class Agent { public static void premain(String argument, Instrumentation inst) { inst.addTransformer(new ClassFileTransformer() { @Override public byte[] transform( ClassLoader loader, String className, Class classBeingRedefined, // 如果類之前沒有載入的話,值為null ProtectionDomain protectionDomain, byte[] classFileBuffer) { // 返回改變後的類檔案。 } }); } }
通過使用Instrumentation範例註冊上述的ClassFileTransformer之後,每個類載入的時候,都會呼叫這個transformer。為了實現這一點,transformer會接受一個二進位制和類載入器的參照,分別代表了類檔案以及試圖載入類的類載入器。
Java agent也可以在Java應用的執行期註冊,如果是在這種場景下,instrumentation API允許重新定義已載入的類,這個特性被稱之為“HotSwap”。不過,重新定義類僅限於替換方法體。在重新定義類的時候,不能新增或移除類成員,並且型別和簽名也不能進行修改。當類第一次載入的時候,並沒有這種限制,如果是在這樣的場景下,那classBeingRedefined會被設定為null。
Byte Buddy的目的並不僅僅是為了生成Java agent。它提供了一個API用於生成任意的Java類,基於這個生成類的API,Byte Buddy提供了額外的API來生成Java agent。
作為Byte Buddy的簡介,如下的樣例展現瞭如何生成一個簡單的類,這個類是Object的子類,並且重寫了toString方法,用來返回“Hello World!”。與原始的ASM類似,“intercept”會告訴Byte Buddy為攔截到的指令提供方法實現:
Class dynamicType = new ByteBuddy() .subclass(Object.class) .method(ElementMatchers.named("toString")) .intercept(FixedValue.value("Hello World!")) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded();
從上面的程式碼中,我們可以看到Byte Buddy要實現一個方法分為兩步。首先,程式設計人員需要指定一個ElementMatcher,它負責識別一個或多個需要實現的方法。Byte Buddy提供了功能豐富的預定義攔截器(interceptor),它們暴露在ElementMatchers類中。在上述的例子中,toString方法完全精確匹配了名稱,但是,我們也可以匹配更為複雜的程式碼結構,如型別或註解。
當Byte Buddy生成類的時候,它會分析所生成型別的類層級結構。在上述的例子中,Byte Buddy能夠確定所生成的類要繼承其超類Object的名為toString的方法,指定的匹配器會要求Byte Buddy重寫該方法,這是通過隨後的 Implementation 範例實現的,在我們的樣例中,這個範例也就是FixedValue。
當建立子類的時候,Byte Buddy始終會攔截(intercept)一個匹配的方法,在生成的類中重寫該方法。但是,我們在本文稍後將會看到Byte Buddy還能夠重新定義已有的類,而不必通過子類的方式來實現。在這種情況下,Byte Buddy會將已有的程式碼替換為生成的程式碼,而將原有的程式碼複製到另外一個合成的(synthetic)方法中。
在我們上面的程式碼樣例中,匹配的方法進行了重寫,在實現裡面,返回了固定的值“Hello World!”。intercept方法接受Implementation型別的引數,Byte Buddy自帶了多個預先定義的實現,如上文所使用的FixedValue類。但是,如果需要的話,可以使用前文所述的ASM API將某個方法實現為自定義的位元組碼,Byte Buddy本身也是基於ASM API實現的。
定義完類的屬性之後,就能通過make方法來進行生成。在樣例應用中,因為使用者沒有指定類名,所以生成的類會給定一個任意的名稱。最終,生成的類將會使用ClassLoadingStrategy來進行載入。通過使用上述的預設 WRAPPER策略,類將會使用一個新的類載入器進行載入,這個類載入器會使用環境類載入器作為父載入器。
類載入之後,使用Java反射API就可以存取它了。如果沒有指定其他構造器的話,Byte Buddy將會生成類似於父類別的構造器,因此生成的類可以使用預設的構造器。這樣,我們就可以檢驗生成的類重寫了 toString方法,如下面的程式碼所示:
assertThat(dynamicType.newInstance().toString(), is("Hello World!"));
當然,這個生成的類並沒有太大的用處。對於實際的應用來講,大多數方法的返回值是在執行時計算的,這個計算過程要依賴於方法的引數和物件的狀態。
要實現某個方法,有一種更為靈活的方式,那就是使用Byte Buddy的MethodDelegation。通過使用方法委託,在生成重寫的實現時,我們就有可能呼叫給定類和範例的其他方法。按照這種方式,我們可以使用如下的委託器(delegator)重新編寫上述的樣例:
class ToStringInterceptor { static String intercept() { return 「Hello World!」; } }
藉助上面的POJO攔截器,我們就可以將之前的FixedValue實現替換為MethodDelegation.to(ToStringInterceptor.class):
Class dynamicType = new ByteBuddy() .subclass(Object.class) .method(ElementMatchers.named("toString")) .intercept(MethodDelegation.to(ToStringInterceptor.class)) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded();
使用上述的委託器,Byte Buddy會在to方法所給定的攔截目標中,確定最優的呼叫方法。就ToStringInterceptor.class來講,選擇過程只是非常簡單地解析這個型別的唯一靜態方法而已。在本例中,只會考慮一個靜態方法,因為委託的目標中指定的是一個類。與之不同的是,我們還可以將其委託給某個類的範例,如果是這樣的話,Byte Buddy將會考慮所有的虛方法(virtual method)。如果類或範例上有多個這樣的方法,那麼Byte Buddy首先會排除掉所有與指定instrumentation不相容的方法。在剩餘的方法中,庫將會選擇最佳的匹配者,通常來講這會是引數最多的方法。我們還可以顯式地指定目標方法,這需要縮小合法方法的範圍,將ElementMatcher傳遞到MethodDelegation中,就會進行方法的過濾。例如,通過新增如下的filter,Byte Buddy只會將名為“intercept”的方法視為委託目標:
MethodDelegation.to(ToStringInterceptor.class) .filter(ElementMatchers.named(「intercept」))
執行上面的攔截之後,被攔截到的方法依然會列印出“Hello World!”,但是這次的結果是動態計算的,這樣的話,我們就可以在攔截器方法上設定斷點,所生成的類每次呼叫toString時,都會觸發攔截器的方法。
當我們為攔截器方法設定引數時,就能釋放出MethodDelegation的全部威力。這裡的引數通常是帶有註解的,用來要求Byte Buddy在呼叫攔截器方法時,注入某個特定的值。例如,通過使用@Origin註解,Byte Buddy提供了新增instrument功能的方法的範例,將其作為Java反射API中類的範例:
class ContextualToStringInterceptor { static String intercept(@Origin Method m) { return 「Hello World from 」 + m.getName() + 「!」; } }
當攔截toString方法時,對instrument方法的呼叫將會返回“Hello world from toString!”。
除了@Origin註解以外,Byte Buddy提供了一組功能豐富的註解。例如,通過在型別為Callable的引數上使用@Super註解,Byte Buddy會建立並注入一個代理範例,它能夠呼叫被instrument方法的原始程式碼。如果對於特定的使用者場景,所提供的註解不能滿足需求或者不太適合的話,我們甚至能夠註冊自定義的註解,讓這些註解注入使用者特定的值。
可以看到,我們在執行時可以藉助簡單的Java程式碼,使用MethodDelegation來動態重寫某個方法。這只是一個簡單的樣例,但是這項技術可以用到更加實際的應用之中。在本文剩餘的內容中,我們將會開發一個樣例,它會使用程式碼生成技術實現一個註解驅動的庫,用來限制方法級別的安全性。在我們的第一個迭代中,這個庫會通過生成子類的方式來限制安全性。然後,我們將會採取相同的方式來實現Java agent,完成相同的功能。
樣例庫會使用如下的註解,允許使用者指定某個方法需要考慮安全因素:
@interface Secured { String user(); }
例如,假設應用需要使用如下的Service類來執行敏感操作,並且只有使用者被認證為管理員才能執行該方法。這是通過為執行這個操作的方法宣告Secured註解來指定的:
class Service { @Secured(user = 「ADMIN」) void doSensitiveAction() { // 執行敏感程式碼... } }
我們當然可以將安全檢查直接編寫到方法中。在實際中,寫死橫切關注點往往會導致複製-貼上的邏輯,使其難以維護。另外,一旦應用需要涉及額外的需求時,如紀錄檔、收集呼叫指標或結果快取,直接新增這樣的程式碼擴充套件性不會很好。通過將這樣的功能抽取到agent中,方法就能很純粹地關注其業務邏輯,使得程式碼庫能夠更易於閱讀、測試和維護。
為了讓我們規劃的庫保持儘可能得簡單,按照註解的協定宣告,如果當前使用者不具備註解的使用者屬性時,將會丟擲IllegalStateException異常。通過使用Byte Buddy,這種行為可以用一個簡單的攔截器來實現,如下面樣例中的SecurityInterceptor所示,它會通過其靜態的user域,跟蹤當前使用者已經進行了登入:
class SecurityInterceptor { static String user = 「ANONYMOUS」 static void intercept(@Origin Method method) { if (!method.getAnnotation(Secured.class).user().equals(user)) { throw new IllegalStateException(「Wrong user」); } } }
通過上面的程式碼,我們可以看到,即便給定使用者授予了存取許可權,攔截器也沒有呼叫原始的方法。為了解決這個問題,Byte Buddy有很多預定義的方法可以實現功能的連結。藉助MethodDelegation類的andThen方法,上述的安全檢查可以放到原始方法的呼叫之前,如下面的程式碼所示。如果使用者沒有進行認證的話,安全檢查將會丟擲異常並阻止後續的執行,因此原始方法將不會執行。
將這些功能集合在一起,我們就能生成Service的一個子類,所有帶有註解方法的都能恰當地進行安全保護。因為所生成的類是Service的子類,所以它能夠替代所有型別為Service的變數,並不需要任何的型別轉換,如果沒有恰當認證的話,呼叫doSensitiveAction方法就會丟擲異常:
new ByteBuddy() .subclass(Service.class) .method(ElementMatchers.isAnnotatedBy(Secured.class)) .intercept(MethodDelegation.to(SecurityInterceptor.class) .andThen(SuperMethodCall.INSTANCE))) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded() .newInstance() .doSensitiveAction();
不過壞訊息是,因為實現instrumentation功能的子類是在執行時建立的,所以除了使用Java反射以外,沒有其他辦法建立這樣的範例。因此,所有instrumentation類的範例都應該通過一個工廠來建立,這個工廠會封裝建立instrumentation子類的複雜性。這樣造成的結果就是,子類instrumentation通常會用於框架之中,這些框架本身就需要通過工廠來建立範例,例如,像依賴管理的框架Spring或物件-關係對映的框架Hibernate,而對於其他型別的應用來講,子類instrumentation實現起來通常過於複雜。
通過使用Java agent,上述安全框架的一個替代實現將會修改Service類的原始位元組碼,而不是重寫它。這樣做的話,我們就沒有必要建立託管的範例了,只需簡單地呼叫
new Service().doSensitiveAction()
即可,如果對應的使用者沒有進行認證的話,就會丟擲異常。為了支援這種方式,Byte Buddy提供一種稱之為rebase某個類的理念。當rebase某個類的時候,不會建立子類,所採用的策略是實現instrumentation功能的程式碼將會合併到被instrument的類中,從而改變其行為。在新增instrumentation功能之後,在被instrument的類中,其所有方法的原始程式碼均可進行存取,因此像SuperMethodCall這樣的instrumentation,工作方式與建立子類是完全一樣的。
建立子類與rebase的行為是非常類似的,所以兩種操作的API執行方式是一致的,都會使用相同的DynamicType.Builder介面來描述某個型別。兩種形式的instrumentation都可以通過ByteBuddy類來進行存取。為了使Java agent的定義更加便利,Byte Buddy還提供了 AgentBuilder類,它希望能夠以一種簡潔的方式應對一些通用的使用者場景。為了定義Java agent實現方法級別的安全性,將如下的類定義為agent的入口點就足以完成該功能了:
class SecurityAgent { public static void premain(String arg, Instrumentation inst) { new AgentBuilder.Default() .type(ElementMatchers.any()) .transform((builder, type) -> builder .method(ElementMatchers.isAnnotatedBy(Secured.class) .intercept(MethodDelegation.to(SecurityInterceptor.class) .andThen(SuperMethodCall.INSTANCE)))) .installOn(inst); } }
如果將這個agent打包為jar檔案並在命令列中進行指定,那麼所有帶有Secured註解的方法將會進行“轉換”或重定義,從而實現安全保護。如果不啟用這個Java agent的話,應用在執行時就不包含額外的安全檢查。當然,這意味著如果對帶有註解的程式碼進行單元測試的話,這些方法的呼叫並不需要特殊的搭建過程來模擬安全上下文。Java執行時會忽略掉無法在classpath中找到的註解型別,因此在執行帶有註解的方法時,我們甚至完全可以在應用中移除掉安全庫。
另外一項優勢在於,Java agent能夠很容易地進行疊加。如果在命令列中指定多個Java agent的話,每個agent都有機會對類進行修改,其順序就是在命令列中所指定的順序。例如,我們可以採取這種方式將安全、紀錄檔以及監控框架聯合在一起,而不需要在這些應用間增添任何形式的整合層。因此,使用Java agent實現橫切的關注點提供了一種更為模組化的程式碼編寫方式,而不必針對某個管理範例的中心框架來整合所有的程式碼。
特別說明:ByteBuddy部分節選Rafael Winterhalter的《Easily Create Java Agents with Byte Buddy》
譯文地址:https://www.jb51.net/article/239718.htm
以上就是skywalking原始碼解析javaAgent工具ByteBuddy應用的詳細內容,更多關於skywalking原始碼解析javaAgent ByteBuddy的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45