1. 程式人生 > 實用技巧 >Spring AOP-用代理代替繁瑣邏輯

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


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

概念詳解

切面

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