Spring-AOP詳解

image.png

前言

在日常開發(fā)中,會遇到一些對不同的功能進(jìn)行相同的一些增強(qiáng)效果,可以在原有基礎(chǔ)上進(jìn)行相應(yīng)的修改,但是會造成大量的代碼重復(fù),并且對于三方或者一些其他人寫的不能更改的類,無法來實(shí)現(xiàn)。這就提出了新的方法AOP來實(shí)現(xiàn)。

AOP簡介

1. 核心概念

AOP(Aspect Oriented Programming):面向切面編程,一種編程范式,指導(dǎo)開發(fā)者如何組織程序結(jié)構(gòu)。

作用:在不驚動原始設(shè)計(jì)的基礎(chǔ)上為其進(jìn)行功能增強(qiáng)。

Spring理念:無入侵式/無侵入式。

1.1 連接點(diǎn)(JoinPoint)

程序執(zhí)行過程中的任意位置,粒度為執(zhí)行方法、拋出異常、設(shè)置變量等。

SpringAOP中,理解為方法的執(zhí)行。

1.2 切入點(diǎn)(Pointcut)

匹配連接點(diǎn)的式子。

SpringAOP中,一個(gè)切入點(diǎn)可以只描述一個(gè)具體方法,也可以匹配多個(gè)方法。

一個(gè)具體方法:com.hao.dao包下的BookDao接口中的無形參無返回值的save()方法。
匹配多個(gè)方法:所有的save()方法,所有的以get開頭的方法,所有以Dao結(jié)尾的接口中的任意方法,所有帶有一個(gè)參數(shù)的方法。

1.3 通知(Advice)

在切入點(diǎn)處執(zhí)行的操作,也就是共性功能。

SpringAOP中,功能最終以方法的形式呈現(xiàn)。

1.4 通知類

定義通知的類。

1.5 切面(Aspect)

描述通知與切入點(diǎn)的對應(yīng)關(guān)系。

快速入門

1. 快速入門

使用注解開發(fā)模式模擬測定接口開始執(zhí)行時(shí)間。這里直接在方法運(yùn)行前打印當(dāng)前時(shí)間即可。

1.1 導(dǎo)入AOP相關(guān)坐標(biāo)

pom.xml中導(dǎo)入如下坐標(biāo)。

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.9</version>
</dependency>

1.2 制作連接點(diǎn)方法(Dao接口與實(shí)現(xiàn)類)

定義一個(gè)模擬接口和實(shí)現(xiàn)類。

public interface BookDao {
    public void save();
    public void update();
}
@Repository
public class BookDaoImpl implements BookDao {
    @Override
    public void save() {
        System.out.println(System.currentTimeMillis());
        System.out.println("book dao save ...");
    }
    @Override
    public void update() {
        System.out.println("book dao update ...");
    }
}

1.3 制作共性功能(通知類與通知)

使用@Component@Aspect注解。

@Component
@Aspect
public class MyAdvice {
}

1.4 定義切入點(diǎn)

使用@Pointcut注解。

@Component
@Aspect
public class MyAdvice {

    @Pointcut("execution(void com.hao.dao.BookDao.update())")
    private void pt() {}
}

切入點(diǎn)定義依托一個(gè)不具有實(shí)際意義的方法進(jìn)行,即無參數(shù),無返回值,方法體無實(shí)際邏輯

1.5 綁定切入點(diǎn)與通知的關(guān)系(切面)

由于是測定方法執(zhí)行前的運(yùn)行時(shí)間,這里使用@Before注解。

@Component
@Aspect
public class MyAdvice {

    @Pointcut("execution(void com.hao.dao.BookDao.update())")
    private void pt() {}

    @Before("pt()")
    public void method() {
        System.out.println(System.currentTimeMillis());
    }
}

將方法pt()加入@Before注解注解中即完成綁定。

1.6 開啟Spring對AOP注解驅(qū)動支持

Spring配置類中添加@EnableAspectJAutoProxy注解即可。

@Configuration
@ComponentScan({"com.hao.dao", "com.hao.aop"})
@EnableAspectJAutoProxy
public class SpringConfig {
}

1.7 測試

public class App {
    public static void main(String[] args) throws IOException {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        bookDao.update();
    }
}

測試調(diào)用BookDaoupdate()方法可以看到在方法內(nèi)容前依然打印了當(dāng)前時(shí)間毫秒值。

2. AOP工作流程

2.1 Spring容器啟動

2.2 讀取所有切面配置中的切入點(diǎn)

2.3 初始化bean

判斷bean對應(yīng)的類中的方法是否匹配到任意切入點(diǎn)。
匹配失敗,創(chuàng)建對象。
匹配成功,創(chuàng)建原始對象(目標(biāo)對象)的代理對象。

2.4 獲取bean執(zhí)行方法

獲取bean,調(diào)用方法并執(zhí)行,完成操作。
獲取的bean是代理對象時(shí),根據(jù)代理對象的運(yùn)行模式運(yùn)行原始方法與增強(qiáng)的內(nèi)容,完成操作。

2.5 涉及概念

目標(biāo)對象(Target):原始功能去掉共性功能對應(yīng)的類產(chǎn)生的對象,這種對象是無法直接完成最終工作的。
代理(Proxy):目標(biāo)對象無法直接完成工作,需要對其進(jìn)行功能回填,通過原始對象的代理對象實(shí)現(xiàn)。

AOP切入點(diǎn)表達(dá)式

1. 語法格式

切入點(diǎn):要進(jìn)行增強(qiáng)的方法。
切入點(diǎn)表達(dá)式:要進(jìn)行增強(qiáng)的方法的描述方式。

1.1 示例

描述方式一:執(zhí)行com.hao.dao包下的BookDao接口中的無參數(shù)update()方法

@Pointcut("execution(void com.hao.dao.BookDao.update())")

描述方式二:執(zhí)行com.hao.dao.impl包下的BookDaoImpl類中的無參數(shù)update()方法

@Pointcut("execution(void com.hao.dao.impl.BookDaoImpl.update())")

1.2 標(biāo)準(zhǔn)格式

動作關(guān)鍵字(訪問修飾符 返回值 包名.類/接口名.方法名(參數(shù))異常名)

@Pointcut("execution(public User com.hao.service.UserService.findById(int))")
  1. 動作關(guān)鍵字:描述切入點(diǎn)的行為動作,例如execution表示執(zhí)行到指定切入點(diǎn)。
  2. 訪問修飾符:public,private等,可以省略。
  3. 返回值
  4. 包名
  5. 類/接口名
  6. 方法名
  7. 參數(shù)
  8. 異常名:方法定義中拋出指定異常,可以省略。

2. 通配符

可以使用通配符描述切入點(diǎn),快速描述。

2.1 *

單個(gè)獨(dú)立的任意符號,可以獨(dú)立出現(xiàn),也可以作為前綴或者后綴匹配符出現(xiàn)。

例:匹配com.hao包下的任意包中的UserService類或接口中所有find開頭的帶有一個(gè)參數(shù)的方法。

@Pointcut("execution(public * com.hao.*.UserService.find* (*))")

2.2 ..

多個(gè)連續(xù)的任意符號,可以獨(dú)立出現(xiàn),常用于簡化包名與參數(shù)的書寫

例:匹配com包下的任意包中的UserService類或接口中所有名稱為findById的方法

@Pointcut("execution(public User com..UserService.findById(..))")

2.3 +

專用于匹配子類類型

@Pointcut("execution(* *..*Service+.* (..))")

3. 書寫技巧

  1. 所有代碼按照標(biāo)準(zhǔn)規(guī)范開發(fā),否則以下技巧全部失效。
  2. 描述切入點(diǎn)通常描述接口,而不描述實(shí)現(xiàn)類。
  3. 訪問控制修飾符針對接口開發(fā)均采用public描述(可省略訪問控制修飾符描述)。
  4. 返回值類型對于增刪改類使用精準(zhǔn)類型加速匹配,對于查詢類使用*通配快速描述。
  5. 包名書寫盡量不使用..匹配,效率過低,常用*做單個(gè)包描述匹配,或精準(zhǔn)匹配。
  6. 接口名/類名書寫名稱與模塊相關(guān)的采用*匹配,例如UserService寫成*Service,綁定業(yè)務(wù)層接口名。
  7. 方法名書寫以動詞進(jìn)行精準(zhǔn)匹配,名詞采用*匹配,例如getById書寫成getBy*,selectAll書寫成select*
  8. 參數(shù)規(guī)則較為復(fù)雜,根據(jù)業(yè)務(wù)方法靈活調(diào)整。
  9. 通常不使用異常作為匹配規(guī)則。

AOP通知類型

1. 概述

AOP通知描述了抽取的共性功能,根據(jù)共性功能抽取的位置不同,最終運(yùn)行代碼時(shí)要將其加入到合理的位置。

AOP通知共分為5種類型:

  1. 前置通知
  2. 后置通知
  3. 環(huán)繞通知(重點(diǎn))
  4. 返回后通知(了解)
  5. 拋出異常后通知(了解)

2. 類型詳解

2.1 @Before

位置:通知方法定義上方。
作用:設(shè)置當(dāng)前通知方法與切入點(diǎn)之間的綁定關(guān)系,當(dāng)前通知方法在原始切入點(diǎn)方法前運(yùn)行。
相關(guān)屬性:value(默認(rèn)):切入點(diǎn)方法名,格式為類名.方法名()

例:

    @Before("pt()")
    public void before() {
        System.out.println("before advice ...");
    }

2.2 @After

位置:通知方法定義上方。
作用:設(shè)置當(dāng)前通知方法與切入點(diǎn)之間的綁定關(guān)系,當(dāng)前通知方法在原始切入點(diǎn)方法后運(yùn)行。
相關(guān)屬性:value(默認(rèn)):切入點(diǎn)方法名,格式為類名.方法名()。

例:

    @After("pt()")
    public void after() {
        System.out.println("after advice ...");
    }

2.3 @Around(重點(diǎn),常用)

位置:通知方法定義上方。
作用:設(shè)置當(dāng)前通知方法與切入點(diǎn)之間的綁定關(guān)系,當(dāng)前通知方法在原始切入點(diǎn)方法前后運(yùn)行。

例:

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice ...");
        // 表示對原始操作的調(diào)用
        Object ret = pjp.proceed();
        System.out.println("around after advice ...");
        return ret;
    }

注意:

  1. 環(huán)繞通知必須依賴形參ProceedingJoinPoint才能實(shí)現(xiàn)對原始方法的調(diào)用,進(jìn)而實(shí)現(xiàn)原始方法調(diào)用前后同時(shí)添加通知。
  2. 通知中如果未使用ProceedingJoinPoint對原始方法進(jìn)行調(diào)用將跳過原始方法的執(zhí)行。
  3. 對原始方法的調(diào)用可以不接收返回值,通知方法設(shè)置成void即可,如果接收返回值,必須設(shè)定為Object類型。
  4. 原始方法的返回值如果是void類型,通知方法的返回值類型可以設(shè)置成void,也可以設(shè)置成Object。
  5. 由于無法預(yù)知原始方法運(yùn)行后是否會拋出異常,因此環(huán)繞通知方法必須拋出Throwable對象。

2.4 @AfterReturning(了解)

位置:通知方法定義上方。
作用:設(shè)置當(dāng)前通知方法與切入點(diǎn)之間的綁定關(guān)系,當(dāng)前通知方法在原始切入點(diǎn)方法正常執(zhí)行完畢后運(yùn)行 。
相關(guān)屬性:value(默認(rèn)):切入點(diǎn)方法名,格式為類名.方法名()。

例:

    @AfterReturning("pt()")
    public void afterReturning() {
        System.out.println("afterReturning advice ...");
    }

2.5 @AfterThrowing(了解)

位置:通知方法定義上方。
作用:設(shè)置當(dāng)前通知方法與切入點(diǎn)之間的綁定關(guān)系,當(dāng)前通知方法在原始切入點(diǎn)方法運(yùn)行拋出異常后執(zhí)行 。
相關(guān)屬性:value(默認(rèn)):切入點(diǎn)方法名,格式為類名.方法名()。

例:

    @AfterThrowing("pt()")
    public void afterThrowing() {
        System.out.println("afterThrowing advice ...");
    }

AOP通知獲取數(shù)據(jù)

1. AOP通知獲取參數(shù)數(shù)據(jù)

1.1 JoinPoint

適用于前置、后置、返回后、拋出異常后通知。

JoinPoint對象描述了連接點(diǎn)方法的運(yùn)行狀態(tài),可以獲取到原始方法的調(diào)用參數(shù)。

@Before("pt()")
public void before(JoinPoint jp) {
    Object[] args = jp.getArgs();
    System.out.println(Arrays.toString(args));
}

1.2 ProceedJointPoint

適用于環(huán)繞通知。

ProceedJointPoint是JoinPoint的子類。

@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    Object[] args = pjp.getArgs();
    System.out.println(Arrays.toString(args));
    // 表示對原始操作的調(diào)用
    Object ret = pjp.proceed();
    return ret;
}

2. AOP通知獲取返回值數(shù)據(jù)

2.1 返回后通知

返回后通知可以獲取切入點(diǎn)方法中的返回值信息,使用形參可以接收對應(yīng)的返回值對象。

@AfterReturning(value = "pt()", returning = "ret")
public void afterReturning(Object ret) {
    System.out.println("afterReturning advice ..." + ret);
}

2.2 環(huán)繞通知

環(huán)繞通知中可以手工書寫對原始方法的調(diào)用,得到的結(jié)果即為原始方法的返回值。

@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    Object ret = pjp.proceed();
    return ret;
}

3. AOP通知獲取異常數(shù)據(jù)(了解)

3.1 拋出異常后通知

拋出異常后通知可以獲取切入點(diǎn)方法中出現(xiàn)的異常信息,使用形參可以接收對應(yīng)的異常對象。

@AfterThrowing(value = "pt()", throwing = "t")
public void afterThrowing(Throwable t) {
    System.out.println("afterThrowing advice ..." + t);
}

3.2 環(huán)繞通知

環(huán)繞通知中可以手工書寫對原始方法的調(diào)用,使用try-catch可以獲取對應(yīng)的異常。

@Around("pt()")
public Object around(ProceedingJoinPoint pjp) {
    Object ret = null;
    try {
        ret = pjp.proceed();
    } catch (Throwable e) {
        e.printStackTrace();
    }
    return ret;
}

總結(jié)

以上就是關(guān)于SpringAOP的全部內(nèi)容。
通過上邊的內(nèi)容可以知道環(huán)繞型通知屬于重點(diǎn),在開發(fā)中會經(jīng)常用到。

如果有什么問題,我們可以一起交流討論解決。

最后,希望可以幫助到有需要的碼友。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容