一、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 |
類型 |
方法注解 |
位置 |
通知方法定義上方 |
作用 |
設置當前通知方法與切入點之間的綁定關系,當前通知方法在原始切入點方法前后運行 |
環繞通知注意事項
- 環繞通知必須依賴形參ProceedingJoinPoint才能實現對原始方法的調用,進而實現原始方法調用前后同時添加通知
- 通知中如果未使用ProceedingJoinPoint對原始方法進行調用將跳過原始方法的執行
- 對原始方法的調用可以不接收返回值,通知方法設置成void即可,如果接收返回值,最好設定為Object類型
- 原始方法的返回值如果是void類型,通知方法的返回值類型可以設置成void,也可以設置成Object
- 由于無法預知原始方法運行后是否會拋出異常,因此環繞通知方法必須要處理Throwable異常,推薦直接拋出異常
原文鏈接:
https://www.cnblogs.com/xiaoyh/p/16412318.html