1. 程式人生 > 實用技巧 >面向切面程式設計AOP

面向切面程式設計AOP

19面向切面程式設計AOP

轉載連結

一、Spring AOP簡介

AOP即 Aspect Oriented Program 面向切面程式設計。首先,在面向切面程式設計的思想裡面,把功能分為核心業務功能和周邊業務功能。

  • 所謂核心業務:比如登陸、增加資料、刪除資料都叫核心業務
  • 所謂周邊功能:比如效能統計、日誌、事務管理等等

周邊功能在Spring的面向切面程式設計思想裡,被定義為切面。

在面向切面程式設計思想裡,核心業務和切面功能分別獨立進行開發,然後把切面功能和核心業務功能"編織"在一起,這就叫AOP。

1.1 AOP的目的

AOP能夠將那些與業務無關,卻為業務模組所共同呼叫的邏輯或責任(例如事務管理、日誌管理、許可權控制等)封裝起來,便於減少系統的重複程式碼,降低模組間的耦合度,並有利於未來的可擴充套件性和可維護性。

1.2 AOP中的概念

概念 含義
Aspect 切面。周邊功能
Join Point 連線點,Spring AOP裡總是代表一次方法執行。可以說目標物件中的方法就是一個連線點
Advice 通知,在連線點執行的動作。在方法執行的時候(方法前、方法後、方法前後)做什麼
Pointcut 切入點,說明如何匹配連線點。即在哪些類,哪些方法上切入,連線點的集合
Introduction 引入,為現有型別宣告額外的方法和屬性
Target object 目標物件
AOP proxy AOP代理物件,可以是JDK動態代理,也可以是CGLIB代理
Weaving 織入,連線切面與目標物件或型別建立代理的過程。把切面加入到物件,並創建出代理物件的過程

1.3常用註解

• @Aspect:宣告定義切面類
• @Pointcut:宣告切點
• @Before:Advice的一種,方法執行前通知
• @After / @AfterReturning / @AfterThrowing:Advice的一種,方法執行後通知
• @Around:Advice的一種,環繞通知

1.4 舉個例子


在上面的例子中,包租婆的核心業務就是籤合同、收房租,那麼這就夠了,,灰色框起來的部分都是邊緣的事情,交給中介就好了。這就是AOP的一個思想:讓關注點程式碼與業務程式碼分離。

二、talk is cheap ,show me the code

2.1 新增依賴

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

2.2 實現切面

package com.lucky.spring.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * Created by zhangdd on 2020/8/22
 */
@Aspect
@Component
@Slf4j
public class DaoOpsAspect {

    @Pointcut("execution(* com.lucky.spring.dao..*(..))")
    private void repositoryOps() {
    }

    @Around("repositoryOps()")
    public Object logPerformance(ProceedingJoinPoint pjp) throws Throwable {
        long startTime = System.currentTimeMillis();
        String name = "-";
        String result = "Y";
        try {
            name = pjp.getSignature().toShortString();
            return pjp.proceed();
        } catch (Throwable throwable) {
            result = "N";
            throw throwable;
        } finally {
            long endTime = System.currentTimeMillis();
            log.info("{},{},{}ms", name, result, endTime - startTime);
        }
    }
    
}

  • 建立Aspect類,通過@Aspect宣告該類是一個切面
  • 使用@Component宣告該切面類是個Bean,讓Spring容器管理
  • 通過@Pointcut宣告切入點
    • 通常情況下使用execution模式可以完成絕大多數場景的設定,詳細可以參考這裡
    • 這裡宣告的切入點repositoryOps是com.lucky.spring.dao這個包下的所有類的所有方法,即sql執行方法
  • 通過@Around連線切點和切面的業務
    • 業務裡首先獲取sql執行的方法名,執行的起始時間,結束時間,最後打印出了所用的時間

2.3 核心業務實現

@Service
public class CoffeeServiceImpl implements CoffeeService {

    @Autowired
    private CoffeeMapperExt coffeeMapperExt;

    @Override
    public List<Coffee> findAllCoffee() {
        return coffeeMapperExt.queryAllCoffee();
    }
}
  • 這裡以一個查詢作為核心業務的內容

2.4 啟動服務

package com.lucky.spring;

import com.lucky.spring.model.Coffee;
import com.lucky.spring.service.CoffeeService;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.util.List;

@SpringBootApplication
@MapperScan("com.lucky.spring.dao")
public class Application implements CommandLineRunner {

    @Autowired
    private CoffeeService coffeeService;

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        List<Coffee> allCoffee = coffeeService.findAllCoffee();
        allCoffee.forEach(c -> System.out.println(c));
    }
}

列印結果如下:

2020-08-22 13:45:14.020  INFO 24567 --- [           main] com.lucky.spring.aspect.DaoOpsAspect     : CoffeeMapperExt.queryAllCoffee(),Y,332ms
Coffee(super=com.lucky.spring.model.Coffee@f6c1129, id=12, name=espresso, price=2000, createTime=Sun Aug 02 14:11:01 CST 2020, updateTime=Sun Aug 02 14:11:01 CST 2020)
Coffee(super=com.lucky.spring.model.Coffee@f897fb0, id=13, name=latte, price=2500, createTime=Sun Aug 02 14:11:01 CST 2020, updateTime=Sun Aug 02 14:11:01 CST 2020)
Coffee(super=com.lucky.spring.model.Coffee@ba2f9386, id=14, name=capuccino, price=2500, createTime=Sun Aug 02 14:11:01 CST 2020, updateTime=Sun Aug 02 14:11:01 CST 2020)
Coffee(super=com.lucky.spring.model.Coffee@5f716224, id=15, name=mocha, price=3000, createTime=Sun Aug 02 14:11:01 CST 2020, updateTime=Sun Aug 02 14:11:01 CST 2020)
Coffee(super=com.lucky.spring.model.Coffee@15642e3c, id=16, name=macchiato, price=3000, createTime=Sun Aug 02 14:11:01 CST 2020, updateTime=Sun Aug 02 14:11:01 CST 2020)
  • 從日誌中可以看出執行sql查詢的時候,列印除了sql的執行時間

三、切入點 execution 表示式語法

Spring 的AOP支援多種切點的申明方式。我們上面使用了最常用的一種 execution。
還有如下切點匹配表示式,他們的主要區別就是粒度:

  • execution:可以定義到方法的的最小粒度是引數的返回型別,修飾符,包名,類名,方法名,Spring AOP主要也是使用這個匹配表示式。
  • within:只能定義到類
  • this:當前生成的代理物件的型別匹配
  • target:目標物件型別匹配
  • args:只針對引數

使用形式如下:

@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {} 

@Pointcut("execution(* com.lucky.spring.dao..*(..))")
private void repositoryOps() {}

所以無論哪種表示式,語法還是要知道是什麼意思的。
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
除了返回型別模式(ret-type-pattern)、名稱模式(name-pattern)、引數模式(param-pattern)之外其他的都是可選的

  1. execution():表示式主體
  2. modifiers-pattern:模式修飾符 可選
  3. ret-type-pattern:返回型別模式。確定該方法的返回型別必須是什麼才能與連線點匹配。*是最常用的返回型別,它匹配任何返回型別。
  4. declaring-type-pattern:宣告型別模式 可選
  5. name-pattern:名稱模式 ,名稱是與方法名匹配的,可以使用萬用字元*作用名稱的全部或一部分
  6. param-pattern:引數模式,引數模式稍微複雜一些
    1. ()匹配不帶引數的方法
    2. (..)匹配任意數量(零個或多個)的引數
    3. (*)模式與採用任何型別的一個引數的方法匹配
    4. (*,String)與採用兩個引數的方法匹配。第一個可以是任何型別,而第二個必須是字串
  7. throws-pattern:異常模式 可選

下面是一些常見的切點表示式:

    @Pointcut("execution(* com.lucky.spring.dao..*(..))")
    private void repositoryOps() {
    }

  • 第一個*代表 返回型別模式ret-type-pattern,*表示匹配所有返回型別
  • com.lucky.spring.dao..代表declaring-type-pattern,這裡的兩個..代表當前com.lucky.spring.dao包及其子包
  • 第二個*表示方法名,*表示匹配所有方法
  • (..)表示任意數量的引數

execution(public * *(..))

返回值型別是public的方法


 execution(* set*(..))

以set開頭的方法


 execution(* com.xyz.service.AccountService.*(..))

AccountService類中的所有方法


 execution(* com..*.*Dao.find*(..))

匹配包名字首為com的任何包下類名字尾為Dao的方法,方法名必須以find為字首。如com.baobaotao.UserDao  findByUserId()、com.baobaotao.dao.ForumDao  findById()的方法都匹配切點

四、通知型別

上面的例子中使用了環繞通知,另外還有如下場景的通知型別。

  • 前置通知(Before advice):在某連線點之前執行的通知,但這個通知不能阻止連線點之前的執行流程(除非它丟擲一個異常)。

  • 後置通知(After returning advice):在某連線點正常完成後執行的通知:例如,一個方法沒有丟擲任何異常,正常返回。

  • 異常通知(After throwing advice):在方法丟擲異常退出時執行的通知。

  • 最終通知(After (finally) advice):當某連線點退出的時候執行的通知(不論是正常返回還是異常退出)。

  • 環繞通知(Around Advice):包圍一個連線點的通知,如方法呼叫。這是最強大的一種通知型別。環繞通知可以在方法呼叫前後完成自定義的行為。它也會選擇是否繼續執行連線點或直接返回它自己的返回值或丟擲異常來結束執行。