Whats is Java Agent? .. java.lang.instrument.Instrumentation
之前有寫 基於AOP的日誌除錯 討論一種跟蹤Java程式的方法, 但不是很完美.後來發現了 Btrace , 由於它藉助動態位元組碼注入技術 , 實現優雅且功能強大.
只不過, 用起來總是磕磕絆絆的, 時常為了跟蹤某個問題, 卻花了大把的時間除錯Btrace的指令碼. 為此, 我嘗試將幾種跟蹤模式固化成指令碼模板, 待用的時候去調整一下正規表示式之類的.
跟蹤過程往往是假設與驗證的螺旋迭代過程, 反覆的用BTrace跟蹤目標程式, 總有那麼幾次莫名其妙的不可用, 最後不得不重啟目標程式. 若真是線上不能停的服務, 我想這種方式還是不靠譜啊.
為此, 據決定自己的搞個用起來簡單, 又能良好支援反覆跟蹤而不用重啟目標程式的工具.
AOP
AOP是Btrace, jip1等眾多監測工具的核心思想, 用一段程式碼最容易說明:
1
2
3
4
5
|
public void say(String words){
Trace.enter();
System.out.println(words);
Trace.exit();
} |
如上, Trace.enter() 和 Trace.exit() 將say(words)內的程式碼環抱起來, 對方法進出的進行切面的處理, 便可獲取執行時的上下文, 如:
- 呼叫棧
- 當前執行緒
- 時間消耗
- 引數與返回值
- 當前例項狀態
實現的選擇
實現切面的方式, 我知道的有以下幾種:
代理(裝飾器)模式
設計模式中裝飾器模式和代理模式, 儘管解決的問題域不同, 程式碼實現是非常相似, 均可以實現切面處理, 這裡視為等價. 依舊用程式碼說明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
interface Person {
void say(String words);
} class Officer implements Person {
public void say(String words) { lie(words); }
private void lie(String words) {...}
} class Proxy implements Person {
private final Officer officer;
public Proxy(Officer officer) { this .officer = officer; }
public void say(String words) {
enter();
officer.say(words);
exit();
}
private void enter() { ... }
private void exit() { ... }
} Person p = new Proxy( new Officer());
|
很明顯, 上述enter() 和exit()是實現切面的地方, 通過獲取Officer的Proxy例項, 便可對Officer例項的行為進行跟蹤. 這種方式實現起來最簡單, 也最直接.
Java Proxy
Java Proxy是JDK內建的代理API, 藉助反射機制實現. 用它來是完成切面則會是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class ProxyInvocationHandler implements InvocationHandler {
private final Object target;
public ProxyInvocationHandler(Object target) { this .target = target;}
public Object handle(Object proxy, Method method, Object[] args) {
enter();
method.invoke(target, args);
exit();
}
private void enter() { ... }
private void exit() { ... }
} ClassLoader loader = ... Class<?>[] interfaces = {Person. class };
Person p = (Person)Proxy.newInstance(loader, interfaces, new ProxyInvocationHandler( new Officer()));
|
相比較上一中方法, 這種不太易讀, 但更為通用, 對具體實現依賴很少.
AspectJ
AspectJ是基於位元組碼操作(執行時利用ASM庫)的AOP實現, 相比較Java proxy, 它會顯得對呼叫更”透明”, 編寫更簡明(類似DSL), 效能更好. 如下程式碼:
1
2
3
|
pointcut say(): execute(* say(..)) before(): say() { ... } after() : say() { ... } |
Aspectj實現切面的時機有兩種: 靜態編譯和類載入期編織(load-time weaving). 並且它對IDE的支援很豐富.
CGlib
與AspectJ一樣CGlib也是操作位元組碼來實現AOP的, 使用上與Java Proxy非常相似, 只是不像Java Proxy對介面有依賴, 我們熟知的Spring, Guice之類的IoC容器實現AOP都是使用它來完成的.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class Callback implements MethodInterceptor {
public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, MethodProxy proxy) throws Throwable {
enter();
proxy.invokeSuper(obj, args);
exit();
}
private void enter() { ... }
private void exit() { ... }
} Enhancer e = new Enhancer();
e.setSuperclass(Officer. class );
e.setCallback( new Callback());
Person p = e.create(); |
位元組碼操縱
上面四種方法各有適用的場景, 但唯獨對執行著的Java程式進行動態的跟蹤支援不了, 當然也許是我瞭解的不夠深入, 若有基於上述方案的辦法還請不吝賜教.
還是回到Btrace的思路上來, 在理解了它藉助java.lang.Instrumentation進行位元組碼注入的實現原理後, 實現動態變化跟蹤方式或目標應該沒有問題.
借下來的問題, 如何操作(注入)位元組碼實現切面的處理. 可喜的是, “構建自己的監測工具”一文給我提供了一個很好的切入點. 在此基礎上, 經過一些對ASM的深入研究, 可以實現:
- 方法呼叫進入時, 獲取當前例項(this) 和 引數值列表;
- 方法呼叫出去時, 獲取返回值;
- 方法異常丟擲時, 觸發回撥並獲取異常例項.
其切面實現的核心程式碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
private static class ProbeMethodAdapter extends AdviceAdapter {
protected ProbeMethodAdapter(MethodVisitor mv, int access, String name, String desc, String className) {
super (mv, access, name, desc);
start = new Label();
end = new Label();
methodName = name;
this .className = className;
}
@Override
public void visitMaxs( int maxStack, int maxLocals) {
mark(end);
catchException(start, end, Type.getType(Throwable. class ));
dup();
push(className);
push(methodName);
push(methodDesc);
loadThis();
invokeStatic(Probe.TYPE, Probe.EXIT);
visitInsn(ATHROW);
super .visitMaxs(maxStack, maxLocals);
}
@Override
protected void onMethodEnter() {
push(className);
push(methodName);
push(methodDesc);
loadThis();
loadArgArray();
invokeStatic(Probe.TYPE, Probe.ENTRY);
mark(start);
}
@Override
protected void onMethodExit( int opcode) {
if (opcode == ATHROW) return ; // do nothing, @see visitMax
prepareResultBy(opcode);
push(className);
push(methodName);
push(methodDesc);
loadThis();
invokeStatic(Probe.TYPE, Probe.EXIT);
}
private void prepareResultBy( int opcode) {
if (opcode == RETURN) { // void
push((Type) null );
} else if (opcode == ARETURN) { // object
dup();
} else {
if (opcode == LRETURN || opcode == DRETURN) { // long or double
dup2();
} else {
dup();
}
box(Type.getReturnType(methodDesc));
}
}
private final String className;
private final String methodName;
private final Label start;
private final Label end;
} |
更多參考請見這裡的 Demo , 它是javaagent, 在伴隨宿主程式啟動後, 提供MBean可用jconsole進行動態跟蹤的管理.
後續的方向
- 提供基於Web的遠端互動介面;
- 提供基於Shell的本地命令列介面;
- 提供Profile統計和趨勢輸出;
- 提供跟蹤日誌定位與分析.
參考
- The Java Interactive Profiler
- Proxy Javadoc
- Aspectj
- CGlib
- BTrace User’s Guide
- java動態跟蹤分析工具BTrace實現原理
- 構建自己的監測工具
- ASM Guide
- 常用 Java Profiling 工具的分析與比較
- AOP@Work: Performance monitoring with AspectJ
- The JavaTM Virtual Machine Specification
- 來自rednaxelafx的JVM分享, 他的 Blog
- BCEL