Java字節(jié)碼增強技術(shù)

1.字節(jié)碼

Java剛誕生的時候有一句非常著名的宣傳口號:“一次編寫,到處運行”。為了實現(xiàn)這個目的,Sun公司以及其他虛擬機提供商發(fā)布了很多可以運行在不同平臺上的jvm虛擬機,虛擬機的作用就是載入和執(zhí)行一種與平臺無關(guān)的字節(jié)碼。簡單來說,java程序從編寫完成到運行,大致會有兩個階段,第一個階段是從.java文件編譯成.class文件;第二階段是jvm載入.class文件,進(jìn)行解釋和執(zhí)行。
為什么稱之為字節(jié)碼,而不叫比特碼呢?是因為字節(jié)碼文件是采用十六進(jìn)制組成,jvm讀取的時候是以兩個十六進(jìn)制數(shù)為一組讀取,我們知道一個十六進(jìn)制是4bit,所以兩個十六進(jìn)制就是一個字節(jié),jvm便是按字節(jié)讀取。

2.字節(jié)碼增強

我們修改字節(jié)碼有兩個過程:
1.修改已生成的字節(jié)碼(即.class文件)
2.重新加載更改后的字節(jié)碼,使之生效

2.1 字節(jié)碼修改技術(shù)

字節(jié)碼修改技術(shù)通常包括以下幾類:

  • ASM :一個輕量級的字節(jié)碼操作框架,直接涉及到j(luò)vm底層操作和指令,使用難度較大。
  • CGLIB:屬于動態(tài)織入(字節(jié)碼加載之后)技術(shù),基于ASM實現(xiàn),性能高。同時,CGLIB突破了Java動態(tài)代理基于接口的限制,采用子類繼承的方式。
  • JAVAssist:屬于動態(tài)織入技術(shù),操作簡單,接口強大,性能較ASM差。
  • ASPECTJ:靜態(tài)織入(字節(jié)碼加載之前)框架,常用于AOP編程框架。

2.2 使修改后的字節(jié)碼生效

我們這里只關(guān)注通過動態(tài)織入框架定義的字節(jié)碼??梢酝ㄟ^JVMTI(JVM提供的一套對JVM操作的接口工具,通過接口注冊事件hook,在jvm事件觸發(fā)時,同時觸發(fā)我們定義好的鉤子),將字節(jié)碼文件寫成一個agent,并在java程序啟動之后,通過Attach API(提供的jvm進(jìn)程之間通信的能力)的方式,動態(tài)加載進(jìn)入虛擬機。

Talk is cheap.Show me the code.

下面我們采用最簡單的JAVAssit+AttachAPI的方式編寫一套demo。
1.首先,我們先模擬一個java進(jìn)程:

package demo;

import java.lang.management.ManagementFactory;
import java.util.concurrent.TimeUnit;

public class Application {
    public static void main(String[] args) {
        String name = ManagementFactory.getRuntimeMXBean().getName();
        String s = name.split("@")[0];
        System.out.println("pid:" + s);
        while (true) {
            boolean logined = login("admin", "111");
            System.out.println((logined ? "成功" : "失敗") + "   pid:" + s);
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static boolean login(String user, String passwd) {
        System.out.println("login...");
        if ("admin".equals(user) && "123".equals(passwd)) {
            return true;
        }
        return false;
    }
}

此程序會一直返回失敗,并且打印出程序的進(jìn)程id。

2.接下來,我們用JVMTI接口編寫一個agent:

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class MyAgent {
    public static void agentmain(String args, Instrumentation inst) throws UnmodifiableClassException {
        inst.addTransformer(new MyTransformer(),true);
        System.out.println("agent加載完畢");
        for (Class aClass : inst.getAllLoadedClasses()) {
            if(aClass.getName().contains("Application")){
                System.out.println(aClass.getName());
                inst.retransformClasses(aClass);
                System.out.println("重新加載class完畢");
            }
        }
    }

}
import javassist.*;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.Objects;

public class MyTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        System.out.println("我進(jìn)到transformer了:"+className);
        if (!className.contains("Application")) {
            return classfileBuffer;
        }
        ClassPool cp = ClassPool.getDefault();
        try {
            CtClass ctClass1 = cp.get("demo.Application");
            CtClass ctClass2 = cp.get(className);
            CtClass ctClass = Objects.isNull(ctClass1) ? ctClass2 : ctClass1;
            CtMethod ctMethod = ctClass.getDeclaredMethod("login");
            ctMethod.setBody("{return true;}");
            System.out.println("修改class完畢");
            return ctClass.toBytecode();
        } catch (NotFoundException e) {
            e.printStackTrace();
        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return classfileBuffer;
    }
}

完成之后,我們用編輯器或者jar命令將以上兩個類打成一個jar包,命名為javabyte.jar,不管用什么方法,最終保持jar包結(jié)構(gòu)如下:


image.png

然后下一步,需要解壓jar,修改里面的MANIFEST.MF文件,保持文件內(nèi)容與以下內(nèi)容一致:

Manifest-Version: 1.0
Agent-Class: MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Class-Path: javassist-3.24.1-GA.jar
Main-Class: 

3.通過Attach API,動態(tài)加載改過的字節(jié)碼

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import java.io.IOException;

public class Demo {
    public static void main(String[] args) {
        try {
            VirtualMachine virtualMachine = VirtualMachine.attach("30421");
            virtualMachine.loadAgent("javabyte.jar");
        } catch (AttachNotSupportedException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (AgentLoadException e) {
            e.printStackTrace();
        } catch (AgentInitializationException e) {
            e.printStackTrace();
        }
    }
}

注意:以上代碼中的路徑一定要跟自己工程路徑一致,比如:demo.Application,demo是我的包名;javabyte.jar這個可以直接替換為jar的絕對路徑。
操作步驟:
1.運行1程序,會打印出進(jìn)程id
2.打包2程序
3.根據(jù)pid修改3程序,運行
結(jié)果如下:
運行1程序:


image.png

運行3程序:


image.png

如果程序運行報錯和tools有關(guān),直接在項目里面添加依賴即可:
<dependency>
        <groupId>com.sun</groupId>
        <artifactId>tools</artifactId>
        <version>1.8.0</version>
        <scope>system</scope>
        <systemPath>/Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home/lib/tools.jar</systemPath>
    </dependency>

遇到問題也不用著急,可以打印各種日志來跟蹤你的程序運行,并找到問題。

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

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

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