作者:rickiyang
出處:www.cnblogs.com/rickiyang/p/11336268.html
Java 位元組碼以二進制的形式存盤在 .class 檔案中,每一個 .class 檔案包含一個 Java 類或介面,
Javaassist 就是一個用來處理 Java 位元組碼的類別庫,它可以在一個已經編譯好的類中添加新的方法,或者是修改已有的方法,并且不需要對位元組碼方面有深入的了解,同時也可以去生成一個新的類物件,通過完全手動的方式,
1. 使用 Javassist 創建一個 class 檔案
首先需要引入jar包:
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>
撰寫創建物件的類:
package com.rickiyang.learn.javassist;
import javassist.*;
/**
* @author rickiyang
* @date 2019-08-06
* @Desc
*/
public class CreatePerson {
/**
* 創建一個Person 物件
*
* @throws Exception
*/
public static void createPseson() throws Exception {
ClassPool pool = ClassPool.getDefault();
// 1. 創建一個空類
CtClass cc = pool.makeClass("com.rickiyang.learn.javassist.Person");
// 2. 新增一個欄位 private String name;
// 欄位名為name
CtField param = new CtField(pool.get("java.lang.String"), "name", cc);
// 訪問級別是 private
param.setModifiers(Modifier.PRIVATE);
// 初始值是 "xiaoming"
cc.addField(param, CtField.Initializer.constant("xiaoming"));
// 3. 生成 getter、setter 方法
cc.addMethod(CtNewMethod.setter("setName", param));
cc.addMethod(CtNewMethod.getter("getName", param));
// 4. 添加無參的建構式
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
cons.setBody("{name = \"xiaohong\";}");
cc.addConstructor(cons);
// 5. 添加有參的建構式
cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
// $0=this / $1,$2,$3... 代表方法引數
cons.setBody("{$0.name = $1;}");
cc.addConstructor(cons);
// 6. 創建一個名為printName方法,無引數,無回傳值,輸出name值
CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("{System.out.println(name);}");
cc.addMethod(ctMethod);
//這里會將這個創建的類物件編譯為.class檔案
cc.writeFile("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");
}
public static void main(String[] args) {
try {
createPseson();
} catch (Exception e) {
e.printStackTrace();
}
}
}
執行上面的 main 函式之后,會在指定的目錄內生成 Person.class 檔案:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.rickiyang.learn.javassist;
public class Person {
private String name = "xiaoming";
public void setName(String var1) {
this.name = var1;
}
public String getName() {
return this.name;
}
public Person() {
this.name = "xiaohong";
}
public Person(String var1) {
this.name = var1;
}
public void printName() {
System.out.println(this.name);
}
}
跟咱們預想的一樣,
在 Javassist 中,類 Javaassit.CtClass 表示 class 檔案,一個 GtClass (編譯時類)物件可以處理一個 class 檔案,ClassPool是 CtClass 物件的容器,它按需讀取類檔案來構造 CtClass 物件,并且保存 CtClass 物件以便以后使用,
需要注意的是 ClassPool 會在記憶體中維護所有被它創建過的 CtClass,當 CtClass 數量過多時,會占用大量的記憶體,API中給出的解決方案是 有意識的呼叫CtClass的detach()方法以釋放記憶體,
ClassPool需要關注的方法:
- getDefault : 回傳默認的
ClassPool是單例模式的,一般通過該方法創建我們的ClassPool; - appendClassPath, insertClassPath : 將一個
ClassPath加到類搜索路徑的末尾位置 或 插入到起始位置,通常通過該方法寫入額外的類搜索路徑,以解決多個類加載器環境中找不到類的尷尬; - toClass : 將修改后的CtClass加載至當前執行緒的背景關系類加載器中,CtClass的
toClass方法是通過呼叫本方法實作,需要注意的是一旦呼叫該方法,則無法繼續修改已經被加載的class; - get , getCtClass : 根據類路徑名獲取該類的CtClass物件,用于后續的編輯,
CtClass需要關注的方法:
- freeze : 凍結一個類,使其不可修改;
- isFrozen : 判斷一個類是否已被凍結;
- prune : 洗掉類不必要的屬性,以減少記憶體占用,呼叫該方法后,許多方法無法將無法正常使用,慎用;
- defrost : 解凍一個類,使其可以被修改,如果事先知道一個類會被defrost, 則禁止呼叫 prune 方法;
- detach : 將該class從ClassPool中洗掉;
- writeFile : 根據CtClass生成
.class檔案; - toClass : 通過類加載器加載該CtClass,
上面我們創建一個新的方法使用了CtMethod類,CtMthod代表類中的某個方法,可以通過CtClass提供的API獲取或者CtNewMethod新建,通過CtMethod物件可以實作對方法的修改,
CtMethod中的一些重要方法:
- insertBefore : 在方法的起始位置插入代碼;
- insterAfter : 在方法的所有 return 陳述句前插入代碼以確保陳述句能夠被執行,除非遇到exception;
- insertAt : 在指定的位置插入代碼;
- setBody : 將方法的內容設定為要寫入的代碼,當方法被 abstract修飾時,該修飾符被移除;
- make : 創建一個新的方法,
注意到在上面代碼中的:setBody()的時候我們使用了一些符號:
// $0=this / $1,$2,$3... 代表方法引數
cons.setBody("{$0.name = $1;}");
具體還有很多的符號可以使用,但是不同符號在不同的場景下會有不同的含義,所以在這里就不在贅述,可以看javassist 的說明檔案,http://www.javassist.org/tutorial/tutorial2.html
Java 核心技術教程和示例原始碼:https://github.com/javastacks/javastack
2. 呼叫生成的類物件
1. 通過反射的方式呼叫
上面的案例是創建一個類物件然后輸出該物件編譯完之后的 .class 檔案,那如果我們想呼叫生成的類物件中的屬性或者方法應該怎么去做呢?javassist也提供了相應的api,生成類物件的代碼還是和第一段一樣,將最后寫入檔案的代碼替換為如下:
// 這里不寫入檔案,直接實體化
Object person = cc.toClass().newInstance();
// 設定值
Method setName = person.getClass().getMethod("setName", String.class);
setName.invoke(person, "cunhua");
// 輸出值
Method execute = person.getClass().getMethod("printName");
execute.invoke(person);
然后執行main方法就可以看到呼叫了 printName方法,
2. 通過讀取 .class 檔案的方式呼叫
ClassPool pool = ClassPool.getDefault();
// 設定類路徑
pool.appendClassPath("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");
CtClass ctClass = pool.get("com.rickiyang.learn.javassist.Person");
Object person = ctClass.toClass().newInstance();
// ...... 下面和通過反射的方式一樣去使用
3. 通過介面的方式
上面兩種其實都是通過反射的方式去呼叫,問題在于我們的工程中其實并沒有這個類物件,所以反射的方式比較麻煩,并且開銷也很大,那么如果你的類物件可以抽象為一些方法得合集,就可以考慮為該類生成一個介面類,這樣在newInstance()的時候我們就可以強轉為介面,可以將反射的那一套省略掉了,
還拿上面的Person類來說,新建一個PersonI介面類:
package com.rickiyang.learn.javassist;
/**
* @author rickiyang
* @date 2019-08-07
* @Desc
*/
public interface PersonI {
void setName(String name);
String getName();
void printName();
}
實作部分的代碼如下:
ClassPool pool = ClassPool.getDefault();
pool.appendClassPath("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");
// 獲取介面
CtClass codeClassI = pool.get("com.rickiyang.learn.javassist.PersonI");
// 獲取上面生成的類
CtClass ctClass = pool.get("com.rickiyang.learn.javassist.Person");
// 使代碼生成的類,實作 PersonI 介面
ctClass.setInterfaces(new CtClass[]{codeClassI});
// 以下通過介面直接呼叫 強轉
PersonI person = (PersonI)ctClass.toClass().newInstance();
System.out.println(person.getName());
person.setName("xiaolv");
person.printName();
使用起來很輕松,
2. 修改現有的類物件#
前面說到新增一個類物件,這個使用場景目前還沒有遇到過,一般會遇到的使用場景應該是修改已有的類,比如常見的日志切面,權限切面,我們利用javassist來實作這個功能,
有如下類物件:
package com.rickiyang.learn.javassist;
/**
* @author rickiyang
* @date 2019-08-07
* @Desc
*/
public class PersonService {
public void getPerson(){
System.out.println("get Person");
}
public void personFly(){
System.out.println("oh my god,I can fly");
}
}
然后對他進行修改:
package com.rickiyang.learn.javassist;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;
import java.lang.reflect.Method;
/**
* @author rickiyang
* @date 2019-08-07
* @Desc
*/
public class UpdatePerson {
public static void update() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("com.rickiyang.learn.javassist.PersonService");
CtMethod personFly = cc.getDeclaredMethod("personFly");
personFly.insertBefore("System.out.println(\"起飛之前準備降落傘\");");
personFly.insertAfter("System.out.println(\"成功落地,,,,\");");
//新增一個方法
CtMethod ctMethod = new CtMethod(CtClass.voidType, "joinFriend", new CtClass[]{}, cc);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("{System.out.println(\"i want to be your friend\");}");
cc.addMethod(ctMethod);
Object person = cc.toClass().newInstance();
// 呼叫 personFly 方法
Method personFlyMethod = person.getClass().getMethod("personFly");
personFlyMethod.invoke(person);
//呼叫 joinFriend 方法
Method execute = person.getClass().getMethod("joinFriend");
execute.invoke(person);
}
public static void main(String[] args) {
try {
update();
} catch (Exception e) {
e.printStackTrace();
}
}
}
在personFly方法前后加上了列印日志,然后新增了一個方法joinFriend,執行main函式可以發現已經添加上了,
另外需要注意的是:上面的insertBefore() 和 setBody()中的陳述句,如果你是單行陳述句可以直接用雙引號,但是有多行陳述句的情況下,你需要將多行陳述句用{}括起來,javassist只接受單個陳述句或用大括號括起來的陳述句塊,
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2021最新版)
2.終于靠開源專案弄到 IntelliJ IDEA 激活碼了,真香!
3.阿里 Mock 工具正式開源,干掉市面上所有 Mock 工具!
4.Spring Cloud 2020.0.0 正式發布,全新顛覆性版本!
5.《Java開發手冊(嵩山版)》最新發布,速速下載!
覺得不錯,別忘了隨手點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/287526.html
標籤:Java
上一篇:面試中你有遇到這些Spring Cloud常問題嗎?知道如何完美解答嗎?
下一篇:列舉類
