來一手 AOP 註解方式進行日誌記錄

   系統日誌對於定位/排查問題的重要性不言而喻,相信許多開發和運維都深有體會。git

   經過日誌追蹤代碼運行情況,模擬系統執行狀況,並迅速定位代碼/部署環境問題。程序員

   系統日誌一樣也是數據統計/建模的重要依據,經過分析系統日誌能窺探出許多隱晦的內容。數據庫

   如系統的健壯性(服務併發訪問/數據庫交互/總體響應時間...)mybatis

   某位用戶的喜愛(分析用戶操做習慣,推送對口內容...)併發

   固然系統開發者還不知足於日誌組件打印出來的日誌,畢竟冗餘且篇幅巨長。mvc

   so,對於關鍵的系統操做設計日誌表,並在代碼中進行操做的記錄,配合 SQL 統計和搜索數據是件很愉快的事情。運維

   本篇旨在總結在 Spring 下使用 AOP 註解方式進行日誌記錄的過程,若是能對你有所啓發閣下不甚感激。ide

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();
    }
}
View Code

   上述三步驟以後,你就能夠在想記錄日誌的方法上面添加註解來進行記錄操做日誌,像下面這樣。

    源碼託管地址:https://git.oschina.net/LanboEx/spmvc-mybatis.git  有這方面需求和興趣的能夠檢出到本地跑一跑。

相關文章
相關標籤/搜索