系統日誌對於定位/排查問題的重要性不言而喻,相信許多開發和運維都深有體會。git
經過日誌追蹤代碼運行情況,模擬系統執行狀況,並迅速定位代碼/部署環境問題。程序員
系統日誌一樣也是數據統計/建模的重要依據,經過分析系統日誌能窺探出許多隱晦的內容。數據庫
如系統的健壯性(服務併發訪問/數據庫交互/總體響應時間...)mybatis
某位用戶的喜愛(分析用戶操做習慣,推送對口內容...)併發
固然系統開發者還不知足於日誌組件打印出來的日誌,畢竟冗餘且篇幅巨長。mvc
so,對於關鍵的系統操做設計日誌表,並在代碼中進行操做的記錄,配合 SQL 統計和搜索數據是件很愉快的事情。運維
本篇旨在總結在 Spring 下使用 AOP 註解方式進行日誌記錄的過程,若是能對你有所啓發閣下不甚感激。ide
<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"/>
目標操做日誌表,其中設計了一些必要的字段,具體字段請拿捏具體項目場景,根據表結構設計註解以下。
@Inherited @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface OperationLog { String operationModular() default ""; String operationContent() default ""; }
上述我只作了兩個必要的參數,一個爲操做的模塊,一個爲具體的操做內容。
其實根據項目場景這裏參數的設計能夠很是豐富,不被其餘程序員吐槽在此一舉。
@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 有這方面需求和興趣的能夠檢出到本地跑一跑。