探針技術-JavaAgent 和位元組碼增強技術-Byte Buddy

語言: CN / TW / HK
  • 能夠基於Java Agent編寫出普通類的代理
  • 理解Byte Buddy的作用
  • 能夠基於Byte Buddy編寫動態代理

1 Byte Buddy

Byte Buddy 是一個程式碼生成和操作庫,用於在 Java 應用程式執行時建立和修改 Java 類,而無需編譯器的幫助。除了 Java 類庫附帶的程式碼生成實用程式外, Byte Buddy 還允許建立任意類,並且不限於實現用於建立執行時代理的介面。此外, Byte Buddy 提供了一種方便的 API,可以使用 Java 代理或在構建過程中手動更改類。

  • 無需理解位元組碼指令,即可使用簡單的 API 就能很容易操作位元組碼,控制類和方法。
  • 已支援Java 11,庫輕量,僅取決於Java位元組程式碼解析器庫ASM的訪問者API,它本身不需要任何其他依賴項。
  • 比起JDK動態代理、cglib、Javassist,Byte Buddy在效能上具有一定的優勢。

官網: http://bytebuddy.net

1.1 Byte Buddy應用場景

Java 是一種強型別的程式語言,即要求所有變數和物件都有一個確定的型別,如果在賦值操作中出現型別不相容的情況,就會丟擲異常。強型別檢查在大多數情況下是可行的,然而在某些特殊場景下,強型別檢查則成了巨大的障礙。

我們在做一些通用工具封裝的時候,型別檢查就成了很大障礙。比如我們編寫一個通用的Dao實現資料操作,我們根本不知道使用者要呼叫的方法會傳幾個引數、每個引數是什麼型別、需求變更又會出現什麼型別,幾乎沒法在方法中引用使用者方法中定義的任何型別。我們絕大多數通用工具封裝都採用了反射機制,通過反射可以知道使用者呼叫的方法或欄位,但是Java反射有很多缺陷:

1:反射效能很差
2:反射能繞開型別安全檢查,不安全,比如許可權暴力破解

java程式語言程式碼生成庫不止 Byte Buddy 一個,以下程式碼生成庫在 Java 中也很流行:

  • Java Proxy

Java Proxy 是 JDK 自帶的一個代理工具,它允許為實現了一系列介面的類生成代理類。Java Proxy 要求目標類必須實現介面是一個非常大限制,例如,在某些場景中,目標類沒有實現任何介面且無法修改目標類的程式碼實現,Java Proxy 就無法對其進行擴充套件和增強了。

  • CGLIB

CGLIB 誕生於 Java 初期,但不幸的是沒有跟上 Java 平臺的發展。雖然 CGLIB 本身是一個相當強大的庫,但也變得越來越複雜。鑑於此,導致許多使用者放棄了 CGLIB 。

  • Javassist

Javassist 的使用對 Java 開發者來說是非常友好的,它使用Java 原始碼字串和 Javassist 提供的一些簡單 API ,共同拼湊出使用者想要的 Java 類,Javassist 自帶一個編譯器,拼湊好的 Java 類在程式執行時會被編譯成為位元組碼並載入到 JVM 中。Javassist 庫簡單易用,而且使用 Java 語法構建類與平時寫 Java 程式碼類似,但是 Javassist 編譯器在效能上比不了 Javac 編譯器,而且在動態組合字串以實現比較複雜的邏輯時容易出錯。

  • Byte Buddy

Byte Buddy 提供了一種非常靈活且強大的領域特定語言,通過編寫簡單的 Java 程式碼即可建立自定義的執行時類。與此同時,Byte Buddy 還具有非常開放的定製性,能夠應付不同複雜度的需求。

上面所有程式碼生成技術中,我們推薦使用Byte Buddy,因為Byte Buddy程式碼生成可的效能最高,Byte Buddy 的主要側重點在於生成更快速的程式碼,如下圖:

1.2 Byte Buddy學習

我們接下來詳細講解一下Byte Buddy Api,對重要的方法和類進行深度剖析。

1.2.1 ByteBuddy語法

任何一個由 Byte Buddy 建立/增強的型別都是通過 ByteBuddy 類的例項來完成的,我們先來學習一下ByteBuddy類,如下程式碼:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
        // 生成 Object的子類
        .subclass(Object.class)
        // 生成類的名稱為"com.itheima.Type"
        .name("com.itheima.Type")
        .make();

Byte Buddy 動態增強程式碼總共有三種方式:

subclass:對應 ByteBuddy.subclass() 方法。這種方式比較好理解,就是為目標類(即被增強的類)生成一個子類,在子類方法中插入動態程式碼。

rebasing:對應 ByteBuddy.rebasing() 方法。當使用 rebasing 方式增強一個類時,Byte Buddy 儲存目標類中所有方法的實現,也就是說,當 Byte Buddy 遇到衝突的欄位或方法時,會將原來的欄位或方法實現複製到具有相容簽名的重新命名的私有方法中,而不會拋棄這些欄位和方法實現。從而達到不丟失實現的目的。這些重新命名的方法可以繼續通過重新命名後的名稱進行呼叫。

redefinition:對應 ByteBuddy.redefine() 方法。當重定義一個類時,Byte Buddy 可以對一個已有的類新增屬性和方法,或者刪除已經存在的方法實現。如果使用其他的方法實現替換已經存在的方法實現,則原來存在的方法實現就會消失。

通過上述三種方式完成類的增強之後,我們得到的是 DynamicType.Unloaded 物件,表示的是一個未載入的型別,我們可以使用 ClassLoadingStrategy 載入此型別。 Byte Buddy 提供了幾種類載入策略,這些載入策略定義在 ClassLoadingStrategy.Default 中,其中:

  • WRAPPER 策略 :建立一個新的 ClassLoader 來載入動態生成的型別。
  • CHILD_FIRST 策略 :建立一個子類優先載入的 ClassLoader,即打破了雙親委派模型。
  • INJECTION 策略 :使用反射將動態生成的型別直接注入到當前 ClassLoader 中。

實現如下:

Class<?> dynamicClazz = new ByteBuddy()
        // 生成 Object的子類
        .subclass(Object.class)
        // 生成類的名稱為"com.itheima.Type"
        .name("com.itheima.Type")
        .make()
        .load(Demo.class.getClassLoader(),
                //使用WRAPPER 策略載入生成的動態型別
                ClassLoadingStrategy.Default.WRAPPER)
        .getLoaded();

前面動態生成的 com.itheima.Type 型別只是簡單的繼承了 Object 類,在實際應用中動態生成新型別的一般目的就是為了增強原始的方法,下面通過一個示例展示 Byte Buddy 如何增強 toString() 方法:

// 建立ByteBuddy物件
String str = new ByteBuddy()
        // subclass增強方式
        .subclass(Object.class)
        // 新型別的類名
        .name("com.itheima.Type") 
        // 攔截其中的toString()方法
        .method(ElementMatchers.named("toString"))
        // 讓toString()方法返回固定值
        .intercept(FixedValue.value("Hello World!"))
        .make()
        // 載入新型別,預設WRAPPER策略
        .load(ByteBuddy.class.getClassLoader())
        .getLoaded()
        // 通過 Java反射建立 com.xxx.Type例項
        .newInstance()
        // 呼叫 toString()方法
        .toString();

首先需要關注這裡的 method() 方法,method() 方法可以通過傳入的 ElementMatchers 引數匹配多個需要修改的方法,這裡的 ElementMatchers.named("toString") 即為按照方法名匹配 toString() 方法。如果同時存在多個過載方法,則可以使用 ElementMatchers 其他 API 描述方法的簽名,如下所示:

// 指定方法名稱
ElementMatchers.named("toString")
    // 指定方法的返回值
    .and(ElementMatchers.returns(String.class))
    // 指定方法引數
    .and(ElementMatchers.takesArguments(0));

接下來需要關注的是 intercept() 方法,通過 method() 方法攔截到的所有方法會由 Intercept() 方法指定的 Implementation 物件決定如何增強。這裡的 FixValue.value() 會將方法的實現修改為固定值,上例中就是固定返回 “Hello World!” 字串。

Byte Buddy 中可以設定多個 method()Intercept() 方法進行攔截和修改, Byte Buddy 會按照棧的順序來進行攔截。

1.2.2 ByteBuddy 案例

建立一個專案 agent-demo ,新增如下座標

<dependencies>
    <dependency>
        <groupId>net.bytebuddy</groupId>
        <artifactId>byte-buddy</artifactId>
        <version>1.9.2</version>
    </dependency>
    <dependency>
        <groupId>net.bytebuddy</groupId>
        <artifactId>byte-buddy-agent</artifactId>
        <version>1.9.2</version>
    </dependency>
</dependencies>

我們先建立一個普通類,再為該類建立代理類,建立代理對方法進行攔截做處理。

1)普通類

建立 com.itheima.service.UserService

package com.itheima.service;

public class UserService {

    //方法1
    public String username(){
        System.out.println("username().....");
        return "張三";
    }

    //方法2
    public String address(String username){
        System.out.println("address(String username).....");
        return username+"來自 【湖北省-仙居-恩施土家族苗族自治州】";
    }

    //方法3
    public String address(String username,String city){
        System.out.println("address(String username,String city).....");
        return username+"來自 【湖北省"+city+"】";
    }
}

2)代理測試 com.itheima.service.UserServiceTest

public static void main(String[] args) throws IllegalAccessException, InstantiationException {
        Class<? extends UserService> aClass = new ByteBuddy()
                // 建立一個UserService 的子類
                .subclass(UserService.class)
                //指定類的名稱
                .name("com.itheima.service.UserServiceImpl")
                // 指定要攔截的方法
                //.method(ElementMatchers.isDeclaredBy(UserService.class))
                .method(ElementMatchers.named("address").and(ElementMatchers.returns(String.class).and(ElementMatchers.takesArguments(1))))
                // 為方法新增攔截器 如果攔截器方法是靜態的 這裡可以傳 LogInterceptor.class
                .intercept(MethodDelegation.to(new LogInterceptor()))
                // 動態建立物件,但還未載入
                .make()
                // 設定類載入器 並指定載入策略(預設WRAPPER)
                .load(ByteBuddy.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
                // 開始載入得到 Class
                .getLoaded();
        UserService userService = aClass.newInstance();

        System.out.println(userService.username());
        System.out.println(userService.address("唐僧老師"));
        System.out.println(userService.address("唐僧老師","仙居恩施"));
    }

3)建立攔截器,編寫攔截器方法: com.itheima.service.LogInterceptor

package com.itheima.service;

import net.bytebuddy.implementation.bind.annotation.*;

import java.lang.reflect.Method;
import java.util.concurrent.Callable;

public class LogInterceptor {

    @RuntimeType //將返回值轉換成具體的方法返回值型別,加了這個註解 intercept 方法才會被執行
    public  Object intercept(
            // 被攔截的目標物件 (動態生成的目標物件)
            @This  Object target,
            // 正在執行的方法Method 物件(目標物件父類的Method)
            @Origin Method method,
            // 正在執行的方法的全部引數
            @AllArguments Object[] argumengts,
            // 目標物件的一個代理
            @Super  Object delegate,
            // 方法的呼叫者物件 對原始方法的呼叫依靠它
            @SuperCall Callable<?> callable) throws Exception {
        //目標方法執行前執行日誌記錄
        System.out.println("準備執行Method="+method.getName());
        // 呼叫目標方法
        Object result = callable.call();
        //目標方法執行後執行日誌記錄
        System.out.println("方法執行完成Method="+method.getName());
        return result;
    }

}

在程式中我們 用到 ByteBuddyMethodDelegation 物件,它可以將攔截的目標方法委託給其他物件處理,這裡有幾個註解我們先進行說明:

  • @RuntimeType :不進行嚴格的引數型別檢測,在引數匹配失敗時,嘗試使用型別轉換方式(runtime type casting)進行型別轉換,匹配相應方法。
  • @This :注入被攔截的目標物件(動態生成的目標物件)。
  • @Origin :注入正在執行的方法Method 物件(目標物件父類的Method)。如果攔截的是欄位的話,該註解應該標註到 Field 型別引數。
  • @AllArguments :注入正在執行的方法的全部引數。
  • @Super :注入目標物件的一個代理
  • @SuperCall :這個註解比較特殊,我們要在 intercept() 方法中呼叫 被代理/增強 的方法的話,需要通過這種方式注入,與 Spring AOP 中的 ProceedingJoinPoint.proceed() 方法有點類似,需要注意的是,這裡不能修改呼叫引數,從上面的示例的呼叫也能看出來,引數不用單獨傳遞,都包含在其中了。另外,@SuperCall 註解還可以修飾 Runnable 型別的引數,只不過目標方法的返回值就拿不到了。

執行測試結果:

準備執行Method=username
username().....
方法執行完成Method=username
張三
準備執行Method=address
address(String username).....
方法執行完成Method=address
唐僧老師來自 【湖北省-仙居-恩施土家族苗族自治州】
準備執行Method=address
address(String username,String city).....
方法執行完成Method=address
唐僧老師來自 【湖北省仙居恩施】

2 探針技術-javaAgent

使用Skywalking的時候,並沒有修改程式中任何一行 Java 程式碼,這裡便使用到了 Java Agent 技術,我們接下來展開對Java Agent 技術的學習。

2.1 javaAgent概述

Java Agent這個技術對大多數人來說都比較陌生,但是大家都都多多少少接觸過一些,實際上我們平時用過的很多工具都是基於java Agent來實現的,例如:熱部署工具JRebel,springboot的熱部署外掛,各種線上診斷工具(btrace, greys),阿里開源的arthas等等。

其實java Agent在JDK1.5以後,我們可以使用agent技術構建一個獨立於應用程式的代理程式(即Agent),用來協助監測、執行甚至替換其他JVM上的程式。使用它可以實現虛擬機器級別的AOP功能,並且這種方式一個典型的優勢就是無程式碼侵入。

Agent分為兩種:

1、在主程式之前執行的Agent,

2、在主程式之後執行的Agent(前者的升級版,1.6以後提供)。

2.2 javaAgent入門

2.2.1 premain

premain:主程式之前執行的Agent

在實際使用過程中,javaagent是java命令的一個引數。通過java 命令啟動我們的應用程式的時候,可通過引數 -javaagent 指定一個 jar 包(也就是我們的代理agent),能夠實現在我們應用程式的主程式執行之前來執行我們指定jar包中的特定方法,在該方法中我們能夠實現動態增強Class等相關功能,並且該 jar包有2個要求:

  1. 這個 jar 包的 META-INF/MANIFEST.MF 檔案必須指定 Premain-Class 項,該選項指定的是一個類的全路徑
  2. Premain-Class 指定的那個類必須實現 premain() 方法。

    META-INF/MANIFEST.MF

    Manifest-Version: 1.0
    Can-Redefine-Classes: true
    Can-Retransform-Classes: true
    Premain-Class: com.itheima.PreMainAgent

    注意:最後需要多一行空行

    Can-Redefine-Classes :true表示能重定義此代理所需的類,預設值為 false(可選)
    Can-Retransform-Classes :true 表示能重轉換此代理所需的類,預設值為 false (可選)
    Premain-Class :包含 premain 方法的類(類的全路徑名)
    

從字面上理解,Premain-Class 就是執行在 main 函式之前的的類。當Java 虛擬機器啟動時,在執行 main 函式之前,JVM 會先執行 -javaagent 所指定 jar 包內 Premain-Class 這個類的 premain 方法 。

我們可以通過在命令列輸入 java 看到相應的引數,其中就有和java agent相關的

在上面 -javaagent 引數中提到了參閱 java.lang.instrument ,這是在 rt.jar 中定義的一個包,該路徑下有兩個重要的類:

該包提供了一些工具幫助開發人員在 Java 程式執行時,動態修改系統中的 Class 型別。其中,使用該軟體包的一個關鍵元件就是 Javaagent,如果從本質上來講,Java Agent 是一個遵循一組嚴格約定的常規 Java 類。 上面說到 javaagent命令要求指定的類中必須要有premain()方法,並且對premain方法的簽名也有要求,簽名必須滿足以下兩種格式:

public static void premain(String agentArgs, Instrumentation inst)
    
public static void premain(String agentArgs)

JVM 會優先載入 帶 Instrumentation 簽名的方法,載入成功忽略第二種,如果第一種沒有,則載入第二種方法

demo:

1、在 agent-demo 中新增如下座標

<build>
    <plugins>
        <plugin>
            <artifactId>maven-assembly-plugin</artifactId>
            <configuration>
                <appendAssemblyId>false</appendAssemblyId>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
                <archive> <!--自動新增META-INF/MANIFEST.MF -->
                    <manifest>
                        <!-- 新增 mplementation-*和Specification-*配置項-->
                        <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                        <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
                    </manifest>
                    <manifestEntries>
                        <!--指定premain方法所在的類-->
                        <Premain-Class>com.itheima.agent.PreMainAgent</Premain-Class>
                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                    </manifestEntries>
                </archive>
            </configuration>
            <executions>
                <execution>
                    <id>make-assembly</id>
                    <phase>package</phase>
                    <goals>
                        <goal>single</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

2、編寫一個agent程式: com.itheima.agent.PreMainAgent ,完成 premain 方法的簽名,先做一個簡單的輸出

package com.itheima.agent;

import java.lang.instrument.Instrumentation;

public class PreMainAgent {

    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("我的agent程式跑起來啦!");
        System.out.println("收到的agent引數是:"+agentArgs);
    }
}

下面先來簡單介紹一下 Instrumentation 中的核心 API 方法:

  • addTransformer()/removeTransformer() 方法 :註冊/登出一個 ClassFileTransformer 類的例項,該 Transformer 會在類載入的時候被呼叫,可用於修改類定義(修改類的位元組碼)。
  • redefineClasses() 方法 :該方法針對的是已經載入的類,它會對傳入的類進行重新定義。
  • getAllLoadedClasses()方法: 返回當前 JVM 已載入的所有類。
  • getInitiatedClasses() 方法 :返回當前 JVM 已經初始化的類。
  • getObjectSize()方法 :獲取引數指定的物件的大小。

3、對 agent-demo 專案進行打包,得到 agent-demo-1.0-SNAPSHOT.jar

4、建立 agent-test 專案,編寫一個類: com.itheima.Application

package com.itheima;

public class Application {

    public static void main(String[] args) {
        System.out.println("main 函式 運行了 ");
    }
}

5、啟動執行,新增 -javaagent 引數

-javaagent:/xxx.jar=option1=value1,option2=value2

執行結果為:

我的agent程式跑起來啦!
收到的agent引數是:k1=v1,k2=v2
main 函式 運行了

總結:

這種agent JVM 會先執行 premain 方法,大部分類載入都會通過該方法,注意:是大部分,不是所有。當然,遺漏的主要是系統類,因為很多系統類先於 agent 執行,而使用者類的載入肯定是會被攔截的。也就是說,這個方法是在 main 方法啟動前攔截大部分類的載入活動,既然可以攔截類的載入,那麼就可以去做重寫類這樣的操作,結合第三方的位元組碼編譯工具,比如ASM,bytebuddy,javassist,cglib等等來改寫實現類。

2.2.2 agentmain(自學)

agentmain:主程式之後執行的Agent

上面介紹的是在 JDK 1.5中提供的,開發者只能在main載入之前新增手腳,在 Java SE 6 中提供了一個新的代理操作方法:agentmain,可以在 main 函式開始執行之後再執行。

premain 函式一樣, 開發者可以編寫一個含有 agentmain 函式的 Java 類,具備以下之一的方法即可

public static void agentmain (String agentArgs, Instrumentation inst)

public static void agentmain (String agentArgs)

同樣需要在MANIFEST.MF檔案裡面設定“Agent-Class”來指定包含 agentmain 函式的類的全路徑。

1:在agentdemo中建立一個新的類: com.itheima.agent.AgentClass ,並編寫方法agenmain

/**
 * Created by 傳智播客*黑馬程式設計師.
 */
public class AgentClass {

    public static void agentmain (String agentArgs, Instrumentation inst){
        System.out.println("agentmain runing");
    }
}

2:在pom.xml中新增配置如下

<plugin>
    <artifactId>maven-assembly-plugin</artifactId>
    <configuration>
        <appendAssemblyId>false</appendAssemblyId>
        <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
        <archive> <!--自動新增META-INF/MANIFEST.MF -->
            <manifest>
                <!-- 新增 mplementation-*和Specification-*配置項-->
                <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
            </manifest>
            <manifestEntries>
                <!--指定premain方法所在的類-->
                <Premain-Class>com.itheima.agent.PreMainAgent</Premain-Class>
                <!--新增這個即可-->
                <Agent-Class>com.itheima.agent.AgentClass</Agent-Class>
                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
            </manifestEntries>
        </archive>
    </configuration>
    <executions>
        <execution>
            <id>make-assembly</id>
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
</plugin>

3:對 agent-demo 重新打包

4:找到 agent-test 中的Application,修改如下:

public class Application {

    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        System.out.println("main 函式 運行了 ");

        //獲取當前系統中所有 執行中的 虛擬機器
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vm : list) {
            if (vm.displayName().endsWith("com.itheima.Application")) {
                VirtualMachine virtualMachine = VirtualMachine.attach(vm.id());
                virtualMachine.loadAgent("D:/agentdemo.jar");
                virtualMachine.detach();
            }
        }
    }
}

list()方法會去尋找當前系統中所有執行著的JVM程序,你可以列印 vmd.displayName() 看到當前系統都有哪些JVM程序在執行。因為main函式執行起來的時候程序名為當前類名,所以通過這種方式可以去找到當前的程序id。

注意:在mac上安裝了的jdk是能直接找到 VirtualMachine 類的,但是在windows中安裝的jdk無法找到,如果你遇到這種情況,請手動將你jdk安裝目錄下:lib目錄中的tools.jar新增進當前工程的Libraries中。

之所以要這樣寫是因為:agent要在主程式執行後加載,我們不可能在主程式中編寫載入的程式碼,只能另寫程式,那麼另寫程式如何與主程式進行通訊?這裡用到的機制就是attach機制,它可以將JVM A連線至JVM B,併發送指令給JVM B執行。

總結:

以上就是Java Agent的倆個簡單小栗子了,Java Agent十分強大,它能做到的不僅僅是列印幾個監控數值而已,還包括使用Transformer等高階功能進行類替換,方法修改等,要使用Instrumentation的相關API則需要對位元組碼等技術有較深的認識。

2.3 agent 案例

需求:通過 java agent 技術實現一個統計方法耗時的案例

1、在 agent-test 專案中新增方法: com.itheima.driver.DriverService

package com.itheima.driver;

import java.util.concurrent.TimeUnit;

public class DriverService {


    public void didi() {
        System.out.println("di di di ------------");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void dada() {
        System.out.println("da da da ------------");
        try {
            TimeUnit.SECONDS.sleep(6);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

並在 com.itheima.Application 進行方法的呼叫

package com.itheima;

import com.itheima.service.DriverService;

public class Application {

    public static void main(String[] args) {
        System.out.println("main 函式 運行了 ");
        DriverService driverService = new DriverService();
        driverService.didi();
        driverService.dada();
    }
}

2、在 agent-demo 中改造 com.itheima.agent.PreMainAgent

public class PreMainAgent {

    /***
     * 執行方法攔截
     * @param agentArgs:-javaagent 命令攜帶的引數。在前面介紹 SkyWalking Agent 接入時提到
     *                 agent.service_name 這個配置項的預設值有三種覆蓋方式,
     *                 其中,使用探針配置進行覆蓋,探針配置的值就是通過該引數傳入的。
     * @param inst:java.lang.instrumen.Instrumentation 是 Instrumention 包中定義的一個介面,它提供了操作類定義的相關方法。
     */
    public static void premain(String agentArgs, Instrumentation inst){
        //動態構建操作,根據transformer規則執行攔截操作
        AgentBuilder.Transformer transformer = new AgentBuilder.Transformer() {
            @Override
            public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
                                                    // 匹配上的具體的型別描述
                                                    TypeDescription typeDescription,
                                                    ClassLoader classLoader,
                                                    JavaModule javaModule) {
                //構建攔截規則
                return builder
                        //method()指定哪些方法需要被攔截,ElementMatchers.any()表示攔截所有方法
                        .method(ElementMatchers.<MethodDescription>any())
                        //intercept()指定攔截上述方法的攔截器
                        .intercept(MethodDelegation.to(TimeInterceptor.class));
            }
        };

        //採用Byte Buddy的AgentBuilder結合Java Agent處理程式
        new AgentBuilder
                //採用ByteBuddy作為預設的Agent例項
                .Default()
                //攔截匹配方式:類以com.itheima.driver開始(其實就是com.itheima.driver包下的所有類)
                .type(ElementMatchers.nameStartsWith("com.itheima.driver"))
                //攔截到的類由transformer處理
                .transform(transformer)
                //安裝到 Instrumentation
                .installOn(inst);
    }
}

agent-demo 專案中,建立 com.itheima.service.TimeInterceptor 實現統計攔截,程式碼如下:

public class TimeInterceptor {

    /***
     * 攔截方法
     * @param method:攔截的方法
     * @param callable:呼叫物件的代理物件
     * @return
     * @throws Exception
     */
    @RuntimeType // 宣告為static
    public static Object intercept(@Origin Method method,
                                   @SuperCall Callable<?> callable) throws Exception {
        //時間統計開始
        long start = System.currentTimeMillis();
        // 執行原函式
        Object result = callable.call();
        //執行時間統計
        System.out.println(method.getName() + ":執行耗時" + (System.currentTimeMillis() - start) + "ms");
        return result;
    }
}

3、重新打包 agent-demo ,然後再次測試執行 agent-test 中的主類 Application

試效果如下:

premain 執行了
main 函式 運行了 
di di di ------------
didi:執行耗時5009ms
da da da ------------
dada:執行耗時6002ms

本文由傳智教育博學谷 - 狂野架構師教研團隊釋出,轉載請註明出處!

如果本文對您有幫助,歡迎關注和點贊;如果您有任何建議也可留言評論或私信,您的支援是我堅持創作的動力