斷點(diǎn) vs 日志
斷點(diǎn)是我們?nèi)粘i_發(fā)最為常見和高效的調(diào)試手段, 相比較輸入日志它給予更多的狀態(tài)信息和靈活的觀察角度, 但斷點(diǎn)調(diào)試是有前提和局限的.
需要一個(gè)界面友好, 功能強(qiáng)大的IDE,
比較適合于在單機(jī)的開發(fā)環(huán)境中進(jìn)行.
企業(yè)應(yīng)用開發(fā)中, 我們常常會(huì)遇到無法斷點(diǎn)調(diào)試的窘境, 例如:
這個(gè)異常僅在生產(chǎn)環(huán)境出現(xiàn), 開發(fā)環(huán)境里無法重現(xiàn);
存在外部系統(tǒng)依賴, 開發(fā)環(huán)境無法模擬等.
這迫使我們不得不回到日志調(diào)試的老路子上來.
Print vs Logging
簡單點(diǎn)的話, 我們用
System.out.println("debug infomation");
就是因?yàn)檫^于簡單, 當(dāng)需要更多信息(如線程, 時(shí)間等), 或是定義輸出模式和形式就需要編寫更多代碼, 于是我們有了Log4j.
為什么要基于AOP
Log4j挺好用的, 只是與System.out.print一樣, 在代碼中隨處可見, 但卻沒有業(yè)務(wù)價(jià)值.
更令人頭痛的是, 并非每次我們都有足夠的經(jīng)驗(yàn)告訴自己應(yīng)該在哪里添加這些語句, 以致于調(diào)試中不斷的因?yàn)檎{(diào)正它們的在代碼中的位置, 而反復(fù)編譯 – 打包 – 發(fā)布系統(tǒng). 這種體力活, 太沒藝術(shù)感了, 囧!
換而言之, 我們會(huì)希望:
將Logging剝離于業(yè)務(wù)之外, 讓代碼更易于維護(hù),
無需重新編譯,甚至能夠運(yùn)行時(shí), 可調(diào)整輸出日志的位置.
AOP完全可以幫助我們做到上述兩點(diǎn).
這完全不是什么新鮮觀點(diǎn), 這在任何介紹AOP文章中, 都會(huì)提到Logging是其最典型的應(yīng)用場景.
所以這兒將基于Guice, 討論如何實(shí)現(xiàn)一個(gè)非侵入式的, 且能無需重新編譯即可調(diào)正Logging位置的簡單示例.
一個(gè)基于Guice的示例
我曾經(jīng)用過一個(gè)叫Log4E的Eclipse插件, 它可根據(jù)我們預(yù)先的配置, 自動(dòng)的為我們?cè)诰帉懙拇a中插入logging的語句, 如方法調(diào)用的進(jìn)口和出口:
public int sum(int a, int b){ if (logger.isDebugEnabled()){ logger.debug("sum - start : a is " + a + ", b is " + b); } int result = a + b; if (logger.isDebugEnabled()){ logger.debug("sum - end : return is " + result); }}
從上例不難發(fā)現(xiàn), 我們?cè)谡{(diào)試過程中, 往往會(huì)通過一個(gè)方法的進(jìn)入或退出的狀態(tài)(參數(shù), 返回值或異常)來分析問題出在什么地方. 那么, 借助MethodInterceptor我們可以這樣做:
Logging
public class LoggingInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { try { Object result = invocation.proceed(); // logging 方法, 參數(shù)與返回值 log(invocation.getMethod(), invocation.getArguments(), result); return result; } catch (Throwable throwable) { // logging 方法, 參數(shù)與異常 error(invocation.getMethod(), invocation.getArguments(), throwable); throw throwable; } }}
接下來, 我們需要配置這個(gè) , 并向Guice聲明它.
public class LoggingModule extends AbstractModule { @Override public void configure() { bindInterceptor(Matchers.any(), Matchers.any(), new LoggingInterceptor()); }}public class Main { public static void main(String[] args) { Injector injector = Guice.createInjector(new BusinessModule(), new LoggingModule()); }}
很簡單, 不是嗎? 這樣我們的業(yè)務(wù)模塊的代碼完全不用編寫輸出日志的代碼, 只需要在創(chuàng)建Injector的時(shí)候加入LoggingModule就可以了.
等等, 好像忘了去實(shí)現(xiàn)如何配置日志輸出的位置. 好吧, 這個(gè)其實(shí)很簡單:
配置Logging位置
bindInterceptor(Matchers.any(), Matchers.any(), new LoggingInterceptor());
bindInterceptor方法的第一個(gè)參數(shù)定義了 將匹配所有類, 第二個(gè)參數(shù)定義了 將匹配一個(gè)類所有方法. 那么, 我們要做的僅僅是通過外部參數(shù)調(diào)整這兩個(gè)參數(shù)就可以啦. 這兒就演示一個(gè)用正則表達(dá)式匹配要Logging的方法的例子:
public class MethodRegexMatcher extends AbstractMatcher<Method> { private final Pattern pattern = Pattern.compile(System.getProperty("logging.method.regex", "*")); @Override public boolean matches(Method method) { return pattern.matcher(method.getName()).matches(); }}
可惜這種方法不能在運(yùn)行時(shí)調(diào)整, 但這也是可以實(shí)現(xiàn)的.
運(yùn)行時(shí)配置Logging位置
還是以用正則表達(dá)式匹配要Logging的方法為例:
public class LoggingInterceptor implements MethodInterceptor { private String regex = "*"; public void setMethodRegex(String regex){ this.regex = regex; } @Override public Object invoke(MethodInvocation invocation) throws Throwable { String methodName = invocation.getMethod().getName(); try { Object result = invocation.proceed(); if (methodName.matches(regex)) // logging 方法, 參數(shù)與返回值 log(invocation.getMethod(), invocation.getArguments(), result); return result; } catch (Throwable throwable) { if (methodName.matches(regex)) // logging 方法, 參數(shù)與異常 error(invocation.getMethod(), invocation.getArguments(), throwable); throw throwable; } }}
而后可借助JMX動(dòng)態(tài)調(diào)整regex的值, 來實(shí)現(xiàn)運(yùn)行時(shí)的配置. 當(dāng)然, 肯定還會(huì)有其它更好的方法, 如果你知道了不妨分享一下.
小結(jié)
本文僅以Guice為例討論如何改進(jìn)我們?nèi)粘i_發(fā)中調(diào)試的問題, 其實(shí)這在Spring應(yīng)用也同樣能夠?qū)崿F(xiàn)的, 甚至其它應(yīng)用AOP的場景都是可行的.
拓展開來, 不僅是Logging, 說不定驗(yàn)證(測試)也是可行的呢!
有句話不是這樣說的嗎, “思想有多遠(yuǎn), 我們就能走多遠(yuǎn)!”