Spring AOP
基礎概念
AOP 是一種面向切面的編程思想,通俗來講,這裏假如我們有多個方法。
@Component
public class Demo {
public void say1() {
System.out.println("say1~~~~~~~");
}
public void say2() {
System.out.println("say2~~~~~~~");
}
public void say3() {
System.out.println("say3~~~~~~~");
}
}
此時,如果我們要在每個方法執行完畢後,再輸出一句話,則需要在每個方法裏面都再加一個方法。
public void say1() {
System.out.println("say1~~~~~~~");
System.out.println("XX say good!!!");
}
public void say2() {
System.out.println("say2~~~~~~~");
System.out.println("XX say good!!!");
}
public void say3() {
System.out.println("say3~~~~~~~");
System.out.println("XX say good!!!");
}
這種方式,就會顯得代碼十分的冗餘且不夠優雅。
我們想一下,該實現的邏輯是在我們要在每個方法後面(切點)實現一個差不多的邏輯(切面實現),通過類似於下圖所示的方式,將和主要業務無關的代碼抽離出來,實現代碼的解耦。
類似於下圖所示的方式:
Spring 實現
首先,我們在一個 Spring Web 程序中,引入 spring-aop 的相關 jar 包。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
然後,我們構建一個切面類,在該類裏面,我們來定義要切入的點,以及切入後該做什麼。
@Aspect
@Component
public class LogAspect {
@After("execution(public * say*(..))")
public void saveLog() {
System.out.println("XX say good!!!");
}
}
在這裏,首先我們用 @Aspect
來聲明這是一個切面,然後用 @Component
來讓 Spring 容器可以掃描到該類。
緊接着,我們定義一個方法 saveLog()
,該方法的目的是在執行完 say1()
後,可以輸出一條日誌,所通過的方式便是註解: @After("execution(public * say*(..))")
。
有關於 aop 可以使用的註解,已經註解裏配置的切點表達式,在後續再進行展開。
最後,我們在啓動類上加上 @EnableAspectJAutoProxy
即可。
最後的實現效果,如下所示:
概念詳解
切面
連接點
Joinpoint,定義在應用程序流程的何處插入切面進行執行。
切入點
Pointcut,一組連接點的集合。
其實在 AOP 中,這些概念點並不重要,重要是理解,以及如何在實戰中進行演練。
可用切面
- before:先執行攔截代碼,如果攔截代碼錯誤,目標代碼不執行;
- after:先執行目標代碼,無論目標代碼執行正確與否,都會執行攔截代碼;
- afterReturning:和after不同的是,只有目標代碼正確返回,纔會執行攔截代碼;
- afterThrowing:和after不同的是,只有目標代碼拋出異常,纔會執行攔截代碼;
- around:能完全控制代碼執行,並可以在目標代碼前後,任意執行攔截代碼。
切點表達式
execution
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
- motifiers-pattern?:修飾符,public、protect、private、*(所有類型);
- ret-type-pattern:返回值;
- declaring-type-pattern?:類路徑匹配;
- name-pattern:方法名,支持*,_佔位符;
- param-pattern:參數匹配,..代表所有參數類型;
- throws-pattern?異常類型匹配
其中?代表該項是可選項。
另外切點表達式是可以組合的,用 || 或 && 可以進行邏輯組合。(不止是 execution,也可以跟其他的切點表達式進行組合)
// 匹配所有方法,無法使用
execution(* *(..))
// 匹配所有 com.demo 包下的公有的,返回值爲void的,方法名是say爲前綴的,參數隨意的方法
execution(public void com.demo say*(..))
@annotation
當執行的方法上有指定的註解,則算是匹配成功。
我認爲該方式會更加的靈活些,在下面的實戰演練中,我用的就是該方式,其攔截規則可以充分自定義,且可以在註解中,定義一些自己需要的值,然後在切面中進行使用。
args
用來匹配方法參數的。
- args():匹配不帶參數的方法;
- args(type(String)):匹配一個參數,且類型爲String的方法;
- args(..):匹配任意參數方法;
- args(String,..):匹配任意參數方法,但第一個參數類型是String的方法;
- args(..,String):匹配任意參數方法,但最後一個參數類型是String的方法;
該方法其實就是 execution 的變種形式,瞭解即可。
@args
也是用來匹配方法參數的,但是其匹配的邏輯是方法參數帶有執行註解的方法。
其他方法,如 within、this、target、@target、@within、bean 不多做介紹了,平常用的也不多,以後有興趣,或者在實際使用中有所涉及,再進行補充。
實戰演練
定義註解
/**
* 操作行爲註解,通過該註解獲取數據詳情
*
* @author iceWang
* @date 2020/9/10
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperatorAnnotation {
String bodyType();
String operatorType();
}
在這裏,我們定義一個註解,後續在要攔截的方法上,加上該註解即可。
其中 bodyType 代表我們要操作的實體類型,OperatorType 代表我們要操作的行爲類型。
業務邏輯
@OperatorAnnotation(bodyType = LogAspect.BODY_TYPE_COMPANY, operatorType = LogAspect.OPERATOR_TYPE_DELETE)
public String deleteCompany(String companyUniqueId) {
Optional.of(companyMapper.deleteCompany(companyUniqueId))
.filter(result -> result > 0)
.orElseThrow(() -> new IllegalArgumentException("無法刪除,請稍後再試!"));
return companyUniqueId;
}
因爲個人原因,這裏我們只展示一部分代碼——根據 id 刪除公司,定義實體類型爲 company,操作類型爲刪除,爲後續插入日誌做數據鋪墊。
切面定義
@Aspect
@Component
public class LogAspect {
public static final String BODY_TYPE_COMPANY = "company";
public static final String OPERATOR_TYPE_DELETE = "delete";
@AfterReturning(value = "@annotation(OperatorAnnotation)", returning = "result")
public void saveOperatorLog(JoinPoint joinPoint, Object result) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
OperatorAnnotation operatorAnnotation = methodSignature.getMethod().getAnnotation(OperatorAnnotation.class);
String bodyType = operatorAnnotation.bodyType();
String operatorType = operatorAnnotation.operatorType();
if (bodyType.contains(BODY_TYPE_COMPANY) && operatorType.contains(OPERATOR_TYPE_DELETE)) {
saveOperatorLog(bodyType, operatorType, result);
return;
}
}
/**
* 返回日誌操作實體類
* @param bodyType
* @param operatorType
* @return
*/
private Operator getOperator(String bodyType, String operatorType) {
return Operator.builder()
.bodyType(bodyType)
.operatorType(operatorType)
.createTime(LocalDateTime.now())
.build();
}
/**
* 保存日誌操作實體類
* @param bodyType
* @param operatorType
* @param result
*/
private void saveOperatorLog(String bodyType, String operatorType, Object result) {
Operator operator = getOperator(bodyType, operatorType);
operator.setOperatorUser(mdUserInfo.getPhone());
operator.setBody(result.toString());
operatorMapper.insert(operator);
}
}
在切面中,首先,我們用反射的方式來獲取方法上的註解,通過註解獲取實際的操作實體類型和操作類型,然後根據不同的實體類型和操作類型,執行不同的方法,將日誌插入數據庫中。
文章在公衆號「iceWang」第一手更新,有興趣的朋友可以關注公衆號,第一時間看到筆者分享的各項知識點,謝謝!筆芯!