Spring AOP-用代理代替繁瑣邏輯
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」第一手更新,有興趣的朋友可以關注公眾號,第一時間看到筆者分享的各項知識點,謝謝!筆芯!