spring,真是一個(gè)好東西;性能,真是個(gè)讓人頭疼又不得不面對(duì)的問題。如何排查出項(xiàng)目中性能瓶頸?如何迅速定位系統(tǒng)的慢查詢?在這我就不說spring自帶的性能監(jiān)控器了,實(shí)在是有些簡(jiǎn)陋。下面就說說我自己寫的這個(gè)性能監(jiān)控器。先看看效果:
2013-07-07 19:19:50,440 WARN [main] [aop.PerformanceInterceptor]
|-144 ms; [HelloService.hellpAop]
|+---10 ms; [AnotherService.childMehhod]
|+---21 ms; [AnotherService.childMehhod3]
|+---+---8 ms; [HelloService.parentMehtod]
|+---12 ms; [AnotherService.childMehhod2]
其實(shí),利用spring AOP,任何人都可以寫一個(gè)定制的監(jiān)控,但大體思路是一樣的,就是在被調(diào)用方法的開始之前,記錄一下開始時(shí)間,在調(diào)用方法結(jié)束之后,記錄結(jié)束時(shí)間,然后,在調(diào)用棧退出前,將日志打印出來。光說不練假把式,下面一步一步構(gòu)建一個(gè)性能監(jiān)控器。
step1.構(gòu)造攔截器。
直接上代碼,攔截器核心代碼:
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
String name = extractLogName(invocation);
//記錄開始時(shí)間
start(name);
return invocation.proceed();
} finally {
//記錄方法結(jié)束時(shí)間
stop();
}
}
因?yàn)樽罱K要打印出來,因此,打印的名稱必須在記錄時(shí)間時(shí)把被調(diào)用方法的名稱也記錄下來。方法extractLogName就是干這個(gè)的。
step2.構(gòu)造數(shù)據(jù)結(jié)構(gòu)
首先,我們需要一個(gè)線程變量,存儲(chǔ)AOP攔截的每個(gè)方法的開始時(shí)間及結(jié)束時(shí)間。就用threadLocal變量了,本人還沒想到更好的方法。其次,調(diào)用過程其實(shí)是在一個(gè)方法棧(Stack)中旅行了一遍,被調(diào)用的方法總是后進(jìn)先出。每進(jìn)一個(gè)方法,都要記錄一個(gè)開始時(shí)間,每當(dāng)退出一個(gè)方法,都要記錄這個(gè)方法運(yùn)行的結(jié)束時(shí)間。最終,我們將得到一個(gè)類似樹形的結(jié)構(gòu)。我們需要定義這個(gè)結(jié)構(gòu),以便我們?cè)谕顺龇椒r(shí)能夠?qū)⒚恳粋€(gè)方法所耗費(fèi)的時(shí)間都打印出來。
/**
* 保存監(jiān)控信息變量
* @author donlianli@126.com
*/
private static class StackData {
/**
* 記錄根根節(jié)點(diǎn)
*/
public StackEntry root;
/**
* 當(dāng)前正在調(diào)用方法節(jié)點(diǎn)
*/
public StackEntry currentEntry;
/**
* 堆棧樹高度
*/
public int level;
}
/**
* aop方法性能統(tǒng)計(jì)實(shí)體
* @author donlianli@126.com
*/
private static class StackEntry {
public String logName ;
public long beginTime;
public long endTime;
/**
* 節(jié)點(diǎn)所處高度
*/
public int level;
/**
* 調(diào)用的子方法
*/
public List<StackEntry> child;
/**
* 上級(jí)節(jié)點(diǎn)
*/
public StackEntry parent ;
public StackEntry(String logName, long currentTimeMillis) {
this.logName = logName;
this.beginTime = currentTimeMillis;
this.child = new ArrayList<StackEntry>(3);
}
}
好了,結(jié)構(gòu)和攔截器都寫好了。只需兩步,大工基本就告成了,在攔截器中,在調(diào)用方法的前面及后面,記錄一個(gè)StackEntry對(duì)象就可以了。start和stop的代碼如下: StackData定義了根節(jié)點(diǎn)的結(jié)構(gòu),StackEntry存儲(chǔ)每個(gè)方法的開始結(jié)束時(shí)間,另外在StackData和StackEntry加入level字段,方便后面打印日志。StackData和StackEntry都是作為一個(gè)內(nèi)部類引入的,因?yàn)檫@兩個(gè)類為了性能,都沒有提供一些封裝方法,不宜暴露出去(出去多丟人�。�
public static void start(String logName) {
StackData data = dataHolder.get();
StackEntry currentEntry = new StackEntry(logName, System.currentTimeMillis());
if (data == null) {
data = new StackData();
data.root = currentEntry;
data.level = 1;
dataHolder.set(data);
} else {
StackEntry parent = data.currentEntry;
currentEntry.parent=parent;
parent.child.add(currentEntry);
}
data.currentEntry = currentEntry;
currentEntry.level=data.level;
data.level++;
}
public static void stop() {
StackData data = dataHolder.get();
StackEntry self = data.currentEntry;
self.endTime = System.currentTimeMillis();
data.currentEntry = self.parent;
data.level--;
printStack(data);
}
/**
* 此處還可以進(jìn)行改進(jìn),可以將超時(shí)的數(shù)據(jù)放入一個(gè)有界隊(duì)列
* 里,在另一個(gè)線程進(jìn)行打印。
* @param data
*/
private static void printStack(StackData data) {
if(logger.isWarnEnabled()){
StringBuilder sb = new StringBuilder("\r\n");
StackEntry root = data.root;
appendNode(root,sb);
logger.warn(sb.toString());
}
}
private static void appendNode(StackEntry entry, StringBuilder sb) {
long totalTime = entry.endTime-entry.beginTime ;
if(entry.level ==1){
sb.append("|-");
}
sb.append(totalTime);
sb.append(" ms; [");
sb.append(entry.logName);
sb.append("]");
for(StackEntry cnode : entry.child){
sb.append("\r\n|");
for(int i=0,l=entry.level;i<l;i++){
sb.append("+---");
}
appendNode(cnode,sb);
}
}
1、我們只想找出慢查詢,而不想把所有的方法的運(yùn)行時(shí)間都打印出來等等,還有需求?
2、希望有一個(gè)開關(guān),平常不需要監(jiān)控,在出現(xiàn)問題的時(shí)候,才把這個(gè)監(jiān)控打開。
好吧,程序員都是被這些需求給搞死的。
在攔截器中增加一個(gè)開關(guān)switchOn和一個(gè)閾值threshold,當(dāng)switchOn==true的時(shí)候,才進(jìn)行監(jiān)控,否則不監(jiān)控。在監(jiān)控時(shí),如果整個(gè)方法的運(yùn)行時(shí)間小于threshold,不打印日志,因?yàn)榇蛴∪罩緯?huì)IO,會(huì)給方法增加額外的開銷。改進(jìn)后代碼如下:
/**
* 性能監(jiān)控開關(guān)
* 可以在運(yùn)行時(shí)動(dòng)態(tài)設(shè)置開關(guān)
*/
private volatile boolean switchOn = true;
/**
* 方法執(zhí)行閾值
*/
private volatile int threshold = 100;
public Object invoke(MethodInvocation invocation) throws Throwable {
if(switchOn){
String name = extractLogName(invocation);
try {
start(name);
return invocation.proceed();
} finally {
stop(threshold);
}
}
else {
return invocation.proceed();
}
}
? 打印日志閾值:
/**
* @param threshold 打印日志的閾值
*/
public static void stop(int threshold) {
StackData data = dataHolder.get();
StackEntry self = data.currentEntry;
self.endTime = System.currentTimeMillis();
data.currentEntry = self.parent;
data.level--;
if(data.root == self && (self.endTime -self.beginTime) > threshold){
printStack(data);
}
}
但這個(gè)監(jiān)控器是運(yùn)行在spring AOP上面的,并且,監(jiān)控的方法必須都是通過interface調(diào)用的。所以,如果你要使用這個(gè)方法,還要確保你是使用的面向接口的編程。不過,如果你的項(xiàng)目沒有使用面向接口,可以利用eclipse自帶的工具,將公用方法Extract成interface。到此,這個(gè)性能監(jiān)控器幾乎算完美了。
spring怎么配置?攔截器怎么配置?你不會(huì)連這個(gè)都不會(huì)吧,那你搜索一下吧。