中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何通過使用Byte?Buddy便捷創建Java?Agent

發布時間:2022-03-05 11:44:24 來源:億速云 閱讀:189 作者:小新 欄目:開發技術

這篇文章主要為大家展示了“如何通過使用Byte Buddy便捷創建Java Agent”,內容簡而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領大家一起研究并學習一下“如何通過使用Byte Buddy便捷創建Java Agent”這篇文章吧。

    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我們還可以在位置路徑上設置可選的 agent 參數。在下面的命令中會啟動一個 Java 程序并且添加給定的 agent,將值 myOptions 作為參數提供給premain方法:

    java -javaagent:myAgent.jar=myOptions -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。

    Java 字節碼與類文件格式

    類文件代表了 Java 類編譯之后的狀態。類文件中會包含字節碼,這些字節碼代表了 Java 源碼中最初的程序指令。Java 字節碼可以視為 Java 虛擬機的語言。實際上,JVM 并不會將 Java 視為編程語言,它只能處理字節碼。因為它采用二進制的表現形式,所以相對于程序的源碼,它占用的空間更少。除此之外,將程序以字節碼的形式進行表現能夠更容易地編譯 Java 以外的其他語言,如 Scala 或 Clojure,從而讓它們運行在 JVM 上。如果沒有字節碼作為中間語言的話,那么其他的程序在運行之前,可能還需要將其轉換為 Java 源碼。

    但是,在代碼處理的時候,這種抽象卻帶來了一定的成本。如果要將ClassFileTransformer應用到某個類上,那我們不能將該類按照 Java 源碼的形式進行處理,甚至不能假設被轉換的代碼最初是由 Java 編寫而成的。更糟糕的是,探查類成員或注解的反射 API 也是禁止使用的,這是因為類加載之前,我們無法訪問這些 API,而在轉換進程完成之前,是無法進行加載的。

    所幸的是,Java 字節碼相對來講是一個比較簡單的抽象形式,它包含了很少量的操作,稍微花點功夫我們就能大致將其掌握起來。Java 虛擬機執行程序的時候,會以基于棧的方式來處理值。字節碼指令一般會告知虛擬機,需要從操作數棧(operand stack)上彈出值,執行一些操作,然后再將結果壓到棧中。

    讓我們考慮一個簡單的樣例:將數字 1 和 2 進行相加操作。JVM 首先會將這兩個數字壓到棧中,這是通過 _iconst_1_ 和 _iconst_2_ 這兩個字節指令實現的。_iconst_1_ 是個單字節的便捷運算符(operator),它會將數字 1 壓到棧中。與之類似,_iconst_2_ 會將數字 2 壓到棧中。然后,會執行 _iadd_ 指令,它會將棧中最新的兩個值彈出,將它們求和計算的結果重新壓到棧中。在類文件中,每個指令并不是以其易于記憶的名稱進行存儲的,而是以一個字節的形式進行存儲,這個字節能夠唯一地標記特定的指令,這也是 _bytecode_ 這個術語的來歷。上文所述的字節碼指令及其對操作數棧的影響,通過下面的圖片進行了可視化。

    如何通過使用Byte?Buddy便捷創建Java?Agent

    對于人類用戶來講,會更喜歡源碼而不是字節碼,不過幸運的是 Java 社區創建了多個庫,能夠解析類文件并將緊湊的字節碼暴露為具有名稱的指令流。例如,流行的 ASM 庫提供了一個簡單的 visitor API,它能夠將類文件剖析為成員和方法指令,其操作方式類似于閱讀 XML 文件時的 SAX 解析器。如果使用 ASM 的話,那上述樣例中的字節碼可以按照如下的代碼來進行實現(在這里,ASM 方式的指令是visitIns,能夠提供修正的方法實現):

    MethodVisitor methodVisitor = ...
    methodVisitor.visitIns(Opcodes.ICONST_1);
    methodVisitor.visitIns(Opcodes.ICONST_2);
    methodVisitor.visitIns(Opcodes.IADD);

    需要注意的是,字節碼規范只不過是一種比喻的說法(metaphor),因為 Java 虛擬機允許將程序轉換為優化后的機器碼(machine code),只要程序的輸出能夠保證是正確的即可。因為字節碼的簡潔性,所以在已有的類中取代和修改指令是很簡單直接的。因此,使用 ASM 及其底層的 Java 字節碼基礎就足以實現類轉換的 Java agent,這需要注冊一個ClassFileTransformer,它會使用這個庫來處理其參數。

    克服字節碼的不足

    對于實際的應用來講,解析原始的類文件依然意味著有很多的手動工作。Java 程序員通常感興趣的是類型層級結構中的類。例如,某個 Java agent 可能需要修改所有實現給定接口的類。如果要確定某個類的超類,那只靠解析ClassFileTransformer所給定的類文件就不夠了,類文件中只包含了直接超類和接口的名字。為了解析可能的超類型關聯關系,程序員依然需要定位這些類型的類文件。

    在項目中直接使用 ASM 的另外一個困難在于,團隊中需要有開發人員學習 Java 字節碼的基礎知識。在實踐中,這往往會導致很多的開發人員不敢再去修改字節碼操作相關的代碼。如果這樣的話,實現 Java agent 很容易為項目的長期維護帶來風險。

    為了克服這些問題,我們最好使用較高層級的抽象來實現 Java agent,而不是直接操作 Java 字節碼。Byte Buddy 是開源的、基于 Apache 2.0 許可證的庫,它致力于解決字節碼操作和 instrumentation API 的復雜性。Byte Buddy 所聲稱的目標是將顯式的字節碼操作隱藏在一個類型安全的領域特定語言背后。通過使用 Byte Buddy,任何熟悉 Java 編程語言的人都有望非常容易地進行字節碼操作。

    Byte Buddy 簡介

    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 重寫該方法,這是通過隨后的

    <ahref="http://bytebuddy.net/javadoc/0.7.1/net/bytebuddy/implementation/Implementation.html">Implementation</a>

    實例實現的,在我們的樣例中,這個實例也就是

    <ahref="http://bytebuddy.net/javadoc/0.7.1/net/bytebuddy/implementation/FixedValue.html">FixedValue</a>

    當創建子類的時候,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!"));

    當然,這個生成的類并沒有太大的用處。對于實際的應用來講,大多數方法的返回值是在運行時計算的,這個計算過程要依賴于方法的參數和對象的狀態。

    通過委托實現 Instrumentation

    要實現某個方法,有一種更為靈活的方式,那就是使用 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

    通過使用 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 實現橫切的關注點提供了一種更為模塊化的代碼編寫方式,而不必針對某個管理實例的中心框架來集成所有的代碼。

    _Byte Buddy 的源碼可以免費地在 GitHub 上獲取到。入門手冊可以在 http://bytebuddy.net上找到。Byte Buddy 當前的可用版本是 0.7.4,所有樣例均是基于該版本的。因為其革新性以及對 Java 生態系統的貢獻,該庫曾經在 2015 年獲得過 Oracle 的 Duke&rsquo;s Choice 獎項。

    以上是“如何通過使用Byte Buddy便捷創建Java Agent”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!

    向AI問一下細節

    免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI

    兰西县| 乐都县| 安岳县| 桦甸市| 永安市| 琼中| 东兰县| 泗洪县| 鄄城县| 隆尧县| 邢台县| 云梦县| 盘锦市| 安龙县| 宜兰县| 湘潭县| 江川县| 大方县| 萨嘎县| 托克逊县| 东辽县| 怀远县| 太康县| 织金县| 灵武市| 马龙县| 玉林市| 石狮市| 西宁市| 龙州县| 扬中市| 子洲县| 武冈市| 通州区| 阿坝县| 崇文区| 建始县| 阳曲县| 沂南县| 文化| 光泽县|