1. 前言
前幾天,在Q群里有個(gè)大佬,展示了下 Android 做無痕埋點(diǎn),覺得挺厲害的
問了下使用的是 AspectJ, 網(wǎng)上搜了下資料 ASM 比 AspectJ 更靈活,更輕量
剛好趁著五一假期系統(tǒng)的學(xué)習(xí)下
2. 介紹
ASM 是一款輕量級(jí)的Java字節(jié)碼操作倉(cāng)庫(kù)
3. 前期準(zhǔn)備
3.1 簡(jiǎn)單的asm 方面的知識(shí)
ASM 主要有幾個(gè)類需要了解 而且需要對(duì) Java字節(jié)碼 比較熟悉
ClassReader
字節(jié)碼的讀取與分析引擎扛点。它采用類似SAX的事件讀取機(jī)制雕沉,每當(dāng)有事件發(fā)生時(shí),調(diào)用注冊(cè)的ClassVisitor奢人、AnnotationVisitor攻走、FieldVisitor、MethodVisitor做相應(yīng)的處理此再。
ClassVisitor
定義在讀取Class字節(jié)碼時(shí)會(huì)觸發(fā)的事件昔搂,如類頭解析完成、注解解析输拇、字段解析摘符、方法解析等
AnnotationVisitor
定義在解析注解時(shí)會(huì)觸發(fā)的事件,如解析到一個(gè)基本值類型的注解策吠、enum值類型的注解逛裤、Array值類型的注解、注解值類型的注解等
FieldVisitor
定義在解析字段時(shí)觸發(fā)的事件猴抹,如解析到字段上的注解带族、解析到字段相關(guān)的屬性等
MethodVisitor
定義在解析方法時(shí)觸發(fā)的事件,如方法上的注解蟀给、屬性蝙砌、代碼等。
ClassWriter
它實(shí)現(xiàn)了ClassVisitor接口跋理,用于拼接字節(jié)碼择克。
3.2 開發(fā)工具準(zhǔn)備
idea / Android studio
ASM Bytecode Viewer(對(duì) Java字節(jié)碼 不熟悉的話必備)
4 實(shí)戰(zhàn)
4.1 要實(shí)現(xiàn)的效果
class User {
public static void main(String[] args) {
show();
}
public static void show(){
System.out.println("Hello World");
}
}
上圖為一個(gè) User類,要對(duì) show() 方法的耗時(shí)進(jìn)行計(jì)算并打印
4.2 編寫 ASM 邏輯
4.2.1 編寫 ClassVisitor
解析類的監(jiān)聽器,解析Class字節(jié)碼時(shí)會(huì)觸發(fā)內(nèi)部的方法
public class TestClassVisitor extends ClassVisitor {
public TestClassVisitor(final ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
if (cv != null) {
cv.visit(version, access, name, signature, superName, interfaces);
}
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
//如果methodName是show,則返回我們自定義的TestMethodVisitor
if ("show".equals(name)) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
return new TestMethodVisitor(mv);
}
if (cv != null) {
return cv.visitMethod(access, name, desc, signature, exceptions);
}
return null;
}
}
4.2.2 編寫 MethodVisitor
解析方法的監(jiān)聽器,解析Method時(shí)會(huì)觸發(fā)內(nèi)部的方法
編寫前若對(duì) Java字節(jié)碼 不熟悉的話
建議安裝 ASM Bytecode Viewer 插件!!!
建議安裝 ASM Bytecode Viewer 插件!!!
建議安裝 ASM Bytecode Viewer 插件!!!
先新建一個(gè)類 編寫要注入的代碼,然后用插件查看
public class TestMethodVisitor extends MethodVisitor implements Opcodes {
public TestMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}
@Override
public void visitCode() {
//方法體內(nèi)開始時(shí)調(diào)用
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(LSTORE, 0);
super.visitCode();
}
@Override
public void visitInsn(int opcode) {
//每執(zhí)行一個(gè)指令都會(huì)調(diào)用
if (opcode == Opcodes.RETURN) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(LLOAD, 0);
mv.visitInsn(LSUB);
mv.visitVarInsn(LSTORE, 2);
Label l3 = new Label();
mv.visitLabel(l3);
mv.visitLineNumber(11, l3);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitLdcInsn("== method cost time = ");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(LLOAD, 2);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
mv.visitLdcInsn(" ==");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
super.visitInsn(opcode);
}
}
4.3 測(cè)試效果
編寫測(cè)試類 運(yùn)行
public class Demo {
public static void main(String[] args) throws IOException {
ClassReader cr = new ClassReader(User.class.getName());
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new TestClassVisitor(cw);
cr.accept(cv, Opcodes.ASM5);
// 獲取生成的class文件對(duì)應(yīng)的二進(jìn)制流
byte[] code = cw.toByteArray();
//將二進(jìn)制流寫到out/下
FileOutputStream fos = new FileOutputStream("out/User.class");
fos.write(code);
fos.close();
}
}
原User 類生成的 .class 文件,以及輸出效果
字節(jié)碼修改后的 User 類的 .class 文件以及輸出效果
用途
可以用于無痕埋點(diǎn),打印日志,以及性能監(jiān)控等