Java之因此能夠"一次編譯,處處運行",一是由於JVM針對各類平臺和操做系統都進行了定製,對開發者屏蔽了底層細節。二是由於不管在任何平臺都會編譯生成固定格式的字節碼(.class)文件供JVM使用,不一樣平臺上的JVM虛擬機均可以載入和執行同一種和平臺無關的字節碼。因而可知字節碼對於Java生態的重要性。之因此被稱爲字節碼,是由於字節碼文件由16進制值組成,JVM以字節爲單位進行讀取。在Java中通常使用javac命令編譯源代碼爲字節碼文件,一個.java文件從編譯到運行的示例以下圖所示。java
.java文件經過javac編譯後將獲得.class文件,如編寫一個簡單的ByteCodeDemo類,以下圖的左側部分。編譯後生成ByteCodeDemo.class文件,打開後是一堆16進制數,按字節爲單位分割展現以下圖右側部分展現。框架
JVM規範要求每個字節碼文件都須要由10個部分按固定順序組成,總體結果以下圖所示。ide
ASM是一個Java字節碼操控框架。它能夠被用來動態生成類或者加強既有類的功能。ASM能夠直接產生二進制的class文件,也能夠在類被加載到Java虛擬機以前改變類行爲。ASM的應用場景有AOP(cglib基於ASM)、熱部署、修改其它jar包中的類等。ASM修改字節碼流程以下圖所示。spa
用於讀取已經編譯好的.class文件;操作系統
用於從新構建編譯後的類,如修改類名、屬性及方法等;插件
ASM內部採用訪問者模式根據字節碼從上到下依次處理,對於字節碼文件中不一樣的區域有不一樣的Visitor。code
當訪問不一樣區域時會回調相應方法,該方法會返回一個對應的字節碼操做對象。經過修改這個對象就能夠修改class文件相應結構對應的內容。最後將這個對象的字節碼內容覆蓋原先的.class文件就能夠實現字節碼的切入。cdn
代碼示例以下,本來Base的process方法只輸出一行"process"。對象
目標:咱們指望經過字節碼加強後,在方法執行前輸出"start",在方法執行後輸出"end"。blog
public class Base {
public void process() {
System.out.println("process");
}
}
複製代碼
要實如今process方法先後插入輸出代碼,須要有如下步驟:
基本實現代碼參考以下,SimpleGenerator類定義了ClassReader和ClassWriter對象。其中ClassReader負責讀取字節碼,而後交給ClassVisitor類處理,處理完成後由ClassWriter完成字節碼文件替換。
public class SimpleGenerator {
public static void main(String[] args) throws Exception {
//讀取類文件
ClassReader classReader = new ClassReader("com.example.aop.asm.Base");
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
//處理,經過classVisitor修改類
ClassVisitor classVisitor = new SimpleClassVisitor(classWriter);
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
byte[] data = classWriter.toByteArray();
//保存新的字節碼文件
File file = new File("target/classes/com/example/aop/asm/Base.class");
FileOutputStream outputStream = new FileOutputStream(file);
outputStream.write(data);
outputStream.close();
System.out.println("generator new class file success.");
}
}
複製代碼
SimpleClassVisitor類繼承ClassVisitor類。ClassVisitor中各方法按如下順序調用,這個順序在ClassVisitor的java doc中也有說明。
[
visitSource
] [visitOuterClass
] (visitAnnotation
|visitTypeAnnotation
|visitAttribute
)* (visitInnerClass
|visitField
|visitMethod
)*visitEnd
.
其中visitMethod在訪問方法時調用,咱們能夠經過擴展MethodVisitor方法來實現加強邏輯。相似地,MethodVisitor也須要按順序調用其方法。
(
visitParameter
)* [visitAnnotationDefault
] (visitAnnotation
|visitAnnotableParameterCount
|visitParameterAnnotation
visitTypeAnnotation
|visitAttribute
)* [visitCode
(visitFrame
|visitXInsn
|visitLabel
|visitInsnAnnotation
|visitTryCatchBlock
|visitTryCatchAnnotation
|visitLocalVariable
|visitLocalVariableAnnotation
|visitLineNumber
)*visitMaxs
]visitEnd
.
public class SimpleClassVisitor extends ClassVisitor {
public SimpleClassVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
}
// visitMethod在訪問類方法時回調,能夠獲知當前訪問到方法信息
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature,
exceptions);
// 不須要加強Base類的構造方法
if (mv != null && !name.equals("<init>")) {
// 經過定製MethodVisitor,替換原來的MethodVisitor對象來實現方法改寫
mv = new SimpleMethodVisitor(mv);
}
return mv;
}
/** * 定製方法visitor */
class SimpleMethodVisitor extends MethodVisitor {
public SimpleMethodVisitor(MethodVisitor methodVisitor) {
super(Opcodes.ASM5, methodVisitor);
}
// visitCode方法在ASM開始訪問方法的Code區時回調
@Override
public void visitCode() {
super.visitCode();
// System.out.println("start")對應的字節碼,在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);
}
// 每當ASM訪問到無參數指令時,都會調用visitInsn方法
@Override
public void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
// System.out.println("end")對應的字節碼,在visitInsn方法返回前添加
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);
}
super.visitInsn(opcode);
}
}
}
複製代碼
運行SimpleGenerator類的main方法便可完成對Base類的字節碼加強。加強後的class文件經過反編譯後結果以下圖所示。能夠看到對應的代碼已經改變,在其先後增長了"start"和"end"的輸出。
能夠經過ASM ByteCode Outline 插件很方便地實現源碼到字節碼的映射。
歡迎關注個人公衆號~