字節(jié)碼增強技術-Javassist

字節(jié)碼

什么是字節(jié)碼,在這里就不在贅述了,網(wǎng)上教程很多。Java 為了能讓 Java 程序編譯一次到處運行,用 Java 編譯器將程序?qū)υ创a編譯生成固定格式的字節(jié)碼(.class文件)供 JVM 使用,因此理論上來說,只要符合 JVM 規(guī)范的字節(jié)碼文件,就可以在 JVM 上運行,不同的 JVM 類語言(如Scala、Groovy、Kotlin)編譯成字節(jié)碼都可在 JVM 運行,除此之外,如果你對 JVM 的字節(jié)碼規(guī)范非常了解的話,通過自己按照 JVM 規(guī)范自己寫也是可以的。

那么什么是字節(jié)碼增強呢?簡單理解就是通過某種手段或者技術修改編譯好的字節(jié)碼,讓新生成的字節(jié)碼能滿足我們的定制需求,這里說的需求就有很多了,比如常用的 AOP 底層很多就是使用字節(jié)碼增強來達到切面攔截,再比如微服務中的鏈路追蹤就使用了字節(jié)碼增強(僅僅只一些 Java 客戶端)來進行埋點標記來記錄調(diào)用鏈關系的,所以了解字節(jié)碼增強對一些框架能有更深入對理解,對問題排查有很大對幫助。

上面說的通過某種手段或者技術到底指哪些呢?我們最常用的 Java Proxy 也是一種增強技術,另外常用的還有 ASM,AspectJ,Javassist等常用的技術,其中ASM在指令層次操作字節(jié)碼的,需要對JVM的指令有一定的了解,同時眾多的指令也很難記住,操作比較高;AspectJ擴展了 Java,定義了一些專門的AOP語法,其中 Spring AOP 就使用了 AspectJ;Javassist 是強調(diào)源代碼層次操作字節(jié)碼的框架,操作起來很容易入手。

Javassist

使用Javassist需要使用javassist.jar。

優(yōu)勢:

  • 操作簡單,容易上手
  • 性能高于反射

缺點:

  • 性能相比 ASM ,會低一些
  • 不支持 continue 和 break 表達式,不支持內(nèi)部類和匿名類,因此在有些場景是不適合的

Javassist 使用 ClassPool 來操作所有的 Java 類。這個類的工作方式與 JVM 類裝載器非常相似,但是有一個重要的區(qū)別是它不是將裝載的、要執(zhí)行的類作為應用程序的一部分鏈接,類池使所裝載的類可以通過 Javassist API 作為數(shù)據(jù)使用??梢允褂媚J的類池(ClassPool.getDefault()),它是從 JVM 搜索路徑中裝載的,也可以定義一個搜索您自己的路徑列表的類池。甚至可以直接從字節(jié)數(shù)組或者流中裝載二進制類,以及從頭開始創(chuàng)建新類。

裝載到類池中的類由 CtClass 實例表示。與標準的 Class 類一樣, CtClass 提供了檢查類數(shù)據(jù)(如字段和方法)的方法。不過,這只是 CtClass 的部分內(nèi)容,它還定義了在類中添加新字段、方法和構造函數(shù)、以及改變類、父類和接口的方法。

字段、方法和構造函數(shù)分別由 CtField、CtMethod 和 CtConstructor 的實例表示。這些類定義了修改由它們所表示的對象的所有方法的方法,包括方法或者構造函數(shù)中的實際字節(jié)碼內(nèi)容。

Javassist 常用類的說明:

  • CtClass(compile-time class):編譯時類信息,它是一個class文件在代碼中的抽象表現(xiàn)形式,可以通過一個類的全限定名來獲取一個CtClass對象,用來表示這個類文件。
  • ClassPool:ClassPool是一張保存CtClass信息的HashTable,key為類的全限定名稱,value為類名對應的CtClass對象。當我們需要對某個類進行修改時,就是通過pool.getCtClass(“className”)方法從pool中獲取到相應的CtClass。
  • CtMethod、CtField:這兩個比較好理解,對應的是類中的方法和屬性,可以用于定義或者修改一些方法和字段。

Javassist 增強的代碼片段是使用字符串來編寫的,基本和平時寫的 Java 源代碼一致,主要的不同是一些是以 $ 開頭的標識符,用于表示方法或者構造函數(shù)參數(shù)、方法返回值等。

比如:


public void method1(String arg1, Object arg2) {
    // 增強代碼片段
    {
        System.out.println("入?yún)?1: " + $1); // arg1
        System.out.println("入?yún)?2: " + $2); // arg2
    }
}

入門 Demo

通過一個平時經(jīng)常需要用的業(yè)務日志記錄來學習,平時業(yè)務日志操作記錄基本都是通過 AOP 實現(xiàn)的,這次就使用字節(jié)碼增強技術來進行實現(xiàn),對業(yè)務代碼基本無任何侵入。

首先定義一個業(yè)務 service

public class BizService {
    public void bizProcess(Map map) {
        System.out.println("do biz process");
    }
}

編寫增強代碼片段

public class BizServiceInteceptor {

    public static void preProcess(Map map) {
        System.out.println("preProcess");
        // do log
    }
}

編寫測試類

public class JavassistTest {

    public static void main(String[] args) throws NotFoundException, CannotCompileException {
        ClassPool pool = ClassPool.getDefault(); // 獲取默認的類池
        CtClass clazz = pool.getOrNull("com.zmc.learning.javassist.BizService");
        if (clazz == null) {
            System.out.println("bizService not found");
            return;
        }
                // 獲取需要增強的代碼方法
        CtMethod bizProcessMethod = clazz.getDeclaredMethod("bizProcess");
                // 植入增強代碼片段
        StringBuffer sb = new StringBuffer();
        sb.append("{");
        sb.append("com.zmc.learning.javassist.BizServiceInteceptor.preProcess($1);"); //獲取入?yún)?        sb.append("}");
        bizProcessMethod.insertBefore(sb.toString()); // 是不是有點像 AOP 的 before?
                // 增強后的 class
        clazz.toClass();
        BizService bizService = new BizService();
        bizService.bizProcess(new HashMap());
    }
}

代碼也很簡單,需要注意的是,BizServiceInteceptor 是靜態(tài)方法,同時入?yún)⑿枰驮椒ㄒ恢?。這個 demo 是不是很 easy,看的出來 Javassist 在源代碼層面上操作字節(jié)碼對程序員還是很友好的。

Demo 2 增強 Http Client

需求:需求也比較簡單,在跨系統(tǒng)進行 Http 調(diào)用的時候,需要記錄請求的來源和調(diào)用的鏈路,如果是你你會怎么做呢。

正常硬編碼的方式

// 基于 Apache httpclient 進行 http 調(diào)用
// 調(diào)用方
{
    httpRequest.setHeader("source", "sys1");
}

// 處理方
{
    Header header = httpRequest.getHeaders("source")[0];
    String sourceSystem = header.getValue();
}

這樣也能比較容易的實現(xiàn)鏈路的記錄,但是這樣的方式明顯不適合。為什么呢?第一這段代碼其實跟業(yè)務關系不大,每個業(yè)務方都需要編寫 http 調(diào)用的時候加上這么一段前置的邏輯,仔細想想,如果有10個系統(tǒng)通過 http 相互調(diào)用呢。第二如果 A 系統(tǒng) set header 的 key 為 source,那下游也得知道你 set 的 key 值,同時下游再調(diào)用下游的的時候,如果它set 的 key 不是 source 呢,那是不是就不統(tǒng)一了,這時你可能會說各個系統(tǒng)協(xié)調(diào)好,統(tǒng)一不就好了,是的,這樣確實可以,那為什么不使用類似 AOP 一樣的技術進行攔截統(tǒng)一加呢,這樣又不會對代碼有侵入性。

使用Javassist處理

http client 依賴

<dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.8</version>
</dependency>

增強的代碼片段

public class HttpClientInterceptor {

    public static void intercept(HttpRequest httpRequest) {
        httpRequest.setHeader("source", "test");
    }
}

增強的工具類

public class HttpClientInstrumentation {

    private static final String ENHANCE_CLASS = "org.apache.http.impl.client.InternalHttpClient"; // 增強的 client
    private static final String ENHANCE_METHOD = "doExecute"; // 增強的方法

    public static void enhance() throws NotFoundException, CannotCompileException {
        ClassPool classPool = ClassPool.getDefault();
        CtClass ctClass = classPool.getOrNull(ENHANCE_CLASS);
        if (ctClass == null) {
            System.out.println("http client not found");
            return;
        }
        CtMethod doExecuteMethod = ctClass.getDeclaredMethod(ENHANCE_METHOD);
        String sb = "{" +
                "com.zmc.learning.javassist.HttpClientInterceptor.intercept" + "($2);" + // 獲取入?yún)?HttpRequest
                "}";
        doExecuteMethod.insertBefore(sb); // 植入代碼片段
        ctClass.toClass();
    }
}

測試類

public class HttpClientTest {

    public static void main(String[] args) throws NotFoundException, CannotCompileException {
        HttpClientInstrumentation.enhance();
        HttpGet httpGet = sendGet();
        Header header = httpGet.getHeaders("source")[0];
        System.out.println(header.getValue());
    }

    private static HttpGet sendGet() {
        //創(chuàng)建默認的httpClient實例
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpGet get = null;

        try {
            //用get方法發(fā)送http請求
            get = new HttpGet("http://www.baidu.com/");
            System.out.println("執(zhí)行get請求:...." + get.getURI());
            CloseableHttpResponse httpResponse = null;

//            get.setHeader("source", "abc");

            //發(fā)送get請求
            httpResponse = httpClient.execute(get);
            try {
                //response實體
                HttpEntity entity = httpResponse.getEntity();
                if (null != entity) {
                    System.out.println("響應狀態(tài)碼:" + httpResponse.getStatusLine());
                    System.out.println("-------------------------------------------------");
                    System.out.println("響應內(nèi)容:" + EntityUtils.toString(entity));
                    System.out.println("-------------------------------------------------");
                }
            } finally {
                httpResponse.close();
            }
        } catch (Exception ignore) {
            ;
        } finally {
            try {
                if (httpClient != null) {
                    httpClient.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return get;
    }
}

這樣一個 http client 的增強就好了,在業(yè)務方需要使用 http client 調(diào)用的系統(tǒng)中只需要在啟動的時候調(diào)用 HttpClientInstrumentation.enhance 方法即可,對業(yè)務方來說是透明的,需要注意的是為什么增強的目標方法是 doExecute 呢?因為這個方法是 http client 最底層的方法,上層的方法最后都會到這個方法來發(fā)起請求,因此攔截這個方法即可,另外,這個 demo 中只攔截了 InternalHttpClient 類,如果需要全面一點的話可能還需要攔截 MinimalHttpClient,AbstractHttpClient 等,需要注意的是這個僅僅對 http client 同步的客戶端進行了增強。

有了這個 demo 是不是對其他對跨系統(tǒng)調(diào)用的客戶端也可以實現(xiàn)呢,redis,kafka,nsq,grpc?大致的思路其實是一致的。說到這里再補一句,其實鏈路追蹤的框架調(diào)用鏈這塊大致的思路也是這樣的,通過增強各個中間件的客戶端來把系統(tǒng)的調(diào)用鏈串起來。

Demo 3 實現(xiàn)代理

參照Jdk Java Proxy 編寫一個動態(tài)代理的工具,Java Proxy 相關的請自行了解。

編寫ProxyFactory

public class ProxyFactory {

    public static Object newProxyInstance(ClassLoader classLoader, Class<?> interfaceClass, InvocationHandler h) throws Throwable {
        ClassPool pool = ClassPool.getDefault();
        
        // 1.創(chuàng)建代理類 ProxyClass
        CtClass proxyClass = pool.makeClass("ProxyClass");

        //  2.給代理類添加字段:private InvocationHandler handler;
        CtClass handlerCc = pool.get(InvocationHandler.class.getName());
        CtField handlerField = new CtField(handlerCc, "handler", proxyClass); // CtField(CtClass fieldType, String fieldName, CtClass addToThisClass)
        handlerField.setModifiers(AccessFlag.PRIVATE);
        proxyClass.addField(handlerField);

        // 3.添加構造函數(shù):public NewProxyClass(InvocationHandler handler) { this.handler = handler; } 
        CtConstructor ctConstructor = new CtConstructor(new CtClass[] { handlerCc }, proxyClass);
        ctConstructor.setBody("$0.handler = $1;"); // $0代表this, $1代表構造函數(shù)的第1個參數(shù)
        proxyClass.addConstructor(ctConstructor);
        
        // 4.為代理類添加相應接口方法及實現(xiàn) 
        CtClass interfaceCc = pool.get(interfaceClass.getName());
        
        // 4.1 為代理類添加接口:public class ProxyClass implements IHello
        proxyClass.addInterface(interfaceCc);
        
        // 4.2 為代理類添加相應方法及實現(xiàn)
        CtMethod[] ctMethods = interfaceCc.getDeclaredMethods();
        for (CtMethod ctMethod : ctMethods) {
            String methodFieldName = ctMethod.getName(); // 新的方法名

            // 4.2.1 為代理類添加反射方法字段
            // 如:private static Method method1 = Class.forName("com.zmc.learning.javassist.IHello").getDeclaredMethod("sayHello", new Class[] { Integer.TYPE });
            // 構造反射字段聲明及賦值語句
            String classParamsStr = "new Class[0]"; // 方法的多個參數(shù)類型以英文逗號分隔
            if (ctMethod.getParameterTypes().length > 0) { // getParameterTypes獲取方法參數(shù)類型列表
                for (CtClass clazz : ctMethod.getParameterTypes()) {
                    classParamsStr = (("new Class[0]".equals(classParamsStr)) ? clazz.getName() : classParamsStr + "," + clazz.getName()) + ".class";
                }
                classParamsStr = "new Class[] {" + classParamsStr + "}";
            }
            String methodFieldTpl = "private static java.lang.reflect.Method %s=Class.forName(\"%s\").getDeclaredMethod(\"%s\", %s);";
            String methodFieldBody = String.format(methodFieldTpl, ctMethod.getName(), interfaceClass.getName(), ctMethod.getName(), classParamsStr);
            // 為代理類添加反射方法字段. CtField.make(String sourceCodeText, CtClass addToThisClass)
            CtField methodField = CtField.make(methodFieldBody, proxyClass);
            proxyClass.addField(methodField);

            // 4.2.2 為方法添加方法體
            // 構造方法體. this.handler.invoke(this, 反射字段名, 方法參數(shù)列表); 
            String methodBody = "$0.handler.invoke($0, " + methodFieldName + ", $args)";
            // 如果方法有返回類型,則需要轉(zhuǎn)換為相應類型后返回,因為invoke方法的返回類型為Object
            if (CtPrimitiveType.voidType != ctMethod.getReturnType()) {
                // 對8個基本類型進行轉(zhuǎn)型
                // 例如:((Integer)this.handler.invoke(this, this.m2, new Object[] { paramString, new Boolean(paramBoolean), paramObject })).intValue();
                if (ctMethod.getReturnType() instanceof CtPrimitiveType) {
                    CtPrimitiveType ctPrimitiveType = (CtPrimitiveType) ctMethod.getReturnType();
                    methodBody = "return ((" + ctPrimitiveType.getWrapperName() + ") " + methodBody + ")." + ctPrimitiveType.getGetMethodName() + "()";
                } else { // 對于非基本類型直接轉(zhuǎn)型即可
                    methodBody = "return (" + ctMethod.getReturnType().getName() + ") " + methodBody;
                }
            }
            methodBody += ";";
            // 為代理類添加方法. CtMethod(CtClass returnType, String methodName, CtClass[] parameterTypes, CtClass addToThisClass)
            CtMethod newMethod = new CtMethod(ctMethod.getReturnType(), ctMethod.getName(),
                    ctMethod.getParameterTypes(), proxyClass);
            newMethod.setBody(methodBody);
            proxyClass.addMethod(newMethod);
        }
        
        // 5.生成代理實例. 將入?yún)nvocationHandler handler設置到代理類的InvocationHandler handler變量
        Class newClass = proxyClass.toClass(classLoader, null);
        return newClass.getConstructor(InvocationHandler.class).newInstance(h);
    }
}

ProxyFactory 和 Jdk Java Proxy 方法簽名都一致,使用的方式也是一致的,需要注意的是對8個基本類型進行轉(zhuǎn)型。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • Javassist是一個用于處理Java字節(jié)碼的類庫。Java字節(jié)碼是一個以二進制文件進行存儲的class文件。每...
    bdqfork閱讀 1,275評論 0 0
  • 1、讀和寫字節(jié)碼 Javassist是一個處理Java字節(jié)碼的庫,java字節(jié)碼是使用二進制格式存儲在文件中的話,...
    礪雪凝霜閱讀 1,393評論 0 2
  • https://blog.csdn.net/luanlouis/article/details/24589193 ...
    小陳阿飛閱讀 968評論 1 1
  • Java動態(tài)追蹤技術探究 在Java虛擬機中,字符串常量到底存放在哪 一次生產(chǎn) CPU 100% 排查優(yōu)化實踐 聊...
    passiontim閱讀 4,308評論 0 38
  • Java動態(tài)追蹤技術探究 在Java虛擬機中,字符串常量到底存放在哪 一次生產(chǎn) CPU 100% 排查優(yōu)化實踐 聊...
    星海辰光大人閱讀 806評論 0 2

友情鏈接更多精彩內(nèi)容