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!!!");
    }


這種方式,就會顯得代碼十分的冗餘且不夠優雅。


我們想一下,該實現的邏輯是在我們要在每個方法後面(切點)實現一個差不多的邏輯(切面實現),通過類似於下圖所示的方式,將和主要業務無關的代碼抽離出來,實現代碼的解耦。


類似於下圖所示的方式:
image.png

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 即可。


最後的實現效果,如下所示:
image.png

概念詳解

切面

Aspect,要抽象出來的橫跨多個地方的功能。

連接點

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公衆號

文章在公衆號「iceWang」第一手更新,有興趣的朋友可以關注公衆號,第一時間看到筆者分享的各項知識點,謝謝!筆芯!

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