Java開發學習(十六)----AOP切入點表示式及五種通知型別解析

2022-07-25 21:00:19

一、AOP切入點表示式

對於AOP中切入點表示式,總共有三個大的方面,分別是語法格式萬用字元書寫技巧

1.1 語法格式

首先我們先要明確兩個概念:

  • 切入點:要進行增強的方法

  • 切入點表示式:要進行增強的方法的描述方式

對於切入點的描述,我們其實是有兩種方式的,先來看下面的例子

描述方式一:執行com.itheima.dao包下的BookDao介面中的無引數update方法

execution(void com.itheima.dao.BookDao.update())

描述方式二:執行com.itheima.dao.impl包下的BookDaoImpl類中的無引數update方法

execution(void com.itheima.dao.impl.BookDaoImpl.update())

因為呼叫介面方法的時候最終執行的還是其實現類的方法,所以上面兩種描述方式都是可以的。

對於切入點表示式的語法為:

  • 切入點表示式標準格式:動作關鍵字(存取修飾符 返回值 包名.類/介面名.方法名(引數) 異常名)

對於這個格式,我們不需要硬記,通過一個例子,理解它:

execution(public User com.itheima.service.UserService.findById(int))
  • execution:動作關鍵字,描述切入點的行為動作,例如execution表示執行到指定切入點

  • public:存取修飾符,還可以是public,private等,可以省略

  • User:返回值,寫返回值型別

  • com.itheima.service:包名,多級包使用點連線

  • UserService:類/介面名稱

  • findById:方法名

  • int:引數,直接寫引數的型別,多個型別用逗號隔開

  • 異常名:方法定義中丟擲指定異常,可以省略

切入點表示式就是要找到需要增強的方法,所以它就是對一個具體方法的描述,但是方法的定義會有很多,所以如果每一個方法對應一個切入點表示式,想想這塊就會覺得將來編寫起來會比較麻煩,有沒有更簡單的方式呢?

就需要用到下面的萬用字元。

1.2 萬用字元

我們使用萬用字元描述切入點,主要的目的就是簡化之前的設定,具體都有哪些萬用字元可以使用?

  • *:單個獨立的任意符號,可以獨立出現,也可以作為字首或者字尾的匹配符出現

    execution(public * com.itheima.*.UserService.find*(*))

    匹配com.itheima包下的任意包中的UserService類或介面中所有find開頭的帶有一個引數的方法

  • ..:多個連續的任意符號,可以獨立出現,常用於簡化包名與引數的書寫

    execution(public User com..UserService.findById(..))

    匹配com包下的任意包中的UserService類或介面中所有名稱為findById的方法

  • +:專用於匹配子類型別

    execution(* *..*Service+.*(..))

    這個使用率較低,描述子類的,咱們做Java開發,繼承機會就一次,使用都很慎重,所以很少用它。*Service+,表示所有以Service結尾的介面的子類。

接下來,我們把使用到的切入點表示式來分析下:

execution(void com.itheima.dao.BookDao.update())
匹配介面,能匹配到
execution(void com.itheima.dao.impl.BookDaoImpl.update())
匹配實現類,能匹配到
execution(* com.itheima.dao.impl.BookDaoImpl.update())
返回值任意,能匹配到
execution(* com.itheima.dao.impl.BookDaoImpl.update(*))
返回值任意,但是update方法必須要有一個引數,無法匹配,要想匹配需要在update介面和實現類新增引數
execution(void com.*.*.*.*.update())
返回值為void,com包下的任意包三層包下的任意類的update方法,匹配到的是實現類,能匹配
execution(void com.*.*.*.update())
返回值為void,com包下的任意兩層包下的任意類的update方法,匹配到的是介面,能匹配
execution(void *..update())
返回值為void,方法名是update的任意包下的任意類,能匹配
execution(* *..*(..))
匹配專案中任意類的任意方法,能匹配,但是不建議使用這種方式,影響範圍廣
execution(* *..u*(..))
匹配專案中任意包任意類下只要以u開頭的方法,update方法能滿足,能匹配
execution(* *..*e(..))
匹配專案中任意包任意類下只要以e結尾的方法,update和save方法能滿足,能匹配
execution(void com..*())
返回值為void,com包下的任意包任意類任意方法,能匹配,*代表的是方法
execution(* com.itheima.*.*Service.find*(..))
將專案中所有業務層方法的以find開頭的方法匹配
execution(* com.itheima.*.*Service.save*(..))
將專案中所有業務層方法的以save開頭的方法匹配

後面兩種更符合我們平常切入點表示式的編寫規則

1.3 書寫技巧

對於切入點表示式的編寫其實是很靈活的,那麼在編寫的時候,有沒有什麼好的技巧讓我們用用:

  • 所有程式碼按照標準規範開發,否則以下技巧全部失效

  • 描述切入點常描述介面,而不描述實現類,如果描述到實現類,否則就出現緊耦合了

  • 存取控制修飾符針對介面開發均採用public描述(可省略存取控制修飾符描述

  • 返回值型別對於增刪改類使用精準型別加速匹配,對於查詢類使用*通配快速描述

  • 包名書寫儘量不使用..匹配,效率過低,常用*做單個包描述匹配,或精準匹配

  • 介面名/類名書寫名稱與模組相關的採用*匹配,例如UserService書寫成*Service,繫結業務層介面名

  • 方法名書寫以動詞進行精準匹配,名詞采用匹配,例如getById書寫成getBy,selectAll書寫成selectAll

  • 引數規則較為複雜,根據業務方法靈活調整

  • 通常不使用異常作為匹配規則

二、AOP通知型別

它所代表的含義是將通知新增到切入點方法執行的前面

除了這個註解外,還有沒有其他的註解,換個問題就是除了可以在前面加,能不能在其他的地方加?

2.1 型別介紹

我們先來回顧下AOP通知:

  • AOP通知描述了抽取的共性功能,根據共性功能抽取的位置不同,最終執行程式碼時要將其加入到合理的位置

通知具體要新增到切入點的哪裡?

共提供了5種通知型別:

  • 前置通知

  • 後置通知

  • 環繞通知(重點)

  • 返回後通知(瞭解)

  • 丟擲異常後通知(瞭解)

為了更好的理解這幾種通知型別,我們來看一張圖

(1)前置通知,追加功能到方法執行前,類似於在程式碼1或者程式碼2新增內容

(2)後置通知,追加功能到方法執行後,不管方法執行的過程中有沒有丟擲異常都會執行,類似於在程式碼5新增內容

(3)返回後通知,追加功能到方法執行後,只有方法正常執行結束後才進行,類似於在程式碼3新增內容,如果方法執行丟擲異常,返回後通知將不會被新增

(4)丟擲異常後通知,追加功能到方法丟擲異常後,只有方法執行出異常才進行,類似於在程式碼4新增內容,只有方法丟擲異常後才會被新增

(5)環繞通知,環繞通知功能比較強大,它可以追加功能到方法執行的前後,這也是比較常用的方式,它可以實現其他四種通知型別的功能,具體是如何實現的,需要我們往下學習。

2.2 環境準備

  • 建立一個Maven專案

  • pom.xml新增Spring依賴

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
          <groupId>org.aspectj</groupId>
          <artifactId>aspectjweaver</artifactId>
          <version>1.9.4</version>
        </dependency>
    </dependencies>
  • 新增BookDao和BookDaoImpl類

    public interface BookDao {
        public void update();
        public int select();
    }
    ​
    @Repository
    public class BookDaoImpl implements BookDao {
        public void update(){
            System.out.println("book dao update ...");
        }
        public int select() {
            System.out.println("book dao select is running ...");
            return 100;
        }
    }
  • 建立Spring的設定類

    @Configuration
    @ComponentScan("com.itheima")
    @EnableAspectJAutoProxy
    public class SpringConfig {
    }
  • 建立通知類

    @Component
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(void com.itheima.dao.BookDao.update())")
        private void pt(){}
    ​
        public void before() {
            System.out.println("before advice ...");
        }
    ​
        public void after() {
            System.out.println("after advice ...");
        }
    ​
        public void around(){
            System.out.println("around before advice ...");
            System.out.println("around after advice ...");
        }
    ​
        public void afterReturning() {
            System.out.println("afterReturning advice ...");
        }
        
        public void afterThrowing() {
            System.out.println("afterThrowing advice ...");
        }
    }
  • 編寫App執行類

    public class App {
        public static void main(String[] args) {
            ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
            BookDao bookDao = ctx.getBean(BookDao.class);
            bookDao.update();
        }
    }

最終建立好的專案結構如下:

2.3 通知型別的使用

前置通知

修改MyAdvice,在before方法上新增@Before註解

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
    
    @Before("pt()")
    //此處也可以寫成 @Before("MyAdvice.pt()"),不建議
    public void before() {
        System.out.println("before advice ...");
    }
}

後置通知
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
    
    @Before("pt()")
    public void before() {
        System.out.println("before advice ...");
    }
    @After("pt()")
    public void after() {
        System.out.println("after advice ...");
    }
}

環繞通知
基本使用
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
    
    @Around("pt()")
    public void around(){
        System.out.println("around before advice ...");
        System.out.println("around after advice ...");
    }
}

執行結果中,通知的內容列印出來,但是原始方法的內容卻沒有被執行。

因為環繞通知需要在原始方法的前後進行增強,所以環繞通知就必須要能對原始操作進行呼叫,具體如何實現?

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
    
    @Around("pt()")
    public void around(ProceedingJoinPoint pjp) throws Throwable{
        System.out.println("around before advice ...");
        //表示對原始操作的呼叫
        pjp.proceed();
        System.out.println("around after advice ...");
    }
}

說明:proceed()為什麼要丟擲異常?

主要原因原始方法不清楚到底執行會不會有異常,所以直接先丟擲異常。原因很簡單,看下原始碼就知道了

再次執行,程式可以看到原始方法已經被執行了

注意事項

(1)原始方法有返回值的處理

  • 修改MyAdvice,對BookDao中的select方法新增環繞通知,

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
    
    @Pointcut("execution(int com.itheima.dao.BookDao.select())")
    private void pt2(){}
    
    @Around("pt2()")
    public void aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice ...");
        //表示對原始操作的呼叫
        pjp.proceed();
        System.out.println("around after advice ...");
    }
}
  • 修改App類,呼叫select方法

public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        int num = bookDao.select();
        System.out.println(num);
    }
}

執行後會報錯,錯誤內容為:

Exception in thread "main" org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type for: public abstract int com.itheima.dao.BookDao.select() at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:226) at com.sun.proxy.$Proxy19.select(Unknown Source) at com.itheima.App.main(App.java:12)

錯誤大概的意思是:空的返回不匹配原始方法的int返回

  • void就是返回Null

  • 原始方法就是BookDao下的select方法

所以如果我們使用環繞通知的話,要根據原始方法的返回值來設定環繞通知的返回值,具體解決方案為:

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
    
    @Pointcut("execution(int com.itheima.dao.BookDao.select())")
    private void pt2(){}
    
    @Around("pt2()")
    public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice ...");
        //表示對原始操作的呼叫
        Object ret = pjp.proceed();
        System.out.println("around after advice ...");
        return ret;
    }
}

說明:

為什麼返回的是Object而不是int的主要原因是Object型別更通用。所以更一般的寫法環繞通知的返回型別寫object而不是void,如果沒有返回值,那麼object就為空

在環繞通知中是可以對原始方法返回值就行修改的。

返回後通知
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
    
    @Pointcut("execution(int com.itheima.dao.BookDao.select())")
    private void pt2(){}
    
    @AfterReturning("pt2()")
    public void afterReturning() {
        System.out.println("afterReturning advice ...");
    }
}

注意:返回後通知是需要在原始方法select正常執行後才會被執行,如果select()方法執行的過程中出現了異常,那麼返回後通知是不會被執行。後置通知是不管原始方法有沒有丟擲異常都會被執行。

異常後通知
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
    
    @Pointcut("execution(int com.itheima.dao.BookDao.select())")
    private void pt2(){}
    
    @AfterReturning("pt2()")
    public void afterThrowing() {
        System.out.println("afterThrowing advice ...");
    }
}

注意:異常後通知是需要原始方法丟擲異常,可以在select()方法中新增一行程式碼int i = 1/0即可。如果沒有拋異常,異常後通知將不會被執行。

介紹完這5種通知型別,我們來思考下環繞通知是如何實現其他通知型別的功能的?

因為環繞通知是可以控制原始方法執行的,所以我們把增強的程式碼寫在呼叫原始方法的不同位置就可以實現不同的通知型別的功能,如:

通知型別總結
知識點1:@After
名稱 @After
型別 方法註解
位置 通知方法定義上方
作用 設定當前通知方法與切入點之間的繫結關係,當前通知方法在原始切入點方法後執行
知識點2:@AfterReturning
名稱 @AfterReturning
型別 方法註解
位置 通知方法定義上方
作用 設定當前通知方法與切入點之間繫結關係,當前通知方法在原始切入點方法正常執行完畢後執行
知識點3:@AfterThrowing
名稱 @AfterThrowing
型別 方法註解
位置 通知方法定義上方
作用 設定當前通知方法與切入點之間繫結關係,當前通知方法在原始切入點方法執行丟擲異常後執行
知識點4:@Around
名稱 @Around
型別 方法註解
位置 通知方法定義上方
作用 設定當前通知方法與切入點之間的繫結關係,當前通知方法在原始切入點方法前後執行

環繞通知注意事項

  1. 環繞通知必須依賴形參ProceedingJoinPoint才能實現對原始方法的呼叫,進而實現原始方法呼叫前後同時新增通知

  2. 通知中如果未使用ProceedingJoinPoint對原始方法進行呼叫將跳過原始方法的執行

  3. 對原始方法的呼叫可以不接收返回值,通知方法設定成void即可,如果接收返回值,最好設定為Object型別

  4. 原始方法的返回值如果是void型別,通知方法的返回值型別可以設定成void,也可以設定成Object

  5. 由於無法預知原始方法執行後是否會丟擲異常,因此環繞通知方法必須要處理Throwable異常,推薦直接丟擲異常