AOP面向切面編程
一、AOP概述
1. AOP簡介
AOP(Aspect Orient Programming),面向切面編程。面向切面編程是從動態角度考慮程式運行過程
AOP 底層,就是採用動態代理模式實現的。採用了兩種代理:JDK 的動態代理,與 CGLIB的動態代理
可以看之前寫的動態代理,
//www.cnblogs.com/mengd/p/13429797.html
- jdk動態代理:使用jdk中的Proxy,Method,InvocaitonHanderl創建代理對象,jdk動態代理要求目標類必須實現介面
- cglib動態代理:第三方的工具庫,創建代理對象,原理是繼承。 通過繼承目標類,創建子類,子類就是代理對象。 要求目標類不能是final的, 方法也不能是final的
動態代理的作用:
- 在目標類源程式碼不改變的情況下,增加功能
- 減少程式碼的重複
- 專註業務邏輯程式碼
- 解耦合,讓你的業務功能和日誌分離,事務和非業務功能分離
2. 如何理解AOP
AOP(Aspect Orient Programming)面向切面編程
- Aspect: 切面,給你的目標類增加的功能,就是切面,切面的特點: 一般都是非業務方法,獨立使用的
- Orient:面向
- oop: 面向對象編程
理解:
- 需要在分析項目功能時,找出切面
- 合理的安排切面的執行時間(在目標方法前, 還是目標方法後)
- 合理的安全切面執行的位置,在哪個類,哪個方法增加增強功能
二、AOP編程術語
1. 切面(Aspect)
表示增強的功能, 就是一堆程式碼,完成某個一個功能,非業務功能
常見的切面功能有日誌, 事務, 統計資訊, 參數檢查, 許可權驗證
2. 連接點(JoinPoint)
連接業務方法和切面的位置,就某類中的業務方法
3. 切入點(Pointcut)
指多個連接點方法的集合,多個方法
4. 目標對象(Target)
給哪個類的方法增加功能, 這個類就是目標對象
5. 通知(Advice)
通知表示切面功能執行的時間
一個切面有三個關鍵的要素:
- 切面的功能程式碼,切面是幹什麼
- 切面的執行位置,使用Pointcut表示切面執行的位置
- 切面的執行時間,使用Advice表示時間,在目標方法之前,還是目標方法之後
三、Aspectj對AOP的實現
aop是一個規範,是動態的一個規範化,一個標準
aop的技術實現框架:
- spring:spring在內部實現了aop規範,能做aop的工作,我們項目開發中很少使用spring的aop實現。 因為spring的aop比較笨重
- aspectJ: 一個開源的專門做aop的框架。spring框架中集成了aspectj框架,通過spring就能使用aspectj的功能
aspectJ框架實現aop有兩種方式:
- 使用xml的配置文件 : 配置全局事務
- 使用註解,我們在項目中要做aop功能,一般都使用註解, aspectj有5個註解
1. Aspectj的通知類型
AspectJ 中常用的通知有五種類型
- 前置通知
- 後置通知
- 環繞通知
- 異常通知
- 最終通知
2. Aspectj的切入點表達式
以上表達式共4個部分
execution(訪問許可權 方法返回值 方法聲明(參數) 異常類型)
切入點表達式要匹配的對象就是目標方法的方法名。所以,execution 表達式中明顯就
是方法的簽名。
注意,表達式中黑色文字表示可省略部分,各部分間用空格分開
在其中可以使用以下符號:
常用的幾個:
execution(public * * (..))
指定切入點的位置:任意的公共方法
execution(* set*(..))
指定切入點的位置:任何一個以set開始的方法
execution(* com.xyz.service.*.*(..))
指定切入點的位置:定義在service包里的任意類的任意方法
execution(* com.xyz.service..*.*(..))
指定切入點的位置:定義在service包或者子包里的任意類的任意方法
..
出現在類名中時,後面必須跟*
,表示包、子包下的所有類
execution(* *..service.*.*(..))
指定所有包下的service子包下所有類中所有的方法為切入點
3. Aspectj的開發環境
1. maven依賴
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!--spring依賴-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<!--aspectj依賴-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
2. 引入AOP約束
在 AspectJ 實現 AOP 時,要引入 AOP 的約束。配置文件中使用的 AOP 約束中的標籤,均是 AspectJ 框架使用的,而非 Spring 框架本身在實現 AOP 時使用的
AspectJ 對於 AOP 的實現有註解和配置文件兩種方式,常用是註解方式
四、AspectJ基於註解的AOP實現
1. 實現步驟
1. 定義業務介面與實現類
package com.md.b1;
/**
* @author MD
* @create 2020-08-09 10:55
*/
public interface SomeService {
void doSome(String name , Integer age);
}
//-------------------------
package com.md.b1;
/**
* @author MD
* @create 2020-08-09 10:55
*/
// 目標類
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String name, Integer age) {
// 給doSome方法增加一個功能,在執行之前輸出時間
System.out.println("目標方法doSome()");
}
}
2. 定義切面類
類中定義了若干普通方法,將作為不同的通知方法,用來增強功能
注意點:
@Aspect
- 這個註解是aspectj框架中的註解
- 作用:表示當前類是切面類
- 切面類:是用來給業務方法增加功能的類,在這個類中有切面的功能程式碼
- 位置:類定義的上面
定義方法,方法是實現切面功能的
方法的要求:
- 公共方法
- 方法名稱自定義
- 方法沒有返回值
- 方法可以有或沒有參數,如果有參數,參數不是自定義的,有幾個參數類型可以使用
package com.md.b1;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import java.util.Date;
/**
* @author MD
* @create 2020-08-09 10:58
*/
@Aspect
public class MyAspect {
// 前置通知,具體的在下變
@Before(value = "execution(public void com.md.b1.SomeServiceImpl.doSome(String,Integer))")
public void myBefore(){
// 就是你切面要執行的功能程式碼
System.out.println("前置通知,切面功能:在目標方法之前輸出時間:"+new Date());
}
}
3. 定義目標對象切面類對象
還是在src/main/resources下建立applicationContext.xml
整體結構如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="//www.springframework.org/schema/beans"
xmlns:xsi="//www.w3.org/2001/XMLSchema-instance" xmlns:aop="//www.springframework.org/schema/aop"
xsi:schemaLocation="//www.springframework.org/schema/beans //www.springframework.org/schema/beans/spring-beans.xsd //www.springframework.org/schema/aop //www.springframework.org/schema/aop/spring-aop.xsd">
<!--把對象交給spring容器,由spring容器統一創建,管理對象-->
<!--聲明目標對象-->
<bean id="someService" class="com.md.b1.SomeServiceImpl"/>
<!--聲明切面類對象-->
<bean id="myAspect" class="com.md.b1.MyAspect"/>
</beans>
4. 註冊AspectJ自動代理
在上面文件的基礎上添加
<bean id="someService" class="com.md.b1.SomeServiceImpl"/>
<bean id="myAspect" class="com.md.b1.MyAspect"/>
<!--聲明自動代理生成器:
使用的是aspectj框架內部的功能,創建目標對象的代理對象
創建代理對象是在記憶體中實現的,修改目標對象的記憶體中的結構,
創建為代理對象。所以,目標對象就是被修改後的代理對象
aspectj-autoproxy:會把spring容器中的所有目標對象,一次性都生成代理對象
-->
<aop:aspectj-autoproxy />
<aop:aspectj-autoproxy/>的底層是由 AnnotationAwareAspectJAutoProxyCreator 實現的。
從其類名就可看出,是基於 AspectJ 的註解適配自動代理生成器。
其工作原理是,<aop:aspectj-autoproxy/>通過掃描找到@Aspect 定義的切面類,再由切
面類根據切入點找到目標類的目標方法,再由通知類型找到切入的時間點
5. 測試類中的使用
package com.md;
import com.md.b1.SomeService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import sun.security.provider.Sun;
/**
* @author MD
* @create 2020-08-09 15:28
*/
public class MyTest01 {
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
// 從容器中獲取目標對象,此時的目標對象是經過了aspectj修改後的代理對象
SomeService proxy = (SomeService) ac.getBean("someService");
//com.sun.proxy.$Proxy8 jdk動態代理
//System.out.println(proxy.getClass().getName());
// 通過代理的對象執行方法,實現目標方法執行,增強了功能
proxy.doSome("張三",19);
// 前置通知,切面功能:在目標方法之前輸出時間:Sun Aug 09 15:33:24 CST 2020
// 目標方法doSome()
}
}
2. @Before前置通知
在目標方法執行之前執行
被註解為前置通知的方法,可以包含一個 JoinPoint 類型參數。該類型的對象本身就是切入點表達式
通過該參數,可獲取切入點表達式、方法簽名、目標對象等
不光前置通知的方法,可以包含一個 JoinPoint 類型參數,所有的通知方法均可包含該參數。
這個JoinPoint參數的值是由框架賦予, 必須是第一個位置的參數
@Aspect
public class MyAspect {
// @Before():前置通知註解
// 屬性:value 是切入點表達式,表示切面功能執行的位置
// 位置:在方法的上面
// 特點:
// 1. 在目標方法之前執行
// 2. 不會改變目標方法的執行結果
// 3. 不會影響目標方法的執行
// @Before(value = "execution(public void com.md.b1.SomeServiceImpl.doSome(String,Integer))")
// public void myBefore(){
// // 就是你切面要執行的功能程式碼
// System.out.println("前置通知,切面功能:在目標方法之前輸出時間:"+new Date());
// }
// @Before(value = "execution( * *..SomeServiceImpl.do*(..))")
// public void myBefore(){
// // 就是你切面要執行的功能程式碼
// System.out.println("前置通知,切面功能:在目標方法之前輸出時間:"+new Date());
// }
/**
* 指定通知方法中的參數 : JoinPoint
* JoinPoint:業務方法,要加入切面功能的業務方法
* 作用是:可以在通知方法中獲取方法執行時的資訊, 例如方法名稱,方法的實參。
* 如果你的切面功能中需要用到方法的資訊,就加入JoinPoint.
* 這個JoinPoint參數的值是由框架賦予, 必須是第一個位置的參數
*/
@Before(value = "execution(void *..SomeServiceImpl.doSome(String,Integer))")
public void myBefore(JoinPoint jp){
// 獲取方法的完整定義
System.out.println("方法的定義:"+jp.getSignature());
System.out.println("方法的名稱:"+jp.getSignature().getName());
// 獲取方法的實參
Object[] args = jp.getArgs();
for (Object arg:args){
System.out.println("參數:"+arg);
}
// 方法的定義:void com.md.b1.SomeService.doSome(String,Integer)
// 方法的名稱:doSome
// 參數:張三
// 參數:19
// 就是你切面要執行的功能程式碼
System.out.println("前置通知,切面功能:在目標方法之前輸出時間:"+new Date());
}
}
3. @AfterReturning後置通知
在目標方法執行之後執行。由於是目標方法之後執行,所以可以獲取到目標方法的返回值
該註解的 returning 屬性就是用於指定接收方法返回值的變數名的
所以,被註解為後置通知的方法,除了可以包含 JoinPoint 參數外,還可以包含用於接收返回值的變數。該變數最好為 Object 類型,因為目標方法的返回值可能是任何類型
增加介面的方法
public interface SomeService {
void doSome(String name, Integer age);
String doOther(String name , Integer age);
}
//--------------------------------------------------
// 目標類
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String name, Integer age) {
// 給doSome方法增加一個功能,在執行之前輸出時間
System.out.println("目標方法doSome()");
}
@Override
public String doOther(String name, Integer age) {
System.out.println("目標方法doOther()");
return "a";
}
}
定義切面
@Aspect
public class MyAspect {
/**
* 後置通知定義方法,方法是實現切面功能的。
* 方法的定義要求:
* 1.公共方法 public
* 2.方法沒有返回值
* 3.方法名稱自定義
* 4.方法有參數的,推薦是Object ,參數名自定義
*/
/**
* @AfterReturning:後置通知
* 屬性:1.value 切入點表達式
* 2.returning 自定義的變數,表示目標方法的返回值的。
* 自定義變數名必須和通知方法的形參名一樣。
* 位置:在方法定義的上面
* 特點:
* 1。在目標方法之後執行的。
* 2. 能夠獲取到目標方法的返回值,可以根據這個返回值做不同的處理功能
* Object res = doOther();
* 3. 可以修改這個返回值,但不影響最後的調用結果
*
* 後置通知的執行
* Object res = doOther();
* 參數傳遞: 傳值, 傳引用
* myAfterReturing(res);
* System.out.println("res="+res)
*
*/
@AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",returning = "res")
public void myAfterReturing(Object res){
if (res.equals("a")){
// 你可以做一些功能
System.out.println("登陸成功");
}else{
System.out.println("登陸失敗");
}
// Object res:是目標方法執行後的返回值,可以根據返回值做切面功能處理
System.out.println("後置通知:獲取的返回值是:"+res);
// 修改目標方法的返回值,是否影響最後的方法調用結果
// 無影響,
if (res != null){
res = "hello Aspectj";
}
}
}
4. @Around環繞通知
在目標方法執行之前之後執行。
被註解為環繞增強的方法要有返回值,Object 類型。並且方法可以包含一個 ProceedingJoinPoint 類型的參數。
介面 ProceedingJoinPoint 其有一個proceed()方法,用於執行目標方法。
若目標方法有返回值,則該方法的返回值就是目標方法的返回值。
最後,環繞增強方法將其返回值返回。該增強方法實際是攔截了目標方法的執行
首先增加方法和實現
public interface SomeService {
void doSome(String name, Integer age);
String doOther(String name, Integer age);
String doFirst(String name,Integer age);
}
//-------------------------------------------
// 目標類
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String name, Integer age) {
// 給doSome方法增加一個功能,在執行之前輸出時間
System.out.println("目標方法doSome()");
}
@Override
public String doOther(String name, Integer age) {
System.out.println("目標方法doOther()");
return "a";
}
@Override
public String doFirst(String name, Integer age) {
System.out.println("目標方法doFirst()");
return "doFirst";
}
}
切面類
@Aspect
public class MyAspect {
/**
* 環繞通知方法的定義格式
* 1.public
* 2.必須有一個返回值,推薦使用Object
* 3.方法名稱自定義
* 4.方法有參數,固定的參數 ProceedingJoinPoint
*/
/**
* @Around: 環繞通知
* 屬性:value 切入點表達式
* 位置:在方法定義的上面
* 特點:
* 1.它是功能最強的通知
* 2.在目標方法的前和後都能增強功能。
* 3.控制目標方法是否被調用執行
* 4.修改原來的目標方法的執行結果。 影響最後的調用結果
*
* 環繞通知,等同於jdk動態代理的,InvocationHandler介面
*
* 參數: ProceedingJoinPoint 就等同於 Method
* 作用:執行目標方法的
* 返回值: 就是目標方法的執行結果,可以被修改。
*
* 環繞通知: 經常做事務, 在目標方法之前開啟事務,執行目標方法, 在目標方法之後提交事務
*/
// @Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
// public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
//
// Object result = null;
//
//
// System.out.println("環繞通知,在目標方法之前加入通知:現在時間:"+new Date());
//
// // 1. 目標方法調用,等同於method.invoke(); 在這裡等同於Object result = doFirst();
// result = pjp.proceed();
//
// // 2. 在目標方法前後加入功能
// System.out.println("環繞通知,在目標方法之後提交事務");
//
// // 3. 返回目標方法的執行結果
// return result;
//
// }
@Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
// 可以獲取到調用方法的參數
String name = "";
// 獲取第一個參數的值
Object args[] = pjp.getArgs();
if (args!=null && args.length > 1){
Object arg = args[0];
name = (String) arg;
}
// 實現環繞通知
Object result = null;
System.out.println("環繞通知,在目標方法之前加入通知:現在時間:"+new Date());
// 1. 目標方法調用,等同於method.invoke(); 在這裡等同於Object result = doFirst();
if ("張三".equals(name)){
// 符合條件,調用目標方法
result = pjp.proceed();
}
// 2. 在目標方法前後加入功能
System.out.println("環繞通知,在目標方法之後提交事務");
// 還可以修改目標方法的執行結果,影響方法最後的調用結果
if (result != null){
result = "修改了";
}
// 3. 返回目標方法的執行結果
return result;
}
}
測試:
public class MyTest03 {
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
// 從容器中獲取目標對象
SomeService proxy = (SomeService) ac.getBean("someService");
// 通過代理的對象執行方法,實現目標方法執行,增強了功能
String str = proxy.doFirst("張三",20);
System.out.println(str);
}
}
//環繞通知,在目標方法之前加入通知:現在時間:Mon Aug 10 20:52:07 CST 2020
// 目標方法doFirst()
// 環繞通知,在目標方法之後提交事務
// 修改了
5. @AfterThrowing 異常通知
在目標方法拋出異常後執行。該註解的 throwing 屬性用於指定所發生的異常類對象。當然,被註解為異常通知的方法可以包含一個參數 Throwable,參數名稱為 throwing 指定的名稱,表示發生的異常對象
相當於try-catch中catch裡面執行的
增加業務方法
public interface SomeService {
void doSome(String name, Integer age);
String doOther(String name, Integer age);
String doFirst(String name, Integer age);
void doSecond();
}
//實現類-----------------------------------------
@Override
public void doSecond() {
System.out.println("執行業務方法doSecond()" + (10/0));
}
切面類:
@Aspect
public class MyAspect {
/**
* 異常通知方法的定義格式
* 1.public
* 2.沒有返回值
* 3.方法名稱自定義
* 4.方法有個一個Exception, 如果還有是JoinPoint,
*/
/**
* @AfterThrowing:異常通知
* 屬性:1. value 切入點表達式
* 2. throwinng 自定義的變數,表示目標方法拋出的異常對象。
* 變數名必須和方法的參數名一樣
* 特點:
* 1. 在目標方法拋出異常時執行的
* 2. 可以做異常的監控程式, 監控目標方法執行時是不是有異常。
* 如果有異常,可以發送郵件,簡訊進行通知
*
* 執行就是:
* try{
* SomeServiceImpl.doSecond(..)
* }catch(Exception e){
* myAfterThrowing(e);
* }
*/
@AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))",
throwing = "ex")
public void myAfterThrowing(Exception ex) {
System.out.println("異常通知:方法發生異常時,執行:"+ex.getMessage());
//發送郵件,簡訊,通知開發人員
}
}
6. @After最終通知
無論目標方法是否拋出異常,該增強均會被執行
相當於try-catch-finally中finally裡面執行的
增加方法及實現
public interface SomeService {
void doSome(String name, Integer age);
String doOther(String name, Integer age);
String doFirst(String name, Integer age);
void doSecond();
void doThird();
}
//------------------------
@Override
public void doThird() {
System.out.println("執行業務方法doThird()"+ (10/0));
}
切面類
@Aspect
public class MyAspect {
/**
* 最終通知方法的定義格式
* 1.public
* 2.沒有返回值
* 3.方法名稱自定義
* 4.方法沒有參數, 如果還有是JoinPoint,
*/
/**
* @After :最終通知
* 屬性: value 切入點表達式
* 位置: 在方法的上面
* 特點:
* 1.總是會執行
* 2.在目標方法之後執行的
*
* try{
* SomeServiceImpl.doThird(..)
* }catch(Exception e){
*
* }finally{
* myAfter()
* }
*
*/
@After(value = "execution(* *..SomeServiceImpl.doThird(..))")
public void myAfter(){
System.out.println("執行最終通知,總是會被執行的程式碼");
//一般做資源清除工作的。
}
}
7. @Pointcut定義切入點
當較多的通知增強方法使用相同的 execution 切入點表達式時,編寫、維護均較為麻煩AspectJ 提供了@Pointcut 註解,用於定義 execution 切入點表達式
其用法是,將@Pointcut 註解在一個方法之上,以後所有的 execution 的 value 屬性值均可使用該方法名作為切入點。代表的就是@Pointcut 定義的切入點。這個使用@Pointcut 註解的方法一般使用 private 的標識方法,即沒有實際作用的方法
五、總結
前面的概念有些繞,看了程式碼就比較清晰了,感覺和python的裝飾器很像,只不過py的沒有這麼繞,就是為已經存在的對象添加額外的功能
總的來說就是在一個方法前或一個方法後執行一些通用的方法,提高效率,把那些業務的方法寫一塊,那些非業務的方法或那些業務方法經常使用的方法寫成切面類,使用方便還便於管理
1. 使用aspectj框架實現aop
使用aop:目的是給已經存在的一些類和方法增加額外的功能,前提是不改變原來類的程式碼
- 新建maven項目
- 加入依賴:spring依賴和aspectj依賴以及junit單元測試
- 創建目標類
- 介面和它的實現類,要做的是給類中的方法增加功能
- 創建切面類:普通類
- 在類的上面加入@Aspect
- 在類中定義方法,這個方法就是切面要執行的功能程式碼
- 在方法的上面加入aspectj中的通知註解。例如:@Before
- 還需要指定切入點表達式,execution()
- 創建spring的配置文件,聲明對象,把對象交給容器統一管理,聲明對象可以使用註解或者<bean>
- 聲明目標對象
- 聲明切面類對象
- 聲明aspectj框架中的自動代理生成器標籤,自動代理生成器:用來完成代理對象的自動創建功能
- 創建測試類
- 從spring容器中獲取目標對象(實際上就是代理對象),通過代理執行,實現aop的功能增強
2. Review
強制使用cglib代理
目標類有介面,還想用cglib代理
proxy-target-class="true" : 這句話就是告訴框架,要使用cglib動態代理
<aop:aspectj-autoproxy proxy-target-class="true" />