Java字節碼增強探秘(java字節碼增強)
本文轉載自公眾號??美團技術團隊
大家好,美美今天給大家推薦一篇Java字節碼增強技術的文章,在實際工作中有很多應用場景。
1. 字節碼
圖3 JVM規定的字節碼結構
(1) 魔數(Magic Number)
圖4 常量池的結構
常量池計數器(constant_pool_count):由于常量的數量不固定,所以需要先放置兩個字節來表示常量池容量計數值。圖2中示例代碼的字節碼前10個字節如下圖5所示,將十六進制的24轉化為十進制值為36,排除掉下標“0”,也就是說,這個類文件***有35個常量。
圖5 前十個字節及含義
常量池數據區:數據區是由(constant_pool_count-1)個cp_info結構組成,一個cp_info結構對應一個常量。在字節碼***有14種類型的cp_info(如下圖6所示),每種類型的結構都是固定的。
圖6 各類型的cp_info
具體以CONSTANT_utf8_info為例,它的結構如下圖7左側所示。首先一個字節“tag”,它的值取自上圖6中對應項的Tag,由于它的類型是utf8_info,所以值為“01”。接下來兩個字節標識該字符串的長度Length,然后Length個字節為這個字符串具體的值。從圖2中的字節碼摘取一個cp_info結構,如下圖7右側所示。將它翻譯過來后,其含義為:該常量類型為utf8字符串,長度為一字節,數據為“a”。
圖7 CONSTANT_utf8_info的結構(左)及示例(右)
其他類型的cp_info結構在本文不再贅述,整體結構大同小異,都是先通過Tag來標識類型,然后后續n個字節來描述長度和(或)數據。先知其所以然,以后可以通過javap -verbose ByteCodeDemo命令,查看JVM反編譯后的完整常量池,如下圖8所示。可以看到反編譯結果將每一個cp_info結構的類型和值都很明確地呈現了出來。
圖8 常量池反編譯結果
(4) 訪問標志
圖9 訪問標志
(5) 當前類名
圖11 字段表示例
(9)方法表
字段表結束后為方法表,方法表也是由兩部分組成,第一部分為兩個字節描述方法的個數;第二部分為每個方法的詳細信息。方法的詳細信息較為復雜,包括方法的訪問標志、方法名、方法的描述符以及方法的屬性,如下圖所示:
圖12 方法表結構
方法的權限修飾符依然可以通過圖9的值查詢得到,方法名和方法的描述符都是常量池中的索引值,可以通過索引值在常量池中找到。而“方法的屬性”這一部分較為復雜,直接借助javap -verbose將其反編譯為人可以讀懂的信息進行解讀,如圖13所示。可以看到屬性中包括以下三個部分:
“Code區”:源代碼對應的JVM指令操作碼,在進行字節碼增強時重點操作的就是“Code區”這一部分。
“LineNumberTable”:行號表,將Code區的操作碼和源代碼中的行號對應,Debug時會起到作用(源代碼走一行,需要走多少個JVM指令操作碼)。
“LocalVariableTable”:本地變量表,包含This和局部變量,之所以可以在每一個方法內部都可以調用This,是因為JVM將This作為每一個方法的第一個參數隱式進行傳入。當然,這是針對非Static方法而言。
我們在上文所說的操作碼或者操作集合,其實控制的就是這個JVM的操作數棧。為了更直觀地感受操作碼是如何控制操作數棧的,以及理解常量池、變量表的作用,將add()方法的對操作數棧的操作制作為GIF,如下圖14所示,圖中僅截取了常量池中被引用的部分,以指令iconst_2開始到ireturn結束,與圖13中Code區0~17的指令一一對應:
圖15 jclasslib查看字節碼
2. 字節碼增強
在上文中,著重介紹了字節碼的結構,這為我們了解字節碼增強技術的實現打下了基礎。字節碼增強技術就是一類對現有字節碼進行修改或者動態生成全新字節碼文件的技術。接下來,我們將從最直接操縱字節碼的實現方式開始深入進行剖析。
圖16 字節碼增強技術
ClassReader:用于讀取已經編譯好的.class文件。
ClassWriter:用于重新構建編譯后的類,如修改類名、屬性以及方法,也可以生成新的類的字節碼文件。
各種Visitor類:如上所述,CoreAPI根據字節碼從上到下依次處理,對于字節碼文件中不同的區域有不同的Visitor,比如用于訪問方法的MethodVisitor、用于訪問類變量的FieldVisitor、用于訪問注解的AnnotationVisitor等。為了實現AOP,重點要使用的是MethodVisitor。
public?class?Base?{
public?void?process(){
System.out.println("process");
}
}
import?org.objectweb.asm.ClassReader;
import?org.objectweb.asm.ClassVisitor;
import?org.objectweb.asm.ClassWriter;
public?class?Generator?{
public?static?void?main(String[]?args)?throws?Exception?{
//讀取
ClassReader?classReader?=?new?ClassReader("meituan/bytecode/asm/Base");
ClassWriter?classWriter?=?new?ClassWriter(ClassWriter.COMPUTE_MAXS);
//處理
ClassVisitor?classVisitor?=?new?MyClassVisitor(classWriter);
classReader.accept(classVisitor,?ClassReader.SKIP_DEBUG);
byte[]?data?=?classWriter.toByteArray();
//輸出
File?f?=?new?File("operation-server/target/classes/meituan/bytecode/asm/Base.class");
FileOutputStream?fout?=?new?FileOutputStream(f);
fout.write(data);
fout.close();
System.out.println("now?generator?cc?success!!!!!");
}
}
import?org.objectweb.asm.ClassVisitor;
import?org.objectweb.asm.MethodVisitor;
import?org.objectweb.asm.Opcodes;
public?class?MyClassVisitor?extends?ClassVisitor?implements?Opcodes?{
public?MyClassVisitor(ClassVisitor?cv)?{
super(ASM5,?cv);
}
@Override
public?void?visit(int?version,?int?access,?String?name,?String?signature,
String?superName,?String[]?interfaces)?{
cv.visit(version,?access,?name,?signature,?superName,?interfaces);
}
@Override
public?MethodVisitor?visitMethod(int?access,?String?name,?String?desc,?String?signature,?String[]?exceptions)?{
MethodVisitor?mv?=?cv.visitMethod(access,?name,?desc,?signature,
exceptions);
//Base類中有兩個方法:無參構造以及process方法,這里不增強構造方法
if?(!name.equals("
mv?=?new?MyMethodVisitor(mv);
}
return?mv;
}
class?MyMethodVisitor?extends?MethodVisitor?implements?Opcodes?{
public?MyMethodVisitor(MethodVisitor?mv)?{
super(Opcodes.ASM5,?mv);
}
@Override
public?void?visitCode()?{
super.visitCode();
mv.visitFieldInsn(GETSTATIC,?"java/lang/System",?"out",?"Ljava/io/PrintStream;");
mv.visitLdcInsn("start");
mv.visitMethodInsn(INVOKEVIRTUAL,?"java/io/PrintStream",?"println",?"(Ljava/lang/String;)V",?false);
}
@Override
public?void?visitInsn(int?opcode)?{
if?((opcode?>=?Opcodes.IRETURN?&&?opcode?<=?Opcodes.RETURN)
||?opcode?==?Opcodes.ATHROW)?{
//方法在返回之前,打印"end"
mv.visitFieldInsn(GETSTATIC,?"java/lang/System",?"out",?"Ljava/io/PrintStream;");
mv.visitLdcInsn("end");
mv.visitMethodInsn(INVOKEVIRTUAL,?"java/io/PrintStream",?"println",?"(Ljava/lang/String;)V",?false);
}
mv.visitInsn(opcode);
}
}
}
首先通過MyClassVisitor類中的visitMethod方法,判斷當前字節碼讀到哪一個方法了。跳過構造方法"
接下來,進入內部類MyMethodVisitor中的visitCode方法,它會在ASM開始訪問某一個方法的Code區時被調用,重寫visitCode方法,將AOP中的前置邏輯就放在這里。
MyMethodVisitor繼續讀取字節碼指令,每當ASM訪問到無參數指令時,都會調用MyMethodVisitor中的visitInsn方法。我們判斷了當前指令是否為無參數的“return”指令,如果是就在它的前面添加一些指令,也就是將AOP的后置邏輯放在該方法中。
綜上,重寫MyMethodVisitor中的兩個方法,就可以實現AOP了,而重寫方法時就需要用ASM的寫法,手動寫入或者修改字節碼。通過調用methodVisitor的visitXXXXInsn()方法就可以實現字節碼的插入,XXXX對應相應的操作碼助記符類型,比如mv.visitLdcInsn("end")對應的操作碼就是ldc "end",即將字符串“end”壓入棧。
完成這兩個Visitor類后,運行Generator中的main方法完成對Base類的字節碼增強,增強后的結果可以在編譯后的Target文件夾中找到Base.class文件進行查看,可以看到反編譯后的代碼已經改變了(如圖18左側所示)。然后寫一個測試類MyTest,在其中new Base(),并調用base.process()方法,可以看到下圖右側所示的AOP實現效果:
圖18 ASM實現AOP的效果
CtClass(compile-time class):編譯時類信息,它是一個Class文件在代碼中的抽象表現形式,可以通過一個類的全限定名來獲取一個CtClass對象,用來表示這個類文件。
ClassPool:從開發視角來看,ClassPool是一張保存CtClass信息的HashTable,Key為類名,Value為類名對應的CtClass對象。當我們需要對某個類進行修改時,就是通過pool.getCtClass("className")方法從pool中獲取到相應的CtClass。
CtMethod、CtField:這兩個比較好理解,對應的是類中的方法和屬性。
import?com.meituan.mtrace.agent.javassist.*;
public?class?JavassistTest?{
public?static?void?main(String[]?args)?throws?NotFoundException,?CannotCompileException,?IllegalAccessException,?InstantiationException,?IOException?{
ClassPool?cp?=?ClassPool.getDefault();
CtClass?cc?=?cp.get("meituan.bytecode.javassist.Base");
CtMethod?m?=?cc.getDeclaredMethod("process");
m.insertBefore("{?System.out.println("start");?}");
m.insertAfter("{?System.out.println("end");?}");
Class?c?=?cc.toClass();
cc.writeFile("/Users/zen/projects");
Base?h?=?(Base)c.newInstance();
h.process();
}
}
import?java.lang.management.ManagementFactory;
public?class?Base?{
public?static?void?main(String[]?args)?{
String?name?=?ManagementFactory.getRuntimeMXBean().getName();
String?s?=?name.split("@")[0];
//打印當前Pid
System.out.println("pid:"+s);
while?(true)?{
try?{
Thread.sleep(5000L);
}?catch?(Exception?e)?{
break;
}
process();
}
}
public?static?void?process()?{
System.out.println("process");
}
}
我們定義一個實現了ClassFileTransformer接口的類TestTransformer,依然在其中利用Javassist對Base類中的process()方法進行增強,在前后分別打印“start”和“end”,代碼如下:
import?java.lang.instrument.ClassFileTransformer;
public?class?TestTransformer?implements?ClassFileTransformer?{
@Override
public?byte[]?transform(ClassLoader?loader,?String?className,?Class>?classBeingRedefined,?ProtectionDomain?protectionDomain,?byte[]?classfileBuffer)?{
System.out.println("Transforming?"?+?className);
try?{
ClassPool?cp?=?ClassPool.getDefault();
CtClass?cc?=?cp.get("meituan.bytecode.jvmti.Base");
CtMethod?m?=?cc.getDeclaredMethod("process");
m.insertBefore("{?System.out.println("start");?}");
m.insertAfter("{?System.out.println("end");?}");
return?cc.toBytecode();
}?catch?(Exception?e)?{
e.printStackTrace();
}
return?null;
}
}
import?java.lang.instrument.Instrumentation;
public?class?TestAgent?{
public?static?void?agentmain(String?args,?Instrumentation?inst)?{
//指定我們自己定義的Transformer,在其中利用Javassist做字節碼替換
inst.addTransformer(new?TestTransformer(),?true);
try?{
//重定義類并載入新的字節碼
inst.retransformClasses(Base.class);
System.out.println("Agent?Load?Done.");
}?catch?(Exception?e)?{
System.out.println("agent?load?failed!");
}
}
}
定義Agent,并在其中實現AgentMain方法,如上一小節中定義的代碼塊7中的TestAgent類;
然后將TestAgent類打成一個包含MANIFEST.MF的jar包,其中MANIFEST.MF文件中將Agent-Class屬性指定為TestAgent的全限定名,如下圖所示;
圖22 Manifest.mf
最后利用Attach API,將我們打包好的jar包Attach到指定的JVM pid上,代碼如下:
import?com.sun.tools.attach.VirtualMachine;
public?class?Attacher?{
public?static?void?main(String[]?args)?throws?AttachNotSupportedException,?IOException,?AgentLoadException,?AgentInitializationException?{
//?傳入目標?JVM?pid
VirtualMachine?vm?=?VirtualMachine.attach("39333");
vm.loadAgent("/Users/zen/operation_server_jar/operation-server.jar");
}
}
由于在MANIFEST.MF中指定了Agent-Class,所以在Attach后,目標JVM在運行時會走到TestAgent類中定義的agentmain()方法,而在這個方法中,我們利用Instrumentation,將指定類的字節碼通過定義的類轉化器TestTransformer做了Base類的字節碼替換(通過javassist),并完成了類的重新加載。由此,我們達成了“在JVM運行時,改變類的字節碼并重新載入類信息”的目的。
以下為運行時重新載入類的效果:先運行Base中的main()方法,啟動一個JVM,可以在控制臺看到每隔五秒輸出一次"process"。接著執行Attacher中的main()方法,并將上一個JVM的pid傳入。此時回到上一個main()方法的控制臺,可以看到現在每隔五秒輸出"process"前后會分別輸出"start"和"end",也就是說完成了運行時的字節碼增強,并重新載入了這個類。
熱部署:不部署服務而對線上服務做修改,可以做打點、增加日志等操作。
Mock:測試時候對某些服務做Mock。
性能診斷工具:比如bTrace就是利用Instrument,實現無侵入地跟蹤一個正在運行的JVM,監控到類和方法級別的狀態信息。
4. 總結
5. 參考文獻
《ASM4-Guide》
Oracle:The class File Format
Oracle:The Java Virtual Machine Instruction Set
Javassist tutorial
JVM Tool Interface - Version 1.2
澤恩,美團到店住宿業務研發團隊工程師。
本文轉載自公眾號【程序員小灰】
Java 存儲 JVM
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。