Spring AOP用法詳解

什麼是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表達式

執行表達式是我們開發過程中最常用的表達式。它的語法如下:


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異常通知。通常,它將被執行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章