Java之Javassist動態編程


Java之Javassist動態編程

動態編程與javassist

動態編程是相對於靜態編程而言的,那二者有什么明顯的區別呢?簡單的說就是在靜態編程中,類型檢查是在編譯時完成的,而動態編程中類型檢查是在運行時完成的。所謂動態編程就是繞過編譯過程在運行時進行操作的技術

那么動態編程的出現是為了解決哪些問題呢?個人感覺比如Spring的依賴注入,用到了動態編程,雖然說不用依賴注入也可以,但是會很繁瑣,比如動態編程來的便捷,那動態編程感覺就是為了解決某些場景中只使用靜態編程顯得比較臃腫和笨拙的地方。

而之前接觸到的比如反射,動態代理(運行時動態插入代碼)都有點動態編程的影子,

下面看下Javassist。

Javassist是一個開源的分析、編輯和創建Java字節碼的類庫,Java 字節碼存儲在稱為類文件的二進制文件中。每個類文件包含一個 Java 類或接口。是由東京工業大學的數學和計算機科學系的 Shigeru Chiba (千葉 滋)所創建的。其主要的優點,在於簡單,而且快速。直接使用java編碼的形式,而不需要了解虛擬機指令,就能動態改變類的結構,或者動態生成類。而個人感覺在安全中最重要的就是在使用Javassist時我們可以像寫Java代碼一樣直接插入Java代碼片段,讓我們不再需要關注Java底層的字節碼的和棧操作,僅需要學會如何使用Javassist的API即可實現字節碼編輯,類似於可以達到任意代碼執行的效果。

Javassist使用

Javassist中最為重要的是ClassPoolCtClassCtMethod 以及 CtField這幾個類。

ClassPool:一個基於HashMap實現的CtClass對象容器,其中鍵是類名稱,值是表示該類的CtClass對象。默認的ClassPool使用與底層JVM相同的類路徑,因此在某些情況下,可能需要向ClassPool添加類路徑或類字節。

CtClass:表示一個類,這些CtClass對象可以從ClassPool獲得。

CtMethods:表示類中的方法。

CtFields :表示類中的字段。

ClassPool

ClassPool:一個基於HashMap實現的CtClass對象容器,其中鍵是類名稱,值是表示該類的CtClass對象。默認的ClassPool使用與底層JVM相同的類路徑,因此在某些情況下,可能需要向ClassPool添加類路徑或類字節。

常用方法:

ClassPool		getDefault()			返回默認的類池。
  
ClassPath		insertClassPath(String pathname)	在搜索路徑的開頭插入目錄或jar(或zip)文件。
  
ClassPath		insertClassPath(ClassPath cp)		ClassPath在搜索路徑的開頭插入一個對象。
  
java.lang.ClassLoader	getClassLoader()	獲取類加載器
  
CtClass	get(java.lang.String classname)	從源中讀取類文件,並返回對CtClass 表示該類文件的對象的引用。
  
ClassPath	appendClassPath(ClassPath cp)	 將ClassPath對象附加到搜索路徑的末尾。
  
CtClass	makeClass(java.lang.String classname)  創建一個新的public類

CtClass

CtClass:表示一個類,一個CtClass(編譯時類)對象可以處理一個class文件,這些CtClass對象可以從ClassPool獲得。

void	setSuperclass(CtClass clazz)	更改超類,除非此對象表示接口。

java.lang.Class<?>	toClass(java.lang.invoke.MethodHandles.Lookup lookup)	
	將此類轉換為java.lang.Class對象。
	
byte[]	toBytecode()	將該類轉換為類文件。

void	writeFile()		將由此CtClass 對象表示的類文件寫入當前目錄。

void	writeFile(java.lang.String directoryName)	 將由此CtClass 對象表示的類文件寫入本地磁盤。

CtConstructor	makeClassInitializer()	制作一個空的類初始化程序(靜態構造函數)。

CtMethod

CtMethod:表示類中的方法。超類為CtBehavior,很多有用的方法都在CtBehavior

void	insertBefore (java.lang.String src)	
在正文的開頭插入字節碼。
void	insertAfter	(java.lang.String src)	
在正文的末尾插入字節碼。
void	setBody (CtMethod src, ClassMap map)	
從另一個方法復制方法體。

CtConstructor

CtConstructor的實例表示一個構造函數。它可能代表一個靜態構造函數。

void	setBody(java.lang.String src)	
	設置構造函數主體。
void	setBody(CtConstructor src, ClassMap map)	
	從另一個構造函數復制一個構造函數主體。
CtMethod	toMethod(java.lang.String name, CtClass declaring)	
	復制此構造函數並將其轉換為方法。

CtField

CtFields :表示類中的字段。

動態生成類

大致有如下幾個步驟

  1. 獲取默認類池ClassPool classPool = ClassPool.getDefault();

  2. 創建一個自定義類CtClass ctClass = classPool.makeClass();

  3. 添加實現接口or屬性or構造方法or普通方法

    • 添加接口

      ctClass.setInterfaces(new CtClass[]{classPool.makeInterface("java.io.Serializable")});
      
    • 添加屬性

      //新建一個int類型名為id的成員變量
      CtField id = new CtField(CtClass.intType, "id", ctClass);
      //將id設置為public
      id.setModifiers(AccessFlag.PUBLIC);
      //將該id屬性"賦值"給ClassDemo
      ctClass.addField(id);
      
    • 添加構造方法(有參)

      //添加有參構造方法
      CtConstructor ctConstructor1 = CtNewConstructor.make("public ClassDemo(int id){this.id = id;}", ctClass);
      ctClass.addConstructor(ctConstructor1);
      
    • 添加方法

      CtMethod ctMethod = CtNewMethod.make("public void calcDemo(){java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");}", ctClass);
      ctClass.addMethod(ctMethod);
      
  4. 寫入磁盤

    這里寫入磁盤可以用如下兩種方法

    • javassist自帶的ctClass.writeFile();可指定絕對路徑寫入
    • 也可轉換為byte流通過FileOutputStream等寫入磁盤
  5. 進行驗證:調用方法or屬性賦值

  6. tips:

    • 這里注意javassist.CannotCompileException異常: 因為同個 Class 是不能在同個 ClassLoader 中加載兩次的,所以在輸出 CtClass 的時候需要注意下,可以使用javassist自帶的classloader解決此問題
    • 反射時newInstance()拋出了java.lang.InstantiationException異常可能是因為沒有寫無參構造
    • 如果已經加載了通過javassist生成的類,即便是通過反射(如class.forName())或者new都不是加載一個"新類",只有換一個ClassLoader加載才會是生成一個"新類"
package javassisttest;

import javassist.*;
import javassist.bytecode.AccessFlag;

import java.io.File;
import java.io.FileOutputStream;

public class JavassistDemo01 {
    public static void main(String[] args) {

        JavassistDemo01 a = new JavassistDemo01();
        a.makeClass0();
    }

    public void makeClass0(){
        //獲取默認類池
        ClassPool classPool = ClassPool.getDefault();
        //創建一個類ClassDemo
        CtClass ctClass = classPool.makeClass("javassisttest.ClassDemo");
        //讓該類實現序列化接口
        ctClass.setInterfaces(new CtClass[]{classPool.makeInterface("java.io.Serializable")});
        try {
            //新建一個int類型名為id的成員變量
            CtField id = new CtField(CtClass.intType, "id", ctClass);
            //將id設置為public
            id.setModifiers(AccessFlag.PUBLIC);
            //將該id屬性"賦值"給ClassDemo
            ctClass.addField(id);

            //添加無參構造方法
            CtConstructor ctConstructor = CtNewConstructor.make("public ClassDemo(){};", ctClass);
            ctClass.addConstructor(ctConstructor);

            //添加有參構造方法
            CtConstructor ctConstructor1 = CtNewConstructor.make("public ClassDemo(int id){this.id = id;}", ctClass);
            ctClass.addConstructor(ctConstructor1);

            //添加普通方法1
            CtMethod ctMethod = CtNewMethod.make("public void calcDemo(){java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");}", ctClass);
            ctClass.addMethod(ctMethod);

            //添加普通方法2
            CtMethod ctMethod1 = CtNewMethod.make("public void hello(){System.out.println(\"Hello Javassist!!!\");}", ctClass);
            ctClass.addMethod(ctMethod1);

            //將class文件寫入磁盤
            //轉換成字節流
            byte[] bytes = ctClass.toBytecode();
            //寫入磁盤
            File classPath = new File(new File(System.getProperty("user.dir"), "/src/main/java/javassisttest/"), "ClassDemo.class");
            FileOutputStream fos = new FileOutputStream(classPath);
            fos.write(bytes);
            fos.close();

            //驗證-調用方法
            //注意這里可能會拋javassist.CannotCompileException異常因為同個 Class 是不能在同個 ClassLoader 中加載兩次的,所以在輸出 CtClass 的時候需要注意下
            //需要通過一個未加載該class的classloader加載即可,為此javassist內置了一個classloader

            //獲取javassist的classloader
            ClassLoader loader = new Loader(classPool);
            System.out.println("loading");
            //通過該classloader加載才是新的一個class
            Class<?> clazz = loader.loadClass("javassisttest.ClassDemo");

            //反射調用hello
            clazz.getDeclaredMethod("hello").invoke(clazz.newInstance());
            //反射調用calc
            clazz.getDeclaredMethod("calcDemo").invoke(clazz.newInstance());

        } catch (Exception e){
            System.out.println(e);
        }
    }
}

動態獲取類方法

  1. 獲取默認類池ClassPool classPool = ClassPool.getDefault();
  2. 獲取目標類CtClass cc = cp.get();
  3. 獲取類的方法CtMethod m = cc.getDeclaredMethod();
  4. 插入任意代碼m.insertBefore("{java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");}");
  5. 轉換為class對象Class c = cc.toClass();
  6. 反射調用對象JavassistDemo j= (JavassistDemo)c.newInstance();
  7. 執行方法j.hello();

JavassistDemo.java

package javassisttest;

public class JavassistDemo {
    public void hello(){
        System.out.println("hello calc!!!");
    }
}

JavassistDemoto01

public static void main(String[] args) {

        JavassistDemo01 a = new JavassistDemo01();
        try {
            a.toGetClass();
        } catch (Exception e) {
            e.printStackTrace();
        }


    }

public void toGetClass() throws Exception {
        ClassPool cp = ClassPool.getDefault();
        CtClass cc = cp.get("javassisttest.JavassistDemo");
        CtMethod m = cc.getDeclaredMethod("hello"); /
        m.insertBefore("{java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");}");
        Class c = cc.toClass();
        JavassistDemo j= (JavassistDemo)c.newInstance(); 
        j.hello();
    }


tips:

  1. 如果目標類未加載過,可以直接調用toClass()方法之后new一個該類的對象即可調用該類。
  2. 如果目標類已加載過,就需要用上面的方法,通過javassist的ClassLoader去加載后進行調用。

Javassist特殊參數

在動態修改類時,可能會碰到如下代碼,這里的$1代表方法中第一個行參。setBody會用參數中寫的方法體覆蓋掉原有的方法

public void makePool(){
  ClassPool classPool = ClassPool.getDefault();
  try {
    CtClass ctClass = classPool.get("javassisttest.Person");
    CtMethod hello = ctClass.getDeclaredMethod("hello", new CtClass[]{classPool.get("java.lang.String")});
    hello.setBody("{" + "System.out.println(\"你好:\" + $1);" + "}");

    ctClass.writeFile();
    ctClass.toClass();
    new Person().hello("CoLoo");


  } catch (NotFoundException e) {
    e.printStackTrace();
  } catch (CannotCompileException e) {
    e.printStackTrace();
  } catch (IOException e) {
    e.printStackTrace();
  }

}

其余的參數可參照如下

標識符 作用
0、0、0、1、$2、 3 、 3、 3、… this和方法參數(1-N是方法參數的順序)
$args 方法參數數組,類型為Object[]
$$ 所有方法參數,例如:m($$)相當於m(1,1,1,2,…)
$cflow(…) control flow 變量
$r 返回結果的類型,在強制轉換表達式中使用。
$w 包裝器類型,在強制轉換表達式中使用。
$_ 返回的結果值
$sig 類型為java.lang.Class的參數類型對象數組
$type 類型為java.lang.Class的返回值類型
$class 類型為java.lang.Class的正在修改的類

參考於:http://www.javassist.org/tutorial/tutorial2.html

獲取類信息

public void makePool2(){
  ClassPool classPool = ClassPool.getDefault();

  try {

    CtClass ctClass = classPool.get("javassisttest.Person");
    byte[] bytes = ctClass.toBytecode();
    System.out.println(bytes.length);
    System.out.println(ctClass.getName());
    System.out.println(ctClass.getSimpleName());
    System.out.println(ctClass.getSuperclass().getName());
    System.out.println(Arrays.toString(ctClass.getInterfaces()));

    for (CtConstructor constructor : ctClass.getConstructors()) {
      System.out.println(constructor);
    }

    for (CtMethod method : ctClass.getMethods()) {
      System.out.println(method);
    }

  } catch (NotFoundException e) {
    e.printStackTrace();
  } catch (CannotCompileException e) {
    e.printStackTrace();
  } catch (IOException e) {
    e.printStackTrace();
  }

}

結語

無論是動態生成類還是動態獲取類中某方法並插入任意代碼都是很令人眼紅的操作,在cc鏈中也有用到此機制。

ps:彈計算器確實比找女朋友有意思多了。

Reference

https://www.javassist.org/html/index.html
https://juejin.cn/post/6952765170544279566
https://www.cnblogs.com/baiqiantao/p/10235049.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM