天天看點

位元組碼增強技術之 Java Agent 入門

前言

分布式鍊路追蹤中為了擷取服務之間調用鍊資訊,采集器通常需要在方法的前後做埋點。在 Java 生态中,常見的埋點方式有兩種:

  1. 依賴 SDK 手動埋點;
  2. 利用 Java Agent 技術來做無侵入埋點。

我們所熟知的分布式監控系統,是 Zipkin 開始的,最經典的是搞懂 X-B3 Ttrace 協定,使用 Brave SDK,手動埋點生成 Trace。但是 SDK 埋點的方式,對業務代碼存在侵入性,當更新埋點時,必須要做代碼的變更。

那麼如何和業務邏輯解綁呢?

Java 還提供了另外一種方式:依賴 Java Agent 技術,修改目标方法的位元組碼,做到無侵入的埋點。這種利用 Java Agent 的方式的采集器,也叫做探針。在應用程式啟動時使用 -javaagent 參數 ,或者運作時使用 attach(pid) 方式,就可以将探針包注入目标應用程式,完成埋點的植入。對業務代碼無侵入的方式,可以做到無感的熱更新。使用者不需要了解深層的原理,就可以使用完整的監控服務

關于位元組碼的基礎知識可以參考美團的這篇文章:

Java Agent 簡介

Java Agent 是 Java 1.5 版本之後引⼊的特性,其主要作⽤是在 class 被加載之前對其攔截,已插⼊我們的監聽位元組碼。使用 Java 的Instrumentation 接口(java.lang.instrument)來編寫 Agent。

基本的思路是在 JVM 啟動的時候添加一個代理(Java Agent),每個代理是一個 Jar 包,其 MANIFEST.MF 檔案裡指定了代理類,這個代理類包含一個 premain 方法。JVM 在類加載時候會先執行代理類的 premain 方法,再執行 Java 程式本身的 main 方法,這就是 premain 名字的來源。在 premain 方法中可以對加載前的 class 檔案進行修改。

這種機制可以認為是虛拟機級别的 AOP,無需對原有應用做任何修改,就可以實作類的動态修改和增強。

從 JDK 1.6 開始支援更加強大的動态 Instrument,在JVM 啟動後通過 Attach(pid) 遠端加載。

注意:

無論是通過 Native 的方式還是通過 Java Instrumentation 接口的方式來編寫 Agent,它們的工作都是借助 JVMTI 來進行完成。JVMTI 是一套 Native 接口,在 Java 1.5 之前,要實作一個 Agent 隻能通過編寫Native 代碼來實作。

Java Instrumentation 核心方法

Instrumentation 是 java.lang.instrument 包下的一個接口,這個接口的方法提供了注冊類檔案轉換器、擷取所有已加載的類等功能,允許我們在對已加載和未加載的類進行修改,實作 AOP、性能監控等功能。

常用方法:

/**
 * 為 Instrumentation 注冊一個類檔案轉換器,可以修改讀取類檔案位元組碼
 */
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

/**
 * 對JVM已經加載的類重新觸發類加載
 */
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

/**
 * 擷取目前 JVM 加載的所有類對象
 */
Class[] getAllLoadedClasses()           

它的 addTransformer 給 Instrumentation 注冊一個 transformer,transformer 是 ClassFileTransformer 接口的執行個體,這個接口就隻有一個 transform 方法,調用 addTransformer 設定 transformer 以後,後續 JVM 加載所有類之前都會被這個 transform 方法攔截,這個方法接收原類檔案的位元組數組,傳回轉換過的位元組數組,在這個方法中可以做任意的類檔案改寫。

public class MyClassTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classBytes) throws IllegalClassFormatException {
        // 在這裡讀取、轉換類檔案
        return classBytes;
    }
}           

Java Agent 核心流程

Java Agent 裝載時序圖(premain):

位元組碼增強技術之 Java Agent 入門

Class 裝載時序圖:

位元組碼增強技術之 Java Agent 入門

Java Agent 所使用的 Instrumentation 依賴 JVMTI 實作,當然也可以繞過 Instrumentation 直接使用 JVMTI 實作 Agent。JVMTI 與 JDI 組成了 Java 平台調試體系(JPDA)的主要能力。

Java Agent 使⽤

Java Agent 其實就是⼀個特殊的 Jar 包,它并不能單獨啟動的,而必須依附于一個 JVM 程序,可以看作是 JVM 的一個寄生插件,使用 Instrumentation 的 API 用來讀取和改寫目前 JVM 的類文,通過 -javaagent:xxx.jar 引⼊⽬标應⽤。

那這個Jar 和 普通的 Jar 有什麼差別麼?

位元組碼增強技術之 Java Agent 入門

Agent 需要打包成一個jar包,在 Maininfe.MF 屬性中指定“Premain-Class”或者“Agent-Class”,且需根據需求定義 Can-Redefine-Classes 和 Can-Retransform-Classes。

Java Agent Jar 包 MANIFEST.MF 配置參數:

Manifest-Version: 1.0
#動态 agent 類 
Agent-Class: com.zuozewei.javaagent01.Agent 
#靜态 agent 類
Premain-Class: com.zuozewei.javaagent01.Agent
是否允許重複裝載
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_112           

demo 預演

1、建立 POM 項目 Java Agent,項目結構如下:

位元組碼增強技術之 Java Agent 入門

2、修改 pom 檔案:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>parent</artifactId>
        <groupId>com.zuozewei</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <packaging>jar</packaging>

    <artifactId>javaagent01</artifactId>

    <build>
        <finalName>agent</finalName>

        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.3.1</version>
                <configuration>
                    <archive>
                        <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                </configuration>
            </plugin>

            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <outputDirectory>${basedir}</outputDirectory>
                    <archive>
                        <index>true</index>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Premain-Class>com.zuozewei.javaagent01.Agent</Premain-Class>
                        </manifestEntries>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>           

3、建立 AgentMain 類,實作控制台列印,addTransformer 給 Instrumentation 注冊一個 transformer。

package com.zuozewei.javaagent01;

import java.lang.instrument.Instrumentation;

public class Agent {

//    public static void premain(String agentArgs) {
//        System.out.println("我是一個萌萌哒的 Java Agent");
//        try {
//            Thread.sleep(2000L);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
//    }

    public static void premain(String agentArgs, Instrumentation instrumentation)  {

        instrumentation.addTransformer(new ClassFileTransformerDemo());

        System.out.println("7DGroup Java Agent");
    }

}           

4、建立 ClassFileTransformerDemo 類,攔截并列印所有類名。

package com.zuozewei.javaagent01;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class ClassFileTransformerDemo implements ClassFileTransformer {

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)  {
        System.out.println("className: " + className);
        if (!className.equalsIgnoreCase("com/zuozewei/Dog")) {
            return null;
        }
        return getBytesFromFile("/Users/zuozewei/IdeaProjects/javaagent/example01/target/classes/com/zuozewei/Dog.class");
    }

    public static byte[] getBytesFromFile(String fileName) {
        File file = new File(fileName);
        try (InputStream is = new FileInputStream(file)) {
            // precondition

            long length = file.length();
            byte[] bytes = new byte[(int) length];

            // Read in the bytes
            int offset = 0;
            int numRead = 0;
            while (offset <bytes.length
                    && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
                offset += numRead;
            }

            if (offset < bytes.length) {
                throw new IOException("Could not completely read file "
                        + file.getName());
            }
            is.close();
            return bytes;
        } catch (Exception e) {
            System.out.println("error occurs in _ClassTransformer!"
                    + e.getClass().getName());
            return null;
        }
    }

}           

5、定義需要修改的項目 example01

位元組碼增強技術之 Java Agent 入門

6、實作需要修改的類的。

main:

package com.zuozewei;

public class Main {

    public static void main(String[] args) {
        System.out.println("7DGroup");

        System.out.println(new Dog().hello());
//        System.out.println(new Cat().hello());
    }

}           

Dog:

package com.zuozewei;

public class Dog {

    public int hello() {
        return 0;
    }

}           

7、運作 example01 的 main 方法:

位元組碼增強技術之 Java Agent 入門

8、打包 javaagent 項目生成 jar 檔案,并将 java 檔案同 example01 項目的 jar 放在同一個目錄下如上圖(放在同一個目錄為了友善執行)

執行如下指令:

java -jar -javaagent:agent.jar  example.jar           

實作了我們的功能,執行結果如下:

位元組碼增強技術之 Java Agent 入門

總結

本文詳細介紹 Java Agent 啟動加載實作位元組碼增強關鍵技術的實作細節,位元組碼增強技術為測試人員進行性能監控提供了一種新的思路。目前衆多開源監控産品已經提供了豐富的 Java 探針庫,作為監控服務的提供者,進一步降低了開發成本,不過開發門檻比較高,對測試人員來說有很大的一部分的學習成本。

源碼位址: