Java Agent技術

来源:https://www.cnblogs.com/qisi/archive/2023/09/10/java_agent.html
-Advertisement-
Play Games

在定位公司問題的時候,需要瞭解一下skywalking的相關知識,而agent就提上了日程。 官網文檔 Agent技術是Jdk在1.5版本之後,所提供的一個在jvm啟動前後對部分java類代理加強的機制。由於是直接修改位元組碼,並不會對業務代碼有註入,所以可以很好的應用於監控或者熱部署等場景。 正常所 ...


在定位公司問題的時候,需要瞭解一下skywalking的相關知識,而agent就提上了日程。

官網文檔
Agent技術是Jdk在1.5版本之後,所提供的一個在jvm啟動前後對部分java類代理加強的機制。由於是直接修改位元組碼,並不會對業務代碼有註入,所以可以很好的應用於監控或者熱部署等場景。
正常所提到的Agent一般都是部署成jar包的樣子,比如agent-1.0-SNAPSHOT.jar。
在這個jar包中,要添加一個MANIFEST.MF文件,在文件中指定jar包的代理類,比如下麵代碼中的Premain-Class。
在對應的代理類,要實現一個permain方法或者agentmain方法,這樣jvm可以通過MANIFEST找到類,通過類再找到對應的方法,從而進行加強,所以加強邏輯是在permain方法或者agentmain方法內部實現的。

Manifest-Version: 1.0
Built-By: qisi
Premain-Class: com.qisi.agent.InterviewAgent
Agent-Class: com.qisi.agent.InterviewAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Class-Path: byte-buddy-1.10.22.jar
Created-By: Apache Maven 3.8.1
Build-Jdk: 1.8.0_332
public class InterviewAgent {
    public static void premain(String agentArgs, Instrumentation instrumentation) {
    }
    public static void agentmain(String agentArgs, Instrumentation instrumentation) {
    }
}

而如果在permain或者agentmain方法打上debug可以發現,執行時是通過sun.instrument.InstrumentationImpl#loadClassAndCallPremain和sun.instrument.InstrumentationImpl#loadClassAndCallAgentmain兩個方法通過反射來執行到我們指定的類的。
Agent技術有兩種場景,一種是在jvm啟動之前,通過-javaagent:path來指定jar包,像是skywalking就是採用的這種方式;另一種則是在jvm啟動之後,通過attach指定的進程,對jvm中的類進行加強,arthas就是採用的這種方式。
在具體介紹這兩種方式之前,需要先講一下Instrumentation相關類和介面

java.lang.Instrumentation

Instrumentation

Instrumentation相關的類都在java.lang.Instrumentation包下,兩個異常,兩個介面,一個類。
image
兩個異常在這裡不做介紹,功能就像類名一樣。核心的其實是Instrumentation介面,本文僅關註紅框內的幾個方法。這幾個方法都是通過permain和agentmain獲取到的instrumentation實例進行的操作。
image
從時間發展來看,其中jdk1.5開始支持的是下麵幾個方法,也就是說在jdk5的時候,僅支持添加和移除類轉換器,且添加的類轉換器只能在載入和重定義的時候使用。就是說如果類沒有載入,那麼通過addTransformer方法註冊的ClassFileTransformer就可以對這個類進行增強,否則一旦類已經載入完畢,則只能通過redefineClasses,完全替換類定義再次觸發loadClass來增強

addTransformer(ClassFileTransformer transformer)
removeTransformer(ClassFileTransformer transformer)
isRedefineClassesSupported();//依賴於MANIFEST中的Can-Redefine-Classes值
redefineClasses(ClassDefinition... definitions)

而從jdk1.6開始,增加了一個retransformClasses的概念。retransform和redefine的區別,前者是在原有類的基礎上進行修改,後者則是完全重定義,不使用原有類做任何參考。
需要註意的事,只有在首次調用addTransformer時,將canRetransform設置為true的類,才可以被重新轉換。

addTransformer(ClassFileTransformer transformer, boolean canRetransform);
isRetransformClassesSupported();
retransformClasses(Class<?>... classes)//依賴於MANIFEST中的Can-Retransform-Classes值
isModifiableClass(Class<?> theClass);

ClassFileTransformer、ClassDefinition

這兩個類其實都是Instrumentation介面方法的入參,其中用的比較多的應該是ClassFileTransformer。這個類只有一個transform,jvm類載入的時候都會調用一遍這個方法。如果需要加強,那麼就利用給定的參數,進行位元組碼的改動,將改動後的位元組碼作為返回值返回;如果無需增強,則直接返回null即可。

byte[]
transform(  ClassLoader         loader,
            String              className,
            Class<?>            classBeingRedefined,
            ProtectionDomain    protectionDomain,
            byte[]              classfileBuffer)

ClassDefinition也類似,不過是在對象里重新綁定class和byte的關係

public final class ClassDefinition {
    /**
     *  The class to redefine
     */
    private final Class<?> mClass;

    /**
     *  The replacement class file bytes
     */
    private final byte[]   mClassFile;

實踐

MANIFEST.MF配置

在pom文件中添加下麵的代碼,根據需要修改參數值

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <configuration>
                <archive>
                    <addMavenDescriptor>false</addMavenDescriptor>
                    <manifest>
                        <addClasspath>true</addClasspath>
                    </manifest>
                    <manifestEntries>
                        <Premain-Class>
                            com.qisi.agent.InterviewByteButtyAgent
                        </Premain-Class>
                        <Agent-Class>
                            com.qisi.agent.InterviewByteButtyAgent
                        </Agent-Class>
                        <Can-Redefine-Classes>
                            true
                        </Can-Redefine-Classes>
                        <Can-Retransform-Classes>
                            true
                        </Can-Retransform-Classes>
                        <Built-By>
                            qisi
                        </Built-By>
                    </manifestEntries>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

-javaagent:

在這種方式下,起作用的是permain,也就是說-javaagent和permain方法是配套使用的。
核心就是添加一個自定義的ClassFileTransformer,可以另起一個類,也可以這樣匿名類。
如果只是熟悉流程可以像下麵一樣,直接列印一些日誌,不去修改類;

    public static void premain(String agentArgs, Instrumentation instrumentation) {
        System.out.println("enhance by premain,params:"+agentArgs);
        instrumentation.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                                    ProtectionDomain protectionDomain, byte[] classfileBuffer)
                    throws IllegalClassFormatException {
                System.out.println("premain load Class     :" + className);
                return classfileBuffer;
            }
        }, true);
    }

如果要真實修改,需要引入asm javassist bytebuddy等修改位元組碼的框架。下麵這部分就是使用了bytebuddy,作用是讓任何類的testAgent方法,都返回固定值transformed

public static void premain(String agentArgs, Instrumentation instrumentation) throws ClassNotFoundException {
    System.out.println("enhance by permain InterviewByteButtyAgent,params:"+agentArgs);
    new AgentBuilder.Default().type(any()).transform(new AgentBuilder.Transformer() {
        @Override
        public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module) {
            return builder.method(named("testAgent"))
                    .intercept(FixedValue.value("transformed"));
        }
    }).installOn(instrumentation);
}

編寫完之後,就可以在任意項目添加一個存在testAgent方法的進行嘗試了,比如
java -javaagent:/xxxx/path/agent-1.0-SNAPSHOT.jar=key1:value1,key2:value2 -jar AppDemo.jar

attach

agentmain

這種方式需要實現agentmain方法,和permian不太一樣的地方是需要在addTransformer之後觸發需要retransformClasses想要加強的類。

public static void agentmain(String agentArgs, Instrumentation instrumentation) {
    System.out.println("enhance by agentmain,params:"+agentArgs);
    instrumentation.addTransformer(new ClassFileTransformer() {
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                                ProtectionDomain protectionDomain, byte[] classfileBuffer)
                throws IllegalClassFormatException {
            System.out.println("agentmain load Class     :" + className);
            return classfileBuffer;
        }
    }, true);
    try {
        instrumentation.retransformClasses(Class.forName("com.qisi.mybatis.app.controller.FirstRequestController"));
    } catch (UnmodifiableClassException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

同樣,提供一個bytebuddy的例子,下麵這個則是指定修改FirstRequestController的testAgent方法的返回值為transformed

public static void agentmain(String agentArgs, Instrumentation instrumentation) throws ClassNotFoundException {
    System.out.println("enhance by agentmain InterviewByteButtyAgent,params:"+agentArgs);
    //這裡RedefinitionStrategy必須註意,預設的DISABLED是不支持retransform
    new AgentBuilder.Default().with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION).type(new AgentBuilder.RawMatcher() {
        @Override
        public boolean matches(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, Class<?> classBeingRedefined, ProtectionDomain protectionDomain) {
            return typeDescription.getName().contains("FirstRequestController");
        }
    }).transform(new AgentBuilder.Transformer() {
        @Override
        public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module) {
            System.out.println("enhance"+typeDescription.getName());
            return builder.method(named("testAgent"))
                    .intercept(FixedValue.value("transformed"));
        }
        //這裡採用disableClassFormatChanges的方案,好像還可以使用advice
    }).disableClassFormatChanges().installOn(instrumentation);
    try {
        instrumentation.retransformClasses(Class.forName("com.qisi.mybatis.app.controller.FirstRequestController"));
    } catch (UnmodifiableClassException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

VirtualMachine

不同於-javaagent命令,這裡需要使用自jdk6開始提供的VirtualMachine類,在tool.jar包里
下麵的方法是我參考arthas寫的一個attach的流程,選擇我們想要attach的進程,然後載入我們上面寫好的jar包就好了。

public class AgentTest {
    public static void main(String[] args) throws IOException, AttachNotSupportedException {
        String pid = null;
        try {
            Process jps = Runtime.getRuntime().exec("jps");
            InputStream inputStream = jps.getInputStream();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
            System.out.println("選擇要attach的進程");
            pid= new Scanner(System.in).nextLine();
            System.out.println("選擇的pid是"+pid);
        } catch (IOException e) {
            e.printStackTrace();
        }
        for (VirtualMachineDescriptor virtualMachineDescriptor : VirtualMachine.list()) {
            if (virtualMachineDescriptor.id().equals(pid)){
                VirtualMachine attach = VirtualMachine.attach(virtualMachineDescriptor);
                try {
                    attach.loadAgent("/xxxxx/agent/target/agent-1.0-SNAPSHOT.jar","參數1,參數2");
                } catch (AgentLoadException e) {
                    e.printStackTrace();
                } catch (AgentInitializationException e) {
                    e.printStackTrace();
                } finally {
                    attach.detach();
                }
                break;
            }
        }
    }
}

參考文檔:

探秘 Java 熱部署二(Java agent premain)
JAVA熱更新1:Agent方式熱更 | 花隱間-JAVA游戲技術解決方案
ByteBuddy入門教程

本文來自博客園,作者:起司啊,轉載請註明原文鏈接:https://www.cnblogs.com/qisi/p/java_agent.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1. mysql資料庫四種常見資料庫引擎 1. MyISAM: MyISAM是MySQL最早的資料庫引擎之一。它被設計成處理大量的插入和查詢操作。MyISAM表格的數據存儲在三個文件上:.frm文件存儲表結構,.MYD文件存儲數據,.MYI文件存儲索引。MyISAM表格不支持事務處理和崩潰恢復,因此 ...
  • 隨著需求的不斷開發,前端項目不斷膨脹,業務提出:你們的首頁載入也太慢啦,我都需要7、8秒才能看到內容,於是乎主管就讓我聯合後端開啟優化專項,目標是3s內展示完全首頁的內容。 性能指標 開啟優化時,我們要清晰的知道現狀和目標,以及我們採用什麼樣的手段,通過檢測什麼指標來查看到優化的過程。 結果指標 根 ...
  • 功能介紹 登錄 首頁 修改密碼 提交申請 提交列表 數據可視化 審核列表 前端 components結構 搭建Vue項目 ​ Vue3快速上手: ​ https://cn.vuejs.org/guide/quick-start.html#creating-a-vue-application 頁面佈局 ...
  • 溫馨提示:本文以vue3+vite+ts舉例,vite配置和ts語法側重較少,比較適合有vuex或者vue基礎的小伙伴們兒查閱。 安裝pinia yarn yarn add pinia npm npm install pinia pnpm pnpm add pinia 1-開始 方式一:在main. ...
  • 官方代碼是直接使用JDK的Deque對象,這樣的代碼能學到什麼?熟練操作API嗎?還是自己實現一個最小棧吧,用時擊敗100%,記憶體擊敗78% ...
  • 在您的應用程式中,由Spring IoC容器管理的形成其核心的對象被稱為"bean"。一個bean是由Spring IoC容器實例化、組裝和管理的對象。這些bean是通過您提供給容器的配置元數據創建的,例如,在前面章節中已經看到的XML <bean/> 定義。 Bean定義包含了所謂的配置元數據,容 ...
  • 因為Docker Hub無法打開,Jupyter Notebook等官方鏡像也無法下載,所以另闢蹊徑下載了熱門的Python3基礎鏡像,從頭開始安裝純凈版本的Jupyter Notebook環境,本文記錄了完整的Jupyter Notebook安裝過程,方便自己查閱,也供其他人員參考,請確保當前已有 ...
  • 1 Nacos ⼀致性協議 1.1 為什麼 Nacos 需要⼀致性協議 Nacos儘可能減少用戶部署以及運維成本,做到用戶只需要⼀個程式包,就快速單機模式啟動 Nacos 或集群模式啟動 Nacos。而 Nacos 是⼀個需要存儲數據的組件,為實現目標,就要在 Nacos 內部實現數據存儲。單機問題 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...