確定專案中包含可以註解的依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
在專案中自定義註解的步驟主要有兩步,第一步:定義註解類,第二步:定義切面
直接建立 @interface
的類,使用註解@Target
和 @Retention
指定其適用範圍及保留時長,如下:
@Target(ElementType.METHOD) // 指定註解的適用範圍
@Retention(RetentionPolicy.RUNTIME) //指定執行時
public @interface ApiLog {
String desc() default "";
boolean timeSpan() default true;
}
註解類的內容一般很簡單,類似於Enum類一樣,裡面是簡單的方法及屬性
通過@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;
}
}
因為此例子使用的型別為METHOD
即方法級的註解,直接在方法上使用即可:
@ApiLog
public JSONObject seachEsData(String indexName, SearchSourceBuilder searchSourceBuilder) {
JSONObject resultMap = new JSONObject();
.......
return resultMap;
}
註解@Target
常常配合列舉類ElementType
來指定註解的作用位置,也叫合法位置,即你定義了一個註解,這個註解是類註解還是方法註解還是XX註解等,具體作用的範圍,取決於@Target({ElementType.TYPE})
中,ElementType
的列舉值,在進行自定義列舉時,根據自己的需求,決定定義的註解是哪類層級使用的註解,例如上面的例子中,@ApiLog這個自定義的註解就是方法級的註解
ElementType的列舉值有
列舉值 | 含義 |
---|---|
TYPE | 類, 介面 (包括註解型別), 或 列舉 宣告 |
FIELD | 欄位、包括列舉常數 |
METHOD | 方法宣告 |
PARAMETER | 正式的引數宣告 |
CONSTRUCTOR | 建構函式的宣告 |
LOCAL_VARIABLE | 區域性變數的宣告 |
ANNOTATION_TYPE | 註解型別的宣告 |
PACKAGE | 包宣告 |
註解@Retention
常常配合列舉類RetentionPolic
來指定註解的各種策略,註解的保留時間,也就是何時生效,即你定義了一個註解,這個註解是編譯時生效還是僅僅只是在程式碼中標記等等,具體作用的範圍,取決於@Retention({RetentionPolic.TYPE})
中,RetentionPolic
的列舉值,在進行自定義列舉時,大多數都是使用RUNTIME(編譯時生效)
RetentionPolic的列舉值
列舉值 | 含義 |
---|---|
SOURCE | 解只在原始碼級別保留,編譯時被忽略 |
CLASS | 註解將被編譯器在類檔案中記錄 , 但在執行時不需要JVM保留。這是預設的 |
RUNTIME | 註解將被編譯器記錄在類檔案中,在執行時保留VM,也是使用最多的(一般自定義均使用這個) |
這種在執行時,動態地將程式碼切入到類的指定方法、指定位置上的程式設計思想就是面向切面的程式設計
切面是一個橫切關注點的模組化,一個切面能夠包含同一個型別的不同增強方法,比如說事務處理和紀錄檔處理可以理解為兩個切面。切面由切入點和通知組成,它既包含了橫切邏輯的定義,也包括了切入點的定義。 Spring AOP就是負責實施切面的框架,它將切面所定義的橫切邏輯織入到切面所指定的連線點中。簡單點理解,在SpringBoot中使用了Aspect註解的類就是切面
@Component
@Aspect
public class LogAspect {
}
目標物件指將要被增強的物件,即包含主業務邏輯的類物件。或者說是被一個或者多個切面所通知的物件。
在我們的例子中,即是使用了@ApiLog註解的地方
程式執行過程中明確的點,如方法的呼叫或特定的異常被丟擲。連線點由兩個資訊確定:
簡單來說,連線點就是被攔截到的程式執行點,因為Spring只支援方法型別的連線點,所以在Spring中連線點就是被攔截到的方法。
切入點是對連線點進行攔截的條件定義。切入點表示式如何和連線點匹配是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)")
通知是指攔截到連線點之後要執行的程式碼,包括了「around」、「before」和「after」等不同型別的通知。Spring AOP框架以攔截器來實現通知模型,並維護一個以連線點為中心的攔截器鏈。
// @Before說明這是一個前置通知,log函數中是要前置執行的程式碼,JoinPoint是連線點,
@Before("pointcut()")
public void log(JoinPoint joinPoint) {
}
//@After 為後置通知
//@Around 為環繞通知
這裡的織入概念是個動作,即Spring將前面的切面、連線點、切入點關聯起來並建立通知代理的過程。織入可以在編譯時,類載入時和執行時完成。在編譯時進行織入就是靜態代理,而在執行時進行織入則是動態代理。
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());
}
}
JoinPoint物件封裝了SpringAop中切面方法的資訊,在切面方法中新增JoinPoint引數,就可以獲取到封裝了該方法資訊的JoinPoint物件.
方法 | 作用 | 返回物件 |
---|---|---|
getSignature() | 獲取封裝了署名資訊的物件,在該物件中可以獲取到目標方法名,所屬類的Class等資訊 | Signature |
getArgs() | 獲取 獲取傳入目標方法的引數物件 | Object[] |
getTarget() | 獲取被代理的物件 | Object |
proceedingJoinPoin物件是JoinPoint的子類,在原本JoinPoint的基礎上,放開了Proceeed()的使用,一般在環繞通知@Around
時使用:
Object proceed() throws Throwable //執行目標方法
Object proceed(Object[] var1) throws Throwable //傳入的新的引數去執行目標方法