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