作者:董子龍
前言
前段時間一直想參照lombok的實作原理寫一篇可以生成業務單據修改記錄插件的專利,再查閱資料的程序中,偶然了解到了位元組碼增強工具-byteBuddy,但是由于當時時間緊促,所以沒有深入的對該組件進行了解,其實再我們的日常開發中,位元組碼增強組件的身影無處不在,例如spring-aop和mybatis,本著知其然也要知其所以然的精神,我決定沉下心來,對位元組碼增強技術做一個深入的學習和總結,本文作為該系列的開篇,主要是對位元組碼做一下簡單的介紹,為我們后面的深入學習打下一個好的基礎,
一、位元組碼簡述
位元組碼是一種中間狀態的二進制檔案,是由原始碼編譯過來的,可讀性沒有原始碼的高,cpu并不能直接讀取位元組碼,在java中,位元組碼需要經過JVM轉譯成機械碼之后,cpu才能讀取并運行,
使用位元組碼的好處:一處編譯,到處運行,java就是典型的使用位元組碼作為中間語言,在一個地方編譯了原始碼,拿著.class檔案就可以在各種計算機運行,
二、位元組碼增強的使用場景
如果我們不想修改原始碼,但是又想加入新功能,讓程式按照我們的預期去運行,可以通過編譯程序和加載程序中去做相應的操作,簡單來講就是:將生成的.class檔案修改或者替換稱為我們需要的目標.class檔案,
由于位元組碼增強可以在完全不侵入業務代碼的情況下植入代碼邏輯,所以可以用它來做一些酷酷的事,比如下面的幾種常見場景:
1、動態代理
2、熱部署
3、呼叫鏈跟蹤埋點
4、動態插入log(性能監控)
5、測驗代碼覆寫率跟蹤
...
三、位元組碼增強的實作方式
| 位元組碼工具 | 類創建 | 實作介面 | 方法呼叫 | 類擴展 | 父類方法呼叫 | 優點 | 缺點 | 常見使用 | 學習成本 |
|---|---|---|---|---|---|---|---|---|---|
| java-proxy | 支持 | 支持 | 支持 | 不支持 | 不支持 | 簡單動態代理首選 | 功能有限,不支持擴展 | spring-aop,MyBatis | 1星 |
| asm | 支持 | 支持 | 支持 | 支持 | 支持 | 任意位元組碼插入,幾乎不受限制 | 學習難度大,撰寫代碼多 | cglib | 5星 |
| javaassit | 支持 | 支持 | 支持 | 支持 | 支持 | java原始語法,字串形式插入,寫入直觀 | 不支持jdk1.5以上的語法,如泛型,增強for | Fastjson,MyBatis | 2星 |
| cglib | 支持 | 支持 | 支持 | 支持 | 支持 | 與bytebuddy看起來差不多 | 正在被bytebuddy淘汰 | EasyMock,jackson-databind | 3星 |
| bytebuddy | 支持 | 支持 | 支持 | 支持 | 支持 | 支持任意維度的攔截,可以獲取原始類、方法,以及代理類和全部引數 | 不太直觀,學習理解有些成本,API非常多 | SkyWalking,Mockito,Hibernate,powermock | 3星 |
四、簡單示例
AOP是我們在日常開發中常用的架構設計思想,AOP的主要的實作有cglib,Aspectj,Javassist,java proxy等,接下來,我們就以我們日常開發中會遇到的在方法執行前后列印日志為切入點,手動用位元組碼來實作一下AOP,
定義目標介面與實作
public class SayService{
public void say(String str) {
System.out.println("hello" + str);
}
}
定義了類SayService,再執行say方法之前,我們會列印方法開始執行start,方法執行之后,我們會列印方法執行結束end
ASM實作AOP
4.1.1、引入jar包
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.1</version>
</dependency>
4.1.2、AOP具體實作
public class ResourceClassVisitor extends ClassVisitor implements Opcodes {
public ResourceClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM4, cv);
}
public ResourceClassVisitor(int i, ClassVisitor classVisitor) {
super(i, classVisitor);
}
/**訪問類基本資訊*/
@Override
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces) {
this.cv.visit(version, access, name, signature, superName, interfaces);
}
/**訪問方法基本資訊*/
@Override
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
MethodVisitor mv = this.cv.visitMethod(access, name, desc,
signature, exceptions);
//假如不是構造方法,我們構建方法的訪問物件(MethodVisitor)
if (!name.equals("<init>") && mv != null) {
mv = new ResourceClassVisitor.MyMethodVisitor((MethodVisitor)mv);
}
return (MethodVisitor)mv;
}
/**自定義方法訪問物件*/
class MyMethodVisitor extends MethodVisitor implements Opcodes {
public MyMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM4, mv);
}
/**此方法會在方法執行之前執行*/
@Override
public void visitCode() {
super.visitCode();
this.mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out",
"Ljava/io/PrintStream;");
this.mv.visitLdcInsn("方法開始執行start");
this.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream",
"println", "(Ljava/lang/String;)V", false);
}
/**對應方法體本身*/
@Override
public void visitInsn(int opcode) {
//在方法return或例外之前,添加一個end輸出
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
this.mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out",
"Ljava/io/PrintStream;");
this.mv.visitLdcInsn("方法執行結束end");
this.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream",
"println", "(Ljava/lang/String;)V", false);
}
this.mv.visitInsn(opcode);
}
}
}
public class AopTest {
public static void main(String[] args) throws IOException {
//第一步:構建ClassReader物件,讀取指定位置的class檔案(默認是類路徑-classpath)
ClassReader classReader = new ClassReader("com/aop/SayService");
//第二步:構建ClassWriter物件,基于此物件創建新的class檔案
//ClassWriter.COMPUTE_FRAMES 表示ASM會自動計算max stacks、max locals和stack map frame的具體內容,
//ClassWriter.COMPUTE_MAXS 表示ASM會自動計算max stacks和max locals,但不會自動計算stack map frames,
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);//推薦使用COMPUTE_FRAMES
//第三步:構建ClassVisitor物件,此物件用于接收ClassReader物件的資料,并將資料處理后傳給ClassWriter物件
ClassVisitor classVisitor = new ResourceClassVisitor(classWriter);
//第四步:基于ClassReader讀取class資訊,并將資料傳遞給ClassVisitor物件
//這里的引數ClassReader.SKIP_DEBUG表示跳過一些除錯資訊等,ASM代碼看上去就會更簡潔
//這里的引數ClassReader.SKIP_FRAMES表示跳過一些方法中的部分堆疊幀資訊,堆疊幀手動計算非常復雜,所以交給系統去做吧
//推薦用這兩個引數
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG|ClassReader.SKIP_FRAMES);
//第五步:從ClassWriter拿到資料,并將資料寫出到一個class檔案中
byte[] data = https://www.cnblogs.com/Jcloud/p/classWriter.toByteArray();
//將位元組碼寫入到磁盤的class檔案
File f = new File("target/classes/com/aop/SayService.class");
FileOutputStream fout = new FileOutputStream(f);
fout.write(data);
fout.close();
SayService rs = new SayService();
rs.say("asm");//start,handle(),end
}
}
4.1.3、測驗類輸出結果
Javassist實作AOP
4.2.1、引入jar包
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
4.2.2、AOP具體實作
public class AopTest {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("com.aop.SayService");
CtMethod personFly = cc.getDeclaredMethod("say");
personFly.insertBefore("System.out.println("方法開始執行start");");
personFly.insertAfter("System.out.println("方法執行結束end");");
cc.toClass();
SayService sayService = new SayService();
sayService.say("assist");
}
}
4.2.3、測驗類輸出結果
五、總結
作為位元組碼增強系列文章的開篇,只是簡單的介紹了一下位元組碼的定義、位元組碼的實作方式,最后通過具體示例向大家展示了如何對位元組碼進行增強,再后續的文章中,會對相關框架的原理及具體應用做一個細化的總結,歡迎各位大佬的批評與指正,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/539770.html
標籤:其他
上一篇:什么是CodeArts?
