一、基本概念
- AOP,即面向切面程式設計,利用AOP可以對業務邏輯的各個部分進行隔離,進而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高開發的效率
- 可以明确的定義這個功能應用在哪裡,以什麼方式應用,并且不必修改受影響的類。這樣一來橫切關注點就被子產品化到特殊的類裡——這樣的類我們通常稱之為“切面”
- 即:不通過修改源代碼的方式,在主幹功能裡面添加新功能
- AOP的好處:
- 每個事物邏輯位于一個位置,代碼不分散,便于維護和更新
- 業務子產品更簡潔,隻包含核心業務代碼
二、底層原理
- AOP底層使用動态代理,在有接口的情況下,使用JDK動态代理;在沒有接口的情況下,使用CGLIB代理。具體的設計模式,詳見代理模式
三、AOP術語
- 橫切關注點:從每個方法中抽取出來的同一類非核心業務。
- 切面(Aspect):封裝橫切關注點資訊的類,每個關注點展現為一個通知方法。
- 通知(Advice):切面必須要完成的各個具體工作
- 目标(Target):被通知的對象
- 代理(Proxy):向目标對象應用通知之後建立的代理對象
-
連接配接點(Joinpoint):橫切關注點在程式代碼中的具體展現,對應程式執行的某個特定位置。例如:類某個方法調用前、調用後、方法捕獲到異常後等。
在應用程式中可以使用橫縱兩個坐标來定位一個具體的連接配接點:
- 切入點(pointcut): 定位連接配接點的方式。每個類的方法中都包含多個連接配接點,是以連接配接點是類中客觀存在的事物。如果把連接配接點看作資料庫中的記錄,那麼切入點就是查詢條件——AOP可以通過切入點定位到特定的連接配接點。切點通過
接口進行描述,它使用類和方法作為連接配接點的查詢條件。org.springframework.aop.Pointcut
- 簡單來說,橫切關注點就是被抽取出來的業務,切面就是封裝業務方法的類,通知就是具體的方法。每個目标都有相對應的橫切關注點,在程式中的具體位置就是連接配接點,而連接配接點可以選擇是否切入通知,選擇被切入通知的位置叫做切入點。
四、AOP依賴
- spring架構一般都是基于AspectJ實作AOP操作
- 相關依賴:
- 或通過maven導入
<dependencies>
<!--spring AOP的包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!--springIOC的包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>
五、基于注解
- 在spring配置檔案中,開啟注解掃描、開啟aop
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 開啟注解掃描 --> <context:component-scan base-package="com.du.spring5"/>
- 開啟生成代理對象
<!-- 開啟生成代理對象 --> <aop:aspectj-autoproxy/>
- 通過注解
建立@Component
對象,在增強類上面添加UserProxy
,說明這是一個切面對象@Aspect
@Component @Aspect public class UserProxy2 { ... }
- 配置不同類型的通知。在作為通知方法上面添加通知類型注解,使用切入點表達式配置。
- AspectJ支援5種類型的通知注解:
-
:前置通知,在方法執行之前執行@Before
-
:後置通知,在方法執行之後執行@After
-
:傳回通知,在方法傳回結果之後執行@AfterRunning
-
:異常通知,在方法抛出異常之後執行@AfterThrowing
-
:環繞通知,圍繞着方法執行@Around
-
- 在切面上再添加一個
注解,可以設定多個切面的優先級,值越小優先級越大,越先運作@Order
@Component //通過注解配置 @Aspect // 生成代理對象 @Order(10) public class UserProxy { }
六、切入點表達式
- 知道對哪個類裡面的哪個方法進行增強
- 文法結構:
execution([權限修飾符] [傳回類型] [類全路徑] [方法名稱]([參數清單]) )
6.1 例子
- 對
類裡面的com.du.spring5.bean.User
進行增強:add
execution(* com.du.spring5.bean.User.add(..))
- 對
類裡面的所有方法進行增強:com.du.spring5.bean.User
execution(* com.du.spring5.bean.User.*(..))
- 對
包内的所有類的所有方法進行增強:com.du.spring5.bean
execution(* com.du.spring5.bean.*.*(..))
-
比對任意數量、任意類型的參數..
6.2 例子二
- 在AspectJ中,切入點表達式可以通過 “&&”、“||”、“!”等操作符結合起來。
- 任意類中第一個參數為int類型的add方法或sub方法:
execution (* *.add(int,..)) || execution(* *.sub(int,..))
七、通知(Advice)
- 在具體的連接配接點上要執行的操作
- 一個切面可以包括一個或者多個通知。
- 通知所使用的注解的值往往是切入點表達式。
7.1 前置通知
- 在方法執行之前執行的通知
- 使用
注解@Before
7.2 後置通知
- 後置通知:後置通知是在連接配接點完成之後執行的,即連接配接點傳回結果或者抛出異常的之後。可以類比成finally,無論是否正常結束,都會執行。
- 使用
注解@After
7.3 傳回通知
- 隻有正常結束了,才會執行
- 使用
注解@AfterReturning
- 若想擷取方法執行完之後的傳回結果,可以在參數清單中添加一個接收
的參數,示例如下:result
@AfterReturning(value = "execution(* com.du.spring5.bean.*.*(..))", returning = "result")
public void afterReturning(Object result) {
System.out.println("方法執行結束,傳回值為" + result);
}
指定 傳回值賦給哪個參數,将參數名
returning
賦給它
result
7.4 異常通知
- 隻在連接配接點抛出異常時才執行異常通知
- 使用
注解@AfterThrowing
- 若想捕獲異常的資訊,就可以像上面一樣,在入參中添加一個接受
的參數,并在注解中說明Exception
@AfterThrowing(value = "execution(* com.du.spring5.bean.*.*(..))", throwing = "exception")
public void afterThrowing(Exception exception) {
System.out.println("發生了" + exception + "異常");
}
7.5 JoinPoint
- 若想在通知中接收目标方法的資訊,就可以在參數清單中添加一個
類型的參數,Spring 會自動将該對象注入到方法中,無需在注解中說明。JoinPoint
@Before(value = "execution(* com.du.spring5.bean.*.*(..))")
public void before(JoinPoint joinPoint) {
// 擷取方法的所有輸入參數
Object[] args = joinPoint.getArgs();
// 擷取簽名
Signature signature = joinPoint.getSignature();
System.out.println("正在執行[" + signature.getName() + "]方法, 參數為" + Arrays.toString(args));
}
更多細節可參考 JoinPoint
接口
7.6 @Pointcut
- 為了避免切入點表達式重複寫,可以通過注解
,統一配置,統一使用。@Pointcut
@Pointcut("execution(* com.du.spring5.bean.*.*(..))") public void pointcut() { } @Before(value = "pointcut()") public void before(JoinPoint joinPoint) { }
7.7 環繞通知
- 環繞通知是所有通知類型中功能最為強大的,能夠全面地控制連接配接點,甚至可以控制是否執行連接配接點。
- 很類似于反射的方法,相當于在參數中接收一個包含需要執行的方法及其參數的對象
,然後通過反射的方法執行該方法。而在執行方法的周圍,可以通過添加一些ProceedingJoinPoint
,實作與上面四種通知類型的效果。try-catch-finally
@Around(value="pointcut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { try { System.out.println("【環繞前置通知】"); Object[] args = joinPoint.getArgs(); // 擷取參數 Object res = joinPoint.proceed(args); // 執行方法,擷取結果 System.out.println("【環繞傳回通知】,結果為" + res); return res; } catch (Throwable throwable) { System.out.println("【環繞異常通知】"); throw new RuntimeException(throwable); } finally { System.out.println("【環繞後置通知】"); } }
記得将接收到的結果傳回出去,否則之前注解的
@AfterReturning
的方法、真正調用方法的地方,沒辦法擷取到執行的結果。
攔截到的
,記得也抛出去,否則其他地方也接收不到。Exception
7.8 執行順序
- 目前版本是
,執行順序如下5.2.10
- 無異常:
-> 真正的方法 ->@Before
->@AfterReturning
@After
- 存在異常:
-> 真正的方法 ->@Before
->@AfterThrowing
@After
- 包含上述的環繞通知之後:
->環繞前置通知
-> 真正的方法 ->@Before
->@AfterReturning
->@After
->環繞傳回通知
環繞後置通知
八、完全注解開發
- 不使用xml檔案,直接使用一個config類
@Configuration // 表示這是個配置類
@ComponentScan(basePackages = {"com.du.spring5"}) // 開啟元件掃描
@EnableAspectJAutoProxy(proxyTargetClass = true) // 開啟aop
public class ConfigAop {
}
- 調用
@Test
public void test3() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigAop.class); // 配置類
User2 user2 = context.getBean("user2", User2.class);
user2.add();
}
九、基于xml檔案
- 除了使用AspectJ注解聲明切面,Spring也支援在bean配置檔案中聲明切面。這種聲明是通過aop名稱空間中的XML元素完成的。
- 正常情況下,基于注解的聲明要優先于基于XML的聲明。通過AspectJ注解,切面可以與AspectJ相容,而基于XML的配置則是Spring專有的。由于AspectJ得到越來越多的 AOP架構支援,是以以注解風格編寫的切面将會有更多重用的機會。
- 比較:較重要的配置,通過
配置;其他配置通過注解配置。xml
9.1 配置案例
- xml檔案,名稱空間添加aop相關内容
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd" >
- 在xml上配置aop
- 在bean配置檔案中,所有的Spring AOP配置都必須定義在
元素内部。對于每個切面而言,都要建立一個<aop:config>
元素來為具體的切面實作引用後端bean執行個體。<aop:aspect>
- 切面bean必須有一個辨別符,供
元素引用。<aop:aspect>
<aop:config> <!-- 設定切入點 --> <aop:pointcut id="user_add_pointcut" expression="execution(* com.du.spring5.bean.User.add(..))"/> <!--設定切面--> <aop:aspect ref="userProxy"> <!-- 設定切面的位置,以及使用切面插入的方法,切入點--> <!-- 标簽頭說明了切入的位置,method為指定的方法,pointcut-ref為指定的切入點 --> <aop:before method="before" pointcut-ref="user_add_pointcut"/> <aop:after method="after" pointcut-ref="user_add_pointcut"/> <aop:after-returning method="afterReturn" pointcut-ref="user_add_pointcut"/> <aop:after-throwing method="afterThrowing" pointcut-ref="user_add_pointcut"/> <aop:around method="around" pointcut-ref="user_add_pointcut"/> </aop:aspect> </aop:config>
- 在bean配置檔案中,所有的Spring AOP配置都必須定義在
- 原始類
public class User { private final static Logger logger = Logger.getLogger(User.class); public void add() { logger.info("add......."); } }
- 代理類
public class UserProxy { private final static Logger logger = Logger.getLogger(UserProxy.class); /** * 前置通知 */ public void before() { logger.info("before"); } /** * 最終通知 */ public void after() { logger.info("after"); } /** * 後置通知(傳回通知) */ public void afterReturn() { logger.info("afterReturn"); } /** * 異常通知 */ public void afterThrowing() { logger.info("afterThrowing"); } /** * 環繞通知 * @param proceedingJoinPoint * @throws Throwable */ public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { logger.info("around before"); proceedingJoinPoint.proceed(); logger.info("around ater"); } }
9.2 配置
- 所有的Spring AOP配置都必須定義在
元素内部。在這個config裡面,把需要的切面類在裡面通過<aop:config>
配置。<aop:aspect>
9.3 切面
-
切面裡面需要指定切面類是哪個(已經被注入到IOC容器内了),通過<aop:aspect>
參數指定對象id;裡面還可以指定優先級ref
order
9.3 切入點
- 切入點使用
元素聲明<aop:pointcut>
- 這個标簽可以放進
裡面,給所有切面使用,也可以放進<aop:config>
隻給目前切面使用。<aop:aspect>
- 該标簽需要通過
指定切入點表達式,設定expression
以供别的标簽調用。id
9.4 聲明通知
-
通知可以通過before
标簽指定,其中,<aop:before>
屬性指定方法,method
指定切入點。pointcut-ref
- 具體配置
<aop:config>
<!-- 設定切入點 -->
<aop:pointcut id="user_add_pointcut" expression="execution(* com.du.spring5.bean.User.add(..))"/>
<!--設定切面-->
<aop:aspect ref="userProxy">
<!-- 設定切面的位置,以及使用切面插入的方法,切入點-->
<!-- method為切入的位置,pointcut-ref為代理類的指定的方法 -->
<aop:before method="before" pointcut-ref="user_add_pointcut"/>
<aop:after method="after" pointcut-ref="user_add_pointcut"/>
<aop:after-returning method="afterReturn" pointcut-ref="user_add_pointcut"/>
<aop:after-throwing method="afterThrowing" pointcut-ref="user_add_pointcut"/>
<aop:around method="around" pointcut-ref="user_add_pointcut"/>
</aop:aspect>
</aop:config>