來一手 AOP 注解方式進行日志記錄


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

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

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

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

   某位用戶的喜好(分析用戶操作習慣,推送對口內容...)

   當然系統開發者還不滿足於日志組件打印出來的日志,畢竟冗余且篇幅巨長。

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

   上述三步驟之后,你就可以在想記錄日志的方法上面添加注解來進行記錄操作日志,像下面這樣。

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

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM