1. 程式人生 > >javassist位元組碼增強

javassist位元組碼增強

javassist是一種能夠在不影響正常編譯的情況下,修改位元組碼。java作為一種強型別的語言,不通過編譯就不能夠進行jar包的生成。而有了javaagent技術,就可以在位元組碼這個層面對類和方法進行修改。同時,也可以把javaagent理解成一種程式碼注入的方式。但是這種注入比起spring的aop更加的優美。

示例:一個javaagent demo程式首先建立agent。作為agent的jar包必須有兩個要求。一個是必須實現premain方法,另一個是必須在MANIFEST.MF檔案中有Premain-Class。先看一下agent的實現。
public class Agent implements 
ClassFileTransformer { public final String injectedClassName = "com.haiziwang.App"; public final String methodName = "hello"; public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws
IllegalClassFormatException { className = className.replace("/", "."); if (className.equals(injectedClassName)) { CtClass ctclass = null; ClassPool classPool = ClassPool.getDefault(); try { //加入當前執行緒的上下文類載入器作為額外的類搜尋路徑 classPool.appendClassPath(new
LoaderClassPath(Thread.currentThread().getContextClassLoader())); ctclass = classPool.get(className);// 使用全稱,用於取得位元組碼類<使用javassist> CtMethod ctmethod = ctclass.getDeclaredMethod(methodName);// 得到這方法例項 ctmethod.insertBefore("System.out.println(\"hello方法之前攔截...\");"); return ctclass.toBytecode(); } catch (Exception e) { System.out.println(e.getMessage()); e.printStackTrace(); } } return null; } }

建立攔截器的主類,必須有premain方法:

public class AgentMain {
    public static void premain(String agentOps, Instrumentation inst) {
        System.out.println("=========premain方法執行========");
// 新增Transformer
inst.addTransformer(new Agent());
}
}

修改pom.xml檔案,設定premain-class, 這一步非常關鍵。

<?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">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.platform</groupId>
    <artifactId>kidstrack</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>kidstrack</name>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
    <dependency>
        <groupId>org.javassist</groupId>
        <artifactId>javassist</artifactId>
        <version>3.22.0-GA</version>
    </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>utf-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.0.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <manifestEntries>
                                        <Premain-Class>com.haiziwang.AgentMain</Premain-Class>
                                    </manifestEntries>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

最後,我們建立DEMO測試類

public class App {
    public static void main(String[] args) {
        hello();
}

    public static void hello() {
        System.out.println("hello方法執行");
}
}

最後一步,我們設定JVM啟動引數即可 -javaagent:d:xxx.jar

OK,至次我們就實現對hello方法的增強

=========premain方法執行========
hello方法之前攔截...
hello方法執行

可以發現利用javassist框架進行動態程式設計是比較輕鬆簡單的,來看幾個比較重要的API

一.    API相關

    最近使用Javassist框架開發了一些功能,使用的過程中碰見了不少問題,將使用方法總結下,以防日後重複踩坑。

    先來看一段程式碼:

ClassPool classPool = ClassPool.getDefault();
classPool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
CtClass ctClass = classPool.getCtClass("cn.com.Test");
CtMethod ctMethod = CtNewMethod.make("public void helloWorld(){ System.out.println(\"hello world!\"); }", ctClass);
ctClass.addMethod(ctMethod);
ctClass.toClass();

    這段程式碼的功能很簡單,在建立了一個預設的classpool後,加入當前執行緒的上下文類載入器作為額外的類搜尋路徑,獲取Test類後向其中加入了helloWorld這個方法,並把修改後的類載入至當前執行緒所在的上下文類載入器中。

    可以發現利用javassist框架進行動態程式設計是比較輕鬆簡單的,來看幾個比較重要的API:

ClassPool

    ClassPool是CtClass物件的容器,每一個CtClass物件都必須從ClassPool中獲取。

    ClassPool自身可以形成層級結構,其工作機制與java的類載入器類似,只有當父節點找不到類檔案時,才會呼叫子節點的get()方法。通過設定 ClassPath.childFirstLookup 屬性可以調整其工作流程。

    需要注意的是ClassPool會在記憶體中維護所有被它建立過的CtClass,當CtClass數量過多時,會佔用大量的記憶體,API中給出的解決方案是週期性的呼叫compress方法 或 重新建立ClassPool 或 有意識的呼叫CtClass的detach()方法以釋放記憶體

    需要關注的方法:

        1.    getDefault : 返回預設的ClassPool,單例模式!一般通過該方法建立我們的ClassPool。

        2.    appendClassPath, insertClassPath  : 將一個ClassPath加到類搜尋路徑的末尾位置 或 插入到起始位置。通常通過該方法寫入額外的類搜尋路徑,以解決多個類載入器環境中找不到類的尷尬。

        3.    toClass : 將修改後的CtClass載入至當前執行緒的上下文類載入器中,CtClass的toClass方法是通過呼叫本方法實現。需要注意的是一旦呼叫該方法,則無法繼續修改已經被載入的class。

        4.    get , getCtClass : 根據類路徑名獲取該類的CtClass物件,用於後續的編輯。

ClassPath

ClassPath是一個介面,代表類的搜尋路徑,含有具體的搜尋實現。當通過其它途徑無法獲取要編輯的類時,可以嘗試定製一個自己的ClassPath。API提供的實現中值得關注的有:

        1.    ByteArrayClassPath : 將類以位元組碼的形式加入到該path中,ClassPool 可以從該path中生成所需的CtClass。

        2.    ClassClassPath : 通過某個class生成的path,通過該class的classloader來嘗試載入指定的類檔案。

        3.    LoaderClassPath : 通過某個classloader生成path,並通過該classloader搜尋載入指定的類檔案。需要注意的是該類載入器以弱引用的方式存在於path中,當不存在強引用時,隨時可能會被清理。

CtClass

    javassist為每個需要編輯的class都建立了一個CtClass物件,通過對CtClass物件的操作來實現對class的編輯工作。

    該類方法較多,此處列出需要重點關注的方法:

        1.    freeze : 凍結一個類,使其不可修改。

        2.    isFrozen : 判斷一個類是否已被凍結。

        3.    prune : 刪除類不必要的屬性,以減少記憶體佔用。呼叫該方法後,許多方法無法將無法正常使用,慎用。

        4.    defrost : 解凍一個類,使其可以被修改。如果事先知道一個類會被defrost, 則禁止呼叫 prune 方法。

        5.    detach : 將該class從ClassPool中刪除。

        6.    writeFile : 根據CtClass生成 .class 檔案。

        7.    toClass : 通過類載入器載入該CtClass。

CtMethod

    CtMthod代表類中的某個方法,可以通過CtClass提供的API獲取或者CtNewMethod新建,通過CtMethod物件可以實現對方法的修改。

    需要注意的是寫入方法體的程式碼無法訪問在其它地方定義的成員變數,一些比較重要的方法:

        1.    insertBefore : 在方法的起始位置插入程式碼。

        2.    insterAfter : 在方法的所有 return 語句前插入程式碼以確保語句能夠被執行,除非遇到exception。

        3.    insertAt : 在指定的位置插入程式碼。

        4.    setBody : 將方法的內容設定為要寫入的程式碼,當方法被 abstract修飾時,該修飾符被移除。

        5.    make : 建立一個新的方法。

CtNewMethod

    提供各種靜態方法來操作CtMethod,不進行詳細描述,有興趣可以看下API。

特殊符號

$0, $1, $2, ...this and actual parameters
$argsAn array of parameters. The type of $args is Object[].
$$All actual parameters.For example, m($$) is equivalent to m($1,$2,...)
$cflow(...)cflow variable
$rThe result type. It is used in a cast expression.
$wThe wrapper type. It is used in a cast expression.
$_The resulting value
$sigAn array of java.lang.Class objects representing the formal parameter types
$typeA java.lang.Class object representing the formal result type.
$classA java.lang.Class object representing the class currently edited.

二.    使用場景總結

1.    實現程式碼插入功能:

CtClass ctClass = classPool.getCtClass("com.netease.HelloWorld");
CtMethod ctMethod = ctClass.getDeclaredMethod("sayHello");
ctMethod.insertAfter("System.out.println(\"Hello world!\");");
ctClass.toClass();

2.    建立一個完整的類:

ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("com.netease.Class");

CtField ctField = new CtField(classPool.get("java.lang.String"), "teacher", ctClass);
ctField.setModifiers(Modifier.PRIVATE);
ctClass.addField(ctField);

ctClass.addMethod(CtNewMethod.setter("setTeacher", ctField));
ctClass.addMethod(CtNewMethod.getter("getTeacher", ctField));
ctClass.writeFile();

 3.    實現攔截器功能:

CtMethod ctMethod = clazz.getDeclaredMethod(method);
String newName = method + "New";
ctMethod.setName(newName);
CtMethod newCtMethod = CtNewMethod.copy(ctMethod, method, clazz, null);
String type = ctMethod.getReturnType().getName();
StringBuilder body = new StringBuilder();
body.append("{\n System.out.println(\"Before Method Execute...\");\n");
if(!"void".equals(type)) {
    body.append(type).append(" result = ");
}
body.append(newName).append("($$);\n");
body.append("System.out.println(\"After Method Execute...\");;\n");
if(!"void".equals(type)) {
    body.append("return result;\n");
}
body.append("}");
newCtMethod.setBody(body.toString());
clazz.addMethod(newCtMethod);