SpringBoot中搭配AOP實現自定義註解

2022-12-08 18:01:05

1 springBoot的依賴

確定專案中包含可以註解的依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2 自定義註解的步驟

在專案中自定義註解的步驟主要有兩步,第一步:定義註解類,第二步:定義切面

2.1 定義註解類

直接建立 @interface 的類,使用註解@Target@Retention指定其適用範圍及保留時長,如下:


@Target(ElementType.METHOD) // 指定註解的適用範圍
@Retention(RetentionPolicy.RUNTIME) //指定執行時
public @interface ApiLog {

    String desc() default "";

    boolean timeSpan() default true;


}

​ 註解類的內容一般很簡單,類似於Enum類一樣,裡面是簡單的方法及屬性

2.2 定義切面

通過@Aspect註解指定一個類,該類必須實現

@Component
@Aspect
@Slf4j(topic = "ApiLogNote")
public class ElasticSearchExecuteLog {

    @Around("@annotation(com.gcc.ApiLog)")
    public Object aroundAdvice(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        //獲取被呼叫方法
        Method method = signature.getMethod();
        //取出被呼叫方法的註解,方便後續使用註解中的屬性
        ApiLog loglinstener = method.getAnnotation(ApiLog.class);
        log.info("----------------------method[{}]start--------------------",method.getName());
        log.info("方法描述:{}",loglinstener.desc());
        log.info("引數 :{}",point.getArgs());
        long startTime = System.currentTimeMillis();
        Object proceed = point.proceed();
        long endTime = System.currentTimeMillis();
        log.info("耗時:{}ss",endTime-startTime);
        log.info("----------------------method[{}] end--------------------\n",method.getName())
        return proceed;
    }
}

2.3 使用註解

因為此例子使用的型別為METHOD即方法級的註解,直接在方法上使用即可:

    @ApiLog
    public JSONObject seachEsData(String indexName, SearchSourceBuilder searchSourceBuilder) {
        JSONObject resultMap = new JSONObject();
        .......
        return resultMap;
    }

3 知識點補充

3.1 關於Target註解補充

註解@Target常常配合列舉類ElementType來指定註解的作用位置,也叫合法位置,即你定義了一個註解,這個註解是類註解還是方法註解還是XX註解等,具體作用的範圍,取決於@Target({ElementType.TYPE})中,ElementType的列舉值,在進行自定義列舉時,根據自己的需求,決定定義的註解是哪類層級使用的註解,例如上面的例子中,@ApiLog這個自定義的註解就是方法級的註解

ElementType的列舉值有

列舉值 含義
TYPE 類, 介面 (包括註解型別), 或 列舉 宣告
FIELD 欄位、包括列舉常數
METHOD 方法宣告
PARAMETER 正式的引數宣告
CONSTRUCTOR 建構函式的宣告
LOCAL_VARIABLE 區域性變數的宣告
ANNOTATION_TYPE 註解型別的宣告
PACKAGE 包宣告

3.2 關於Retention註解補充

註解@Retention常常配合列舉類RetentionPolic來指定註解的各種策略,註解的保留時間,也就是何時生效,即你定義了一個註解,這個註解是編譯時生效還是僅僅只是在程式碼中標記等等,具體作用的範圍,取決於@Retention({RetentionPolic.TYPE})中,RetentionPolic的列舉值,在進行自定義列舉時,大多數都是使用RUNTIME(編譯時生效)

RetentionPolic的列舉值

列舉值 含義
SOURCE 解只在原始碼級別保留,編譯時被忽略
CLASS 註解將被編譯器在類檔案中記錄 , 但在執行時不需要JVM保留。這是預設的
RUNTIME 註解將被編譯器記錄在類檔案中,在執行時保留VM,也是使用最多的(一般自定義均使用這個)

3.3 關於AOP的一些概念補充

這種在執行時,動態地將程式碼切入到類的指定方法、指定位置上的程式設計思想就是面向切面的程式設計

切面(Aspect)

切面是一個橫切關注點的模組化,一個切面能夠包含同一個型別的不同增強方法,比如說事務處理和紀錄檔處理可以理解為兩個切面。切面由切入點和通知組成,它既包含了橫切邏輯的定義,也包括了切入點的定義。 Spring AOP就是負責實施切面的框架,它將切面所定義的橫切邏輯織入到切面所指定的連線點中。簡單點理解,在SpringBoot中使用了Aspect註解的類就是切面

@Component
@Aspect
public class LogAspect {
}

目標物件(target)

目標物件指將要被增強的物件,即包含主業務邏輯的類物件。或者說是被一個或者多個切面所通知的物件。

在我們的例子中,即是使用了@ApiLog註解的地方

連線點(JoinPoint)

程式執行過程中明確的點,如方法的呼叫或特定的異常被丟擲。連線點由兩個資訊確定:

  • 方法(表示程式執行點,即在哪個目標方法)
  • 相對點(表示方位,即目標方法的什麼位置,比如呼叫前,後等)

簡單來說,連線點就是被攔截到的程式執行點,因為Spring只支援方法型別的連線點,所以在Spring中連線點就是被攔截到的方法。

切入點(PointCut)

切入點是對連線點進行攔截的條件定義。切入點表示式如何和連線點匹配是AOP的核心,Spring預設使用AspectJ切入點語法。 一般認為,所有的方法都可以認為是連線點,但是我們並不希望在所有的方法上都新增通知,而切入點的作用就是提供一組規則(使用 AspectJ pointcut expression language 來描述) 來匹配連線點,給滿足規則的連線點新增通知。

//此處的匹配規則是 com.remcarpediem.test.aop.service包下的所有類的所有函數。
@Pointcut("execution(* com.remcarpediem.test.aop.service..*(..))")
public void pointcut() {
}

這裡切入點的概念其實就是確定對哪些目標物件進行切面插入功能,一開始的例子是採用註解的方式來達到切入**點的作用

 @Around("@annotation(com.gcc.ApiLog)")

通知(Advice)

通知是指攔截到連線點之後要執行的程式碼,包括了「around」、「before」和「after」等不同型別的通知。Spring AOP框架以攔截器來實現通知模型,並維護一個以連線點為中心的攔截器鏈。

// @Before說明這是一個前置通知,log函數中是要前置執行的程式碼,JoinPoint是連線點,
@Before("pointcut()")
public void log(JoinPoint joinPoint) { 
}

//@After 為後置通知

//@Around 為環繞通知

織入(Weaving)

這裡的織入概念是個動作,即Spring將前面的切面、連線點、切入點關聯起來並建立通知代理的過程。織入可以在編譯時,類載入時和執行時完成。在編譯時進行織入就是靜態代理,而在執行時進行織入則是動態代理。

增強器(Adviser)

Advisor是切面的另外一種實現,能夠將通知以更為複雜的方式織入到目標物件中,是將通知包裝為更復雜切面的裝配器。Advisor由切入點和Advice組成。 Advisor這個概念來自於Spring對AOP的支撐,在AspectJ中是沒有等價的概念的。Advisor就像是一個小的自包含的切面,這個切面只有一個通知。切面自身通過一個Bean表示,並且必須實現一個預設介面。

簡單來講,整個 aspect 可以描述為: 滿足 pointcut 規則的 joinpoint 會被新增相應的 advice 操作

將上方通過註解使用切面的方式改寫一下:

@Component
@Aspect
@Sl4fj
public class ElasticSearchExecuteLog {
 
  // 不使用註解,而通過基礎的規則設定選擇切入點,表示式是指com.gcc.controller
 // 包下的所有類的所有方法
 @Pointcut("execution(* com.gcc.controller..*(..))")
 public void aspect() {}
 
  // 通知,在符合aspect切入點的方法前插入如下程式碼,並且將連線點作為引數傳遞
 @Before("aspect()")
 public void log(JoinPoint joinPoint) { //連線點作為引數傳入
   // 獲得類名,方法名,引數和引數名稱。
   Signature signature = joinPoint.getSignature();
   String className = joinPoint.getTarget().getClass().getName();
   String methodName = joinPoint.getSignature().getName();
   Object[] arguments = joinPoint.getArgs();
   MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
   String[] argumentNames = methodSignature.getParameterNames();
   StringBuilder sb = new StringBuilder(className + "." + methodName + "(");
   	for (int i = 0; i< arguments.length; i++) {
                  Object argument = arguments[i];
                  sb.append(argumentNames[i] + "->");
                  sb.append(argument != null ? argument.toString() : "null ");
   	}
   sb.append(")");
   log.info(sb.toString());
  }
}

3.4 關於AOP中一些類及函數的使用

JoinPoint物件

JoinPoint物件封裝了SpringAop中切面方法的資訊,在切面方法中新增JoinPoint引數,就可以獲取到封裝了該方法資訊的JoinPoint物件.

方法 作用 返回物件
getSignature() 獲取封裝了署名資訊的物件,在該物件中可以獲取到目標方法名,所屬類的Class等資訊 Signature
getArgs() 獲取 獲取傳入目標方法的引數物件 Object[]
getTarget() 獲取被代理的物件 Object

ProceedingJoinPoint物件

proceedingJoinPoin物件是JoinPoint的子類,在原本JoinPoint的基礎上,放開了Proceeed()的使用,一般在環繞通知@Around時使用:

Object proceed() throws Throwable //執行目標方法
Object proceed(Object[] var1) throws Throwable //傳入的新的引數去執行目標方法