系統日志對於定位/排查問題的重要性不言而喻,相信許多開發和運維都深有體會。
通過日志追蹤代碼運行狀況,模擬系統執行情況,並迅速定位代碼/部署環境問題。
系統日志同樣也是數據統計/建模的重要依據,通過分析系統日志能窺探出許多隱晦的內容。
如系統的健壯性(服務並發訪問/數據庫交互/整體響應時間...)
某位用戶的喜好(分析用戶操作習慣,推送對口內容...)
當然系統開發者還不滿足於日志組件打印出來的日志,畢竟冗余且篇幅巨長。
so,對於關鍵的系統操作設計日志表,並在代碼中進行操作的記錄,配合 SQL 統計和搜索數據是件很愉快的事情。
本篇旨在總結在 Spring 下使用 AOP 注解方式進行日志記錄的過程,如果能對你有所啟發閣下不甚感激。
1. 依賴類庫
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>${aspectjweaver.version}</version> </dependency>
AspectJ 中的很多語法結構基本上已成為 AOP 領域的標准。
Spring 也有自己的 Spring-AOP,采用運行時生成代理類,底層可以選用 JDK 或者 CGLIB 動態代理。
通俗點,AspectJ 在編譯時增強要切入的類,而 Spring-AOP 是在運行時通過代理類增強切入的類,效率和性能可想而知。
Spring 在 2.0 的時候就已經開始支持 AspectJ ,現在到 4.X 的時代已經很完美的和 AspectJ 擁抱到了一起。
開啟掃描 AspectJ 注解的支持:
<!-- proxy-target-class等於true是強制使用cglib代理,proxy-target-class默認false,如果你的類實現了接口 就走JDK代理,如果沒有,走cglib代理 --> <!-- 注:對於單利模式建議使用cglib代理,雖然JDK動態代理比cglib代理速度快,但性能不如cglib --> <aop:aspectj-autoproxy proxy-target-class="true"/>
2. 定義切入點日志注解
目標操作日志表,其中設計了一些必要的字段,具體字段請拿捏具體項目場景,根據表結構設計注解如下。
@Inherited @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface OperationLog { String operationModular() default ""; String operationContent() default ""; }
上述我只做了兩個必要的參數,一個為操作的模塊,一個為具體的操作內容。
其實根據項目場景這里參數的設計可以非常豐富,不被其他程序員吐槽在此一舉。
3. 編寫處理日志切點類
@Pointcut("@annotation(com.rambo.spm.common.aop.OperationLog)") public void operationLogAspect() { }
類的構造函數上描述了該類要攔截的為 OperationLog 的注解方法, 同樣你也可以配置 XML 進行攔截。
切入點的姿勢有很多,不僅是正則同樣也支持組合表達式,強大的表達式能讓你精准的切入到任何你想要的地方。
更多詳情:http://blog.csdn.net/zhengchao1991/article/details/53391244
看到這里如果你對 Spring AOP 數據庫事務控制熟悉,其實 Spring AOP 記錄日志是相似的機制。
@Before("operationLogAspect()") public void doBefore(JoinPoint joinPoint) { logger.info("before aop:{}", joinPoint); //do something } @Around("operationLogAspect()") public Object doAround(ProceedingJoinPoint point) { logger.info("Around:{}", point); Object proceed = null; try { proceed = point.proceed(); //do somthing } catch (Throwable throwable) { throwable.printStackTrace(); logger.error("日志 aop 異常信息:{}", throwable.getMessage()); } return proceed; } @AfterThrowing("operationLogAspect()") public void doAfterThrowing(JoinPoint pjp) { logger.info("@After:{}", pjp); //do somthing } @After("operationLogAspect()") public void doAfter(JoinPoint pjp) { logger.info("@After:{}", pjp); } @AfterReturning("operationLogAspect()") public void doAfterReturning(JoinPoint point) { logger.info("@AfterReturning:{}", point); }
AspectJ 提供了幾種通知方法,通過在方法上注解這幾種通知,解析對應的方法入參,你就能洞悉切點的一切運行情況。
前置通知(@Before):在某連接點(join point)之前執行的通知,但這個通知不能阻止連接點前的執行(除非它拋出一個異常);
返回后通知(@AfterReturning):在某連接點(join point)正常完成后執行的通知:例如,一個方法沒有拋出任何異常,正常返回;
拋出異常后通知(@AfterThrowing):方法拋出異常退出時執行的通知;
后通知(@After):當某連接點退出的時候執行的通知(不論是正常返回還是異常退出);
環繞通知(@Around):包圍一個連接點(joinpoint)的通知,如方法調用;
通知方法中的值與構造函數一致,指定該通知對哪個切點有效,
上述 @Around 為最強大的一種通知類型,可以在方法調用前后完成自定義的行為,它可選擇是否繼續執行切點、直接返回、拋出異常來結束執行。
@Around 之所以如此強大是和它的入參有關,別的注解注解入參只容許 JoinPoint ,而 @Around 注解容許入參 ProceedingJoinPoint。
package org.aspectj.lang; import org.aspectj.runtime.internal.AroundClosure; public interface ProceedingJoinPoint extends JoinPoint { void set$AroundClosure(AroundClosure var1); Object proceed() throws Throwable; Object proceed(Object[] var1) throws Throwable; }
反編譯 ProceedingJoinPoint 你會恍然大悟,Proceedingjoinpoint 繼承了 JoinPoint 。
在 JoinPoint 的基礎上暴露出 proceed 這個方法。proceed 方法很重要,這是 aop 代理鏈執行的方法。
暴露出這個方法,就能支持 aop:around 這種切面(而其他的幾種切面只需要用到 JoinPoint,這跟切面類型有關), 能決定是否走代理鏈還是走自己攔截的其他邏輯。
如果項目沒有特定的需求,妥善使用 @Around 注解就能幫你解決一切問題。
@Around("operationLogAspect()") public Object doAround(ProceedingJoinPoint point) { logger.info("Around:{}", point); Object proceed = null; try { proceed = point.proceed(); Object pointTarget = point.getTarget(); Signature pointSignature = point.getSignature(); String targetName = pointTarget.getClass().getName(); String methodName = pointSignature.getName(); Method method = pointTarget.getClass().getMethod(pointSignature.getName(), ((MethodSignature) pointSignature).getParameterTypes()); OperationLog methodAnnotation = method.getAnnotation(OperationLog.class); String operationModular = methodAnnotation.operationModular(); String operationContent = methodAnnotation.operationContent(); OperationLogPO log = new OperationLogPO(); log.setOperUserid(SecureUtil.simpleUUID()); log.setOperUserip(HttpUtil.getClientIP(getHttpReq())); log.setOperModular(operationModular); log.setOperContent(operationContent); log.setOperClass(targetName); log.setOperMethod(methodName); log.setOperTime(new Date()); log.setOperResult("Y"); operationLogService.insert(log); } catch (Throwable throwable) { throwable.printStackTrace(); logger.error("日志 aop 異常信息:{}", throwable.getMessage()); } return proceed; }
別忘記將上面切點處理類/和要切入的類托管給 Spring,Aop 日志是不是很簡單,復雜的應該是 aspectj 內部實現機制,有機會要看看源碼哦。
處理切點類完整代碼:

@Aspect @Component public class OperationLogAspect { private static final Logger logger = LoggerFactory.getLogger(OperationLogAspect.class); //ProceedingJoinPoint 與 JoinPoint //注入Service用於把日志保存數據庫 //這里我用resource注解,一般用的是@Autowired,他們的區別如有時間我會在后面的博客中來寫 @Resource private OperationLogService operationLogService; //@Pointcut("execution (* com.rambo.spm.*.controller..*.*(..))") @Pointcut("@annotation(com.rambo.spm.common.aop.OperationLog)") public void operationLogAspect() { } @Before("operationLogAspect()") public void doBefore(JoinPoint joinPoint) { logger.info("before aop:{}", joinPoint); gePointMsg(joinPoint); } @Around("operationLogAspect()") public Object doAround(ProceedingJoinPoint point) { logger.info("Around:{}", point); Object proceed = null; try { proceed = point.proceed(); Object pointTarget = point.getTarget(); Signature pointSignature = point.getSignature(); String targetName = pointTarget.getClass().getName(); String methodName = pointSignature.getName(); Method method = pointTarget.getClass().getMethod(pointSignature.getName(), ((MethodSignature) pointSignature).getParameterTypes()); OperationLog methodAnnotation = method.getAnnotation(OperationLog.class); String operationModular = methodAnnotation.operationModular(); String operationContent = methodAnnotation.operationContent(); OperationLogPO log = new OperationLogPO(); log.setOperUserid(SecureUtil.simpleUUID()); log.setOperUserip(HttpUtil.getClientIP(getHttpReq())); log.setOperModular(operationModular); log.setOperContent(operationContent); log.setOperClass(targetName); log.setOperMethod(methodName); log.setOperTime(new Date()); log.setOperResult("Y"); operationLogService.insert(log); } catch (Throwable throwable) { throwable.printStackTrace(); logger.error("日志 aop 異常信息:{}", throwable.getMessage()); } return proceed; } @AfterThrowing("operationLogAspect()") public void doAfterThrowing(JoinPoint pjp) { logger.info("@AfterThrowing:{}", pjp); } @After("operationLogAspect()") public void doAfter(JoinPoint pjp) { logger.info("@After:{}", pjp); } @AfterReturning("operationLogAspect()") public void doAfterReturning(JoinPoint point) { logger.info("@AfterReturning:{}", point); } private void gePointMsg(JoinPoint joinPoint) { logger.info("切點所在位置:{}", joinPoint.toString()); logger.info("切點所在位置的簡短信息:{}", joinPoint.toShortString()); logger.info("切點所在位置的全部信息:{}", joinPoint.toLongString()); logger.info("切點AOP代理對象:{}", joinPoint.getThis()); logger.info("切點目標對象:{}", joinPoint.getTarget()); logger.info("切點被通知方法參數列表:{}", joinPoint.getArgs()); logger.info("切點簽名:{}", joinPoint.getSignature()); logger.info("切點方法所在類文件中位置:{}", joinPoint.getSourceLocation()); logger.info("切點類型:{}", joinPoint.getKind()); logger.info("切點靜態部分:{}", joinPoint.getStaticPart()); } private HttpServletRequest getHttpReq() { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; return servletRequestAttributes.getRequest(); } }
上述三步驟之后,你就可以在想記錄日志的方法上面添加注解來進行記錄操作日志,像下面這樣。
源碼托管地址:https://git.oschina.net/LanboEx/spmvc-mybatis.git 有這方面需求和興趣的可以檢出到本地跑一跑。