基於Spring的AspectJ操作日誌實現

前言

  • 前段日子項目部分方法訪問需要記錄相對應的用戶和前後變化的值,爲了減免代碼的重複性,提高代碼的可複用性。特定採用面向切面的註解形式來實現操作日誌記錄。

AOP簡單概念介紹

這裏寫圖片描述

常見術語

  • 切面(Aspect): 一個關注點的模塊化,比如日誌、事務,這個關注點可能會橫切多個對象,一個類,裏面有各種通知方法
  • 連接點(JoinPoint) 程序執行過程中的某個特定的切入點,比如service層login()方法被調用執行時,login()方法就是一個連接點
  • 通知(Advice) 在切面的某個特定的連接點上執行的動作,有前置通知、後置通知等等,是一個具體方法
  • 切入點(Pointcut) 匹配連接點的斷言,在AOP中通知和一個切入點表達式關聯,就是一個表達式(在Aspectj註解中是一個空方法),把連接點和通知聯通起來
  • 引入(Introduction) 在不修改類代碼的前提下,爲類添加新的方法和屬性
  • 目標對象(Target Object) 被一個類或者多個切面所通知的對象,業務對象,比如LoginServiceImpl對象
  • AOP代理(AOP Proxy) AOP框架創建的對象,用來實現切面契約(包括通知方法執行等功能),AOP並不直接使用目標對象,而通過動態代理生產目標對象的代理對象
  • 織入(Weaving) 把切面連接到其它的應用程序或者對象上,並創建一個被通知的對象,分爲:編譯時織入/類加載時織入/執行時織入,生成代理對象的過程

五種通知類型

  • Before
    在方法被調用之前調用
  • After
    在方法完成後調用通知,無論方法是否執行成功
  • After-returning
    在方法成功執行之後調用通知
  • After-throwing
    在方法拋出異常後調用通知
  • Around
    通知了好、包含了被通知的方法,在被通知的方法調用之前後調用之後執行自定義的行爲

引入相關jar包

<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>4.3.5.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>4.3.5.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>4.3.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.8.2</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.2</version>
        </dependency>
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.1_3</version>
        </dependency>

配置文件配置

<!-- @描述:proxy-target-class爲true選擇CGLib來生成AOP代理類,無或默認用 JDK 動態代理。即使你未聲明 proxy-target-class="true" ,但運行類沒有繼承接口,spring也會自動使用CGLIB代理。 高版本spring自動根據運行類選擇 JDK 或 CGLIB 代理
JDK動態代理是模擬接口實現的方式,cglib是模擬子類繼承的方式,一般採用前者,因爲前者效率高。後者不建議使用,除非非用不可-->
    <aop:aspectj-autoproxy />

構造註解接口

package com.demo.base.aop;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD})
public @interface opLog {

    /**
     * 要執行的操作碼比如:add操作
     **/
    int opCode() default 0;

    /**
     * 要執行的具體操作比如:添加用戶
     **/
    String opName() default "";

    /**
     * 更改前備註
     **/
    String beforeRemark() default "";

    /**
     * 更改後備注
     **/
    String afterRemark() default "";

}

實現操作日誌處理邏輯類

package com.demo.base.aop;


import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


/**
 * <p>Description:記錄操作日誌</p>
 * <p>Author:Gred</p>
 * <p>Date:2017/9/19 19:28</p>
 * <p>version:1.0</p>
 **/
@Aspect
@Component
public class AdminLogAop {

    @Autowired
    private HttpServletRequest request;

    //@描述:方法前處理邏輯
    @Before("@annotation(opLog)")
    public void logInfo() {
        //記錄開始時間
        request.setAttribute("startTime", System.currentTimeMillis());
    }

    //@時間:2017/9/19 20:39
    //@描述:方法後處理邏輯
    @After("@annotation(opLog)")
    public void logAfter(){
        long entTime = System.currentTimeMillis();
        Long startTime = (Long) request.getAttribute("startTime");
        System.out.println("方法耗時:"+(entTime-startTime)+"ms");
    }

    //@描述:方法環繞處理邏輯
    @Around("@annotation(opLog)")
    public Object logInfoAround(ProceedingJoinPoint pjp) throws Throwable {

        //執行方法,獲取方法的返回值獲取方法返回值
        Object object =  pjp.proceed();
        //根據返回值,判斷判斷該方法是否有執行成功
        //獲取操作碼和操作說明
        Map<String, Object> map = getLogParameter(pjp, request);
        //進行操作日誌插入記錄處理
        System.out.println("插入成功");

        return object;
    }

    /**
     * <p>Description:獲取註解中對方法的描述信息 用於Controler層註解</p>
     * <p>Author:Gred</p>
     * <p>Date:2017/9/19 19:57</p>
     * <p>param:joinPoint</p>
     **/
    public static Map<String, Object> getLogParameter(ProceedingJoinPoint pjp, HttpServletRequest request)
            throws Exception {

        Map<String, Object> map = new HashMap<String, Object>();
        String targetName = pjp.getTarget().getClass().getName();//獲取目標類的名稱
        String methodName = pjp.getSignature().getName();//獲取目標方法名
        Object[] arguments = pjp.getArgs();//返回目標方法的參數
        Class targetClass = Class.forName(targetName);//加載目標類
        Method[] methods = targetClass.getMethods();//獲取該目標類的所有方法

        String opName = "";
        String beforeRemark = "";
        String afterRemark = "";
        int opCode = 0;
        //獲取註解中參數值【下一步需要根據裏面的值來當做參數名 從封裝好的map裏獲取值】
        for (Method method : methods) {
            if (method.getName().equals(methodName)) {//判斷方法名是否一致
                Class[] clazzs = method.getParameterTypes();
                if (clazzs.length == arguments.length) {//判斷參數個數是否一致
                    opName = method.getAnnotation(opLog.class).opName();
                    opCode = method.getAnnotation(opLog.class).opCode();
                    beforeRemark = method.getAnnotation(opLog.class).beforeRemark();
                    afterRemark = method.getAnnotation(opLog.class).afterRemark();
                    break;
                }
            }
        }

        //將隱射成RequestParam的參數封裝一個map=
        Map<String, Object> paramMap = changMap(pjp);
        //根據從註解接口中獲取的參數名,在從map獲取我們需要的值
        beforeRemark = paramMap.get(beforeRemark) == null ? "" : String.valueOf(paramMap.get(beforeRemark));
        afterRemark = paramMap.get(afterRemark) == null ? "" : String.valueOf(paramMap.get(afterRemark));

        map.put("opCode", opCode);
        map.put("opName", opName);
        map.put("beforeRemark", beforeRemark);
        map.put("afterRemark", afterRemark);

        return map;
    }

    //@描述:從切點中中獲取已經映射好的參數,因爲不確定request的類型,所以只能夠通過映射封裝好
    //的參數來獲取參數值,參數一定要用方法@RequestParam接收
    private static Map<String, Object> changMap(ProceedingJoinPoint pjp) {
        Signature signature = pjp.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        String[] strings = methodSignature.getParameterNames();//獲取該方法所有參數
        Object[] args = pjp.getArgs();
        List<String> list = Arrays.asList(strings);
        Map<String, Object> map = new HashMap<String, Object>();
        for (int i = 0; i < args.length; i++) {
            map.put(list.get(i), args[i]);
        }

        return map;
    }


}

使用方法

@RequestMapping("/index.page")
    public String test(){
        return "/demo/index";
    }

    @opLog(opCode = 1,opName = "test",beforeRemark = "beforeRemark",afterRemark = "afterRemark")
    @RequestMapping("/testAop.page")
    public String testAop(@RequestParam String beforeRemark,
                          @RequestParam String afterRemark,
                          @RequestParam String test,
                          Model model){

        model.addAttribute("test",test);
        return "/demo/success";
    }

結果

獲取到參數:{opName=test, afterRemark=test, beforeRemark=test, opCode=1}
插入成功
方法耗時:0ms

感謝以下博客作者的文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章