什麼是AOP
AOP(Aspect-Oriented Programming,面向方面編程),可以說是OOP(Object-Oriented Programing,面向對象編程)的補充和完善。OOP引入封裝、繼承和多態性等概念來建立一種對象層次結構,用以模擬公共行爲的一個集合。當我們需 要爲分散的對象引入公共行爲的時候,OOP則顯得無能爲力。也就是說,OOP允許你定義從上到下的關係,但並不適合定義從左到右的關係。例如日誌功能。報告代碼往往分散在各個級別,不與該對象的基本功能有關,其他類型的代碼也同樣適用,例如安全、不尋常的處理和持續的透明度。這種 散佈在各處的無關的代碼被稱爲橫切(cross-cutting)代碼,在OOP設計中,它導致了大量代碼的重複,而不利於各個模塊的重用。
而AOP技術則恰恰相反,它使用一種稱爲橫切的技術來分解封裝的對象,並將影響多個類的常見行爲封裝到一個可重用模塊中,並將其名爲 “Aspect”,即方面。所謂“方面”,簡單地說,就是將那些與業務無關,卻爲業務模塊所共同調用的邏輯或責任封裝起來,它易於減少系統的重複代碼,降低模塊之間的耦合度,有利於未來的可操作性和可維護性。AOP代表的是一個橫向的關係,如果對象是中空圓柱體,則它封裝對象的屬性和行爲。因此,面向方面的編程就像一個鋒利的邊緣,將這些空心圓柱體切割開來獲得它們的內部信息。而切割表面就是所謂的“面”。然後,它恢復了切割表面沒有留下任何痕跡與熟練的手。
AOP的術語
1.Join point(連接點)
Spring 官方文檔的描述:
程序執行過程中的點,如方法的執行或異常的處理。在Spring AOP中,連接點總是表示方法執行。
2.Pointcut(切入點)
切入點是與連接點匹配的表達式,用於確定是否需要執行通知。Pointcut使用與連接點匹配的不同種類的表達式,Spring框架使用AspectJ切入點表達式語言
3.Advice(增強/通知)
通知指的是在攔截Joinpoint之後要做什麼。通知分爲事前通知、事後通知、異常通知、最終通知和環繞通知。
4.Aspect(切面)
Aspect切面表示Pointcut(切入點)和Advice(增強/通知)的結合
Spring AOP用法
示例代碼:
/**
* 設置登錄用戶名
*/
public class CurrentUserHolder {
private static final ThreadLocal<String> holder = new ThreadLocal<>();
public static String get() {
return holder.get();
}
public static void set(String user) {
holder.set(user);
}
}
/**
* 校驗用戶權限
*/
@Service("authService")
public class AuthServiceImpl implements AuthService {
@Override
public void checkAccess() {
String user = CurrentUserHolder.get();
if(!"admin".equals(user)) {
throw new RuntimeException("該用戶無此權限!");
}
}
}
/**
* 業務邏輯類
*/
@Service("productService")
public class ProductServiceImpl implements ProductService {
@Autowired
private AuthService authService;
@Override
public Long deleteProductById(Long id) {
System.out.println("刪除商品id爲" + id + "的商品成功!");
return id;
}
@Override
public void deleteProductByName(String name) {
System.out.println("刪除商品名稱爲" + name + "的商品成功!");
}
@Override
public void selectProduct(Long id) {
if("100".equals(id.toString())) {
System.out.println("查詢商品成功!");
} else {
System.out.println("查詢商品失敗!");
throw new RuntimeException("該商品不存在!");
}
}
}
1.使用within表達式匹配包類型
2.使用this、target、bean表達式匹配對象類型
//匹配AOP對象的目標對象爲指定類型的方法,即ProductServiceImpl的aop代理對象的方法
@Pointcut("this(com.aop.service.impl.ProductServiceImpl)")
public void matchThis() {}
//匹配實現ProductService接口的目標對象
@Pointcut("target(com.aop.service.ProductService)")
public void matchTarget() {}
//匹配所有以Service結尾的bean裏面的方法
@Pointcut("bean(*Service)")
public void matchBean() {}
3.使用args表達式匹配參數
//匹配第一個參數爲Long類型的方法
@Pointcut("args(Long, ..) ")
public void matchArgs() {}
4.使用@annotation、@within、@target、@args匹配註解
5.使用execution表達式
執行表達式是我們開發過程中最常用的表達式。它的語法如下:
modifier-pattern:用於匹配訪問修飾符,如public、private等。
ret-type-pattern:用於匹配返回值類型,不省略
declaring-type-pattern:用於匹配包類型
modifier-pattern(param-pattern):在匹配類中使用的方法不能被省略
throws-pattern:用於匹配拋出異常的方法
代碼示例:
@Component
@Aspect
public class SecurityAspect {
@Autowired
private AuthService authService;
//匹配com.aop.service.impl.ProductServiceImpl類下的方法名以delete開頭、參數類型爲Long的public方法
@Pointcut("execution(public * com.aop.service.impl.ProductServiceImpl.delete*(Long))")
public void matchCondition() {}
//使用matchCondition這個切入點進行增強
@Before("matchCondition()")
public void before() {
System.out.println("before 前置通知......");
authService.checkAccess();
}
}
單元測試:
運行結果(只有deleteProductById方法攔截成功):
查詢商品成功!
刪除商品名稱爲衣服的商品成功!
before 前置通知......
java.lang.RuntimeException: 該用戶無此權限!
at com.aop.service.impl.AuthServiceImpl.checkAccess(AuthServiceImpl.java:15)
at com.aop.security.SecurityAspect.before(SecurityAspect.java:50)
可以在多個表達式之間使用連接符匹配多個條件, 如使用||表示“或”,使用 &&表示“且”
//匹配com.aop.service.impl.ProductServiceImpl類下方法名以select或delete開頭的所有方法
@Pointcut("execution(* com.aop.service.impl.ProductServiceImpl.select*(..)) || " +
"execution(* com.aop.service.impl.ProductServiceImpl.delete*(..))")
public void matchCondition() {}
//使用matchCondition這個切入點進行增強
@Before("matchCondition()")
public void before() {
System.out.println("before 前置通知......");
authService.checkAccess();
}
單元測試:
運行結果(所有方法均攔截成功):
before 前置通知......
查詢商品成功!
before 前置通知......
刪除商品名稱爲衣服的商品成功!
before 前置通知......
刪除商品id爲100的商品成功!
6.Advice註解
Advice註解一共有5種,分別是:
①. @Before前置通知
前置通知在切入點運行前執行,不會影響切入點的邏輯
②. @After後置通知
在切入點的正常操作之後執行後通知。如果切入點引發異常,則在引發異常之前執行該異常。
③. @AfterThrowing異常通知
異常通知在切入點拋出異常前執行,如果切入點正常運行(未拋出異常),則不執行
④. @AfterReturning返回通知
返回通知在入口點正確運行之後執行,如果入口點拋出異常,則不執行。
⑤. @Around環繞通知
圓周通知是最強大的通知,您可以在執行入口點之前和之後自定義一些操作。環繞通知負責決定是繼續處理連接點(調用ProceedingJoinPoint的.方法)還是中斷執行
示例代碼:
//匹配com.aop.service.impl.ProductServiceImpl類下面的所有方法
@Pointcut("execution(* com.aop.service.impl.ProductServiceImpl.*(..))")
public void matchAll() {}
@Around("matchAll()")
public Object around(ProceedingJoinPoint joinPoint) {
Object result = null;
authService.checkAccess();
System.out.println("befor 在切入點執行前運行");
try{
result = joinPoint.proceed(joinPoint.getArgs());//獲取參數
System.out.println("after 在切入點執行後運行,result = " + result);
} catch (Throwable e) {
System.out.println("after 在切入點執行後拋出exception運行");
e.printStackTrace();
} finally {
System.out.println("finally......");
}
return result;
}
單元測試:
@Test
public void contextLoads() {
CurrentUserHolder.set("admin");
productService.deleteProductById(100L);
productService.selectProduct(10L);
}
運行結果:
在執行ProceedingJoinPoint對象的.方法之前,它等同於事前通知;在執行.方法之後,它等同於運行切入點(並且可以獲得參數);在執行該方法之後,它等同於事後通知;如果通過運行切入點引發異常catch中的內容等效於AfterThrowing異常通知;不管切入點是否引發異常,final中的內容等效於AfterThrowing異常通知。通常,它將被執行。