什么是字節碼
java
程序通過javac
編譯之后生成文件.class
就是字節碼集合,正是有這樣一種中間碼(字節碼)
,使得scala/groovy/clojure
等函數語言只用實現一個編譯器即可運行在JVM
上。
看看一段簡單代碼。
public long getExclusiveTime() {
long startTime = System.currentTimeMillis();
System.out.printf("exclusive code");
long endTime = System.currentTimeMillis();
return endTime - startTime;
}
public class com.blueware.agent.StartAgent {
編譯后通過命令(javap -c com.blueware.agent.StartAgent
)查看,具體含義請參考oracle
public com.blueware.agent.StartAgent();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public long getExclusiveTime();
Code:
0: invokestatic #2 // Method java/lang/System.currentTimeMillis:()J
3: lstore_1
4: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #4 // String exclusive code
9: iconst_0
10: anewarray #5 // class java/lang/Object
13: invokevirtual #6 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
16: pop
17: invokestatic #2 // Method java/lang/System.currentTimeMillis:()J
20: lstore_3
21: lload_3
22: lload_1
23: lsub
24: lreturn
}
為什么要學習字節碼
- 能了解技術背后的原理,更容易寫出高質量代碼;
- 字節碼設計非常優秀,發展十幾年只僅僅刪除和增加幾個指令,學懂之后長期受益高,如果懂字節碼再學習
scala/groovy/clojure
會容易很多; - 開發框架、監控系統、中間件、語言字節碼技術都是必殺技;
字節碼框架(ASM/Javassist
)
操作字節碼框架有很多,具體可以參考博文,下面對比ASM/Javassist
選項 | 優點 | 缺點 |
---|---|---|
ASM |
速度快、代碼量小、功能強大 | 要寫字節碼、學習曲線高 |
Javassist |
學習簡單,不用寫字節碼 | 比ASM 慢,功能少 |
Java Instrumentation
介紹
指的是可以用獨立於應用程序之外的代理(agent
)程序,agent
程序通過增強字節碼動態修改或者新增類,利用這樣特性可以設計出更通用的監控、框架、中間件程序,在JVM
啟動參數加–javaagent:agent_jar_path/agent.jar
即可運行(在JDK5
及其后續版本才可以),更多關於Instrumentation
知識請參考博文
計算方法執行時間方式
- 直接在代碼開始和結束出打印當前時間,相減即可得到;
- 實現一個動態代理,或者借助
Spring/AspectJ
等框架; - 上面兩種實現方式都需要修改代碼或者配置文件,下面我要介紹方式不僅不需要修改代碼,而且效率高;
具體實現方式
1.StartAgent
類必須提供premain
方法,代碼如下:
public class StartAgent {
//代理程序入口函數
public static void premain(String args, Instrumentation inst) {
System.out.println("agent begin");
//添加字節碼轉換器
inst.addTransformer(new PrintTimeTransformer());
System.out.println("agent end");
}
}
2.PrintTimeTransformer
實現一個轉換器,代碼如下:
//字節碼轉化器類
public class PrintTimeTransformer implements ClassFileTransformer {
//實現字節碼轉化接口,一個小技巧建議實現接口方法時寫@Override,方便重構
//loader:定義要轉換的類加載器,如果是引導加載器,則為 null(在這個小demo暫時還用不到)
//className:完全限定類內部形式的類名稱和中定義的接口名稱,例如"java.lang.instrument.ClassFileTransformer"
//classBeingRedefined:如果是被重定義或重轉換觸發,則為重定義或重轉換的類;如果是類加載,則為 null
//protectionDomain:要定義或重定義的類的保護域
//classfileBuffer:類文件格式的輸入字節緩沖區(不得修改)
//一個格式良好的類文件緩沖區(轉換的結果),如果未執行轉換,則返回 null。
@Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
//簡化測試demo,直接寫待修改的類(com/blueware/agent/TestTime)
if (className != null && className.equals("com/blueware/agent/TestTime")) {
//讀取類的字節碼流
ClassReader reader = new ClassReader(classfileBuffer);
//創建操作字節流值對象,ClassWriter.COMPUTE_MAXS:表示自動計算棧大小
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
//接受一個ClassVisitor子類進行字節碼修改
reader.accept(new TimeClassVisitor(writer, className), 8);
//返回修改后的字節碼流
return writer.toByteArray();
}
return null;
}
}
3.TimeClassVisitor
類訪問器,實現字節碼修改,代碼如下:
//定義掃描待修改class的visitor,visitor就是訪問者模式
public class TimeClassVisitor extends ClassVisitor {
private String className;
public TimeClassVisitor(ClassVisitor cv, String className) {
super(Opcodes.ASM5, cv);
this.className = className;
}
//掃描到每個方法都會進入,參數詳情下一篇博文詳細分析
@Override public MethodVisitor visitMethod(int access, final String name, final String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
final String key = className + name + desc;
//過來待修改類的構造函數
if (!name.equals("<init>") && mv != null) {
mv = new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
//方法進入時獲取開始時間
@Override public void onMethodEnter() {
//相當於com.blueware.agent.TimeUtil.setStartTime("key");
this.visitLdcInsn(key);
this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/blueware/agent/TimeUtil", "setStartTime", "(Ljava/lang/String;)V", false);
}
//方法退出時獲取結束時間並計算執行時間
@Override public void onMethodExit(int opcode) {
//相當於com.blueware.agent.TimeUtil.setEndTime("key");
this.visitLdcInsn(key);
this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/blueware/agent/TimeUtil", "setEndTime", "(Ljava/lang/String;)V", false);
//向棧中壓入類名稱
this.visitLdcInsn(className);
//向棧中壓入方法名
this.visitLdcInsn(name);
//向棧中壓入方法描述
this.visitLdcInsn(desc);
//相當於com.blueware.agent.TimeUtil.getExclusiveTime("com/blueware/agent/TestTime","testTime");
this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/blueware/agent/TimeUtil", "getExclusiveTime", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", false);
}
};
}
return mv;
}
}
4.TimeClassVisitor
記錄時間幫助類,代碼如下:
public class TimeUtil {
private static Map<String, Long> startTimes = new HashMap<String, Long>();
private static Map<String, Long> endTimes = new HashMap<String, Long>();
private TimeUtil() {
}
public static long getStartTime(String key) {
return startTimes.remove(key);
}
public static void setStartTime(String key) {
startTimes.put(key, System.currentTimeMillis());
}
public static long getEndTime(String key) {
return endTimes.remove(key);
}
public static void setEndTime(String key) {
endTimes.put(key, System.currentTimeMillis());
}
public static void getExclusiveTime(String className, String methodName, String methodDesc) {
String key = className + methodName + methodDesc;
long exclusive = getEndTime(key) - getStartTime(key);
System.out.println(className.replace("/", ".") + "." + methodName + " exclusive:" + exclusive);
}
}
題記