Spring AOP 面向切面編程

? ? ? ?AOP全稱Aspect Oriented Programming。在OOP
(面向?qū)ο蟪绦蛟O(shè)計(jì))中,正是這種分散在各處且與對象核心功能無關(guān)的代碼(橫切代碼)的存在,使得模塊復(fù)用難度增加。AOP則將封裝好的對象剖開,找出其中對多個(gè)對象產(chǎn)生影響的公共行為,并將其封裝為一個(gè)可重用的模塊,這個(gè)模塊被命名為“切面”(Aspect),切面將那些與業(yè)務(wù)無關(guān),卻被業(yè)務(wù)模塊共同調(diào)用的邏輯提取并封裝起來,減少了系統(tǒng)中的重復(fù)代碼,降低了模塊間的耦合度,同時(shí)提高了系統(tǒng)的可維護(hù)性。

? ? ? ?AOP(Aspect-OrientedProgramming,面向方面編程),可以說是OOP(Object-Oriented Programing,面向?qū)ο缶幊蹋┑难a(bǔ)充和完善。OOP引入封裝、繼承和多態(tài)性等概念來建立一種對象層次結(jié)構(gòu),用以模擬公共行為的一個(gè)集合。當(dāng)我們需要為分散的對象引入公共行為的時(shí)候,OOP則顯得無能為力。也就是說,OOP允許你定義從上到下的關(guān)系,但并不適合定義從左到右的關(guān)系。例如日志功能。日志代碼往往水平地散布在所有對象層次中,而與它所散布到的對象的核心功能毫無關(guān)系。對于其他類型的代碼,如安全性、異常處理和透明的持續(xù)性也是如此。這種散布在各處的無關(guān)的代碼被稱為橫切(cross-cutting)代碼,在OOP設(shè)計(jì)中,它導(dǎo)致了大量代碼的重復(fù),而不利于各個(gè)模塊的重用。

一 AOP編程相關(guān)名詞

  • 切面(Aspect):在代碼體系中,對象與對象之間,方法與方法之間,模塊與模塊之間都可以認(rèn)為是一個(gè)個(gè)的切面。Spring中通過@Aspect:來描述一個(gè)切面。

  • 連接點(diǎn)(Joinpoint):在程序執(zhí)行過程中某個(gè)特定的點(diǎn),比如某方法調(diào)用的時(shí)候或者處理異常的時(shí)候。在Spring AOP中,一個(gè)連接點(diǎn)總是表示一個(gè)方法的執(zhí)行。

  • 通知(Advice):在切面的某個(gè)特定的連接點(diǎn)上執(zhí)行的動(dòng)作。其中包括了“around”、“before”和“after”等不同類型的通知。許多AOP框架(包括Spring)都是以攔截器做通知模型,并維護(hù)一個(gè)以連接點(diǎn)為中心的攔截器鏈。

  • 切入點(diǎn)(Pointcut):定義在什么時(shí)候切人方法。匹配連接點(diǎn)的斷言。通知和一個(gè)切入點(diǎn)表達(dá)式關(guān)聯(lián),并在滿足這個(gè)切入點(diǎn)的連接點(diǎn)上運(yùn)行(例如,當(dāng)執(zhí)行某個(gè)特定名稱的方法時(shí))。切入點(diǎn)表達(dá)式如何和連接點(diǎn)匹配是AOP的核心:Spring缺省使用AspectJ切入點(diǎn)語法。Spring里面通過@Pointcut來引入切入點(diǎn)。

  • 引入(Introduction):用來給一個(gè)類型聲明額外的方法或?qū)傩裕ㄒ脖环Q為連接類型聲明(inter-type declaration))。Spring允許引入新的接口(以及一個(gè)對應(yīng)的實(shí)現(xiàn))到任何被代理的對象。例如,你可以使用引入來使一個(gè)bean實(shí)現(xiàn)IsModified接口,以便簡化緩存機(jī)制。

  • 目標(biāo)對象(Target Object):被一個(gè)或者多個(gè)切面所通知的對象。也被稱做被通知(advised)對象。既然Spring AOP是通過運(yùn)行時(shí)代理實(shí)現(xiàn)的,這個(gè)對象永遠(yuǎn)是一個(gè)被代理(proxied)對象。

  • AOP代理(AOP Proxy):AOP框架創(chuàng)建的對象,用來實(shí)現(xiàn)切面契約(例如通知方法執(zhí)行等等)。在Spring中,AOP代理可以是JDK動(dòng)態(tài)代理或者CGLIB代理。

  • 織入(Weaving):把切面連接到其它的應(yīng)用程序類型或者對象上,并創(chuàng)建一個(gè)被通知的對象。這些可以在編譯時(shí)(例如使用AspectJ編譯器),類加載時(shí)和運(yùn)行時(shí)完成。Spring和其他純Java AOP框架一樣,在運(yùn)行時(shí)完成織入。

二 連接點(diǎn)(Joinpoint)

? ? ? ?連接點(diǎn)是在應(yīng)用執(zhí)行過程中能夠插入切面的一個(gè)點(diǎn),這個(gè)點(diǎn)可以是調(diào)用方法時(shí),拋出異常時(shí),甚至是修改一個(gè)字段時(shí),切面代碼可以利用這些連接點(diǎn)插入到應(yīng)用的正常流程中,并添加新的行為,如日志、安全、事務(wù)、緩存等。具體代碼中,連接點(diǎn)體現(xiàn)在每個(gè)通知方法的參數(shù)中。
比如如下前置@Before通知方法的第一個(gè)參數(shù)就是連接點(diǎn),通過連接點(diǎn)我們可以獲取到一些上下文的信息。

    /**
     * 前置通知:目標(biāo)方法執(zhí)行之前執(zhí)行以下方法體的內(nèi)容
     */
    @Before(value = "operateLog()")
    public void beforeMethod(JoinPoint jp) {
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }

? ? ? ?@Before、@After、@AfterReturning、@AfterThrowing都是使用的org.aspectj.lang.JoinPoint接口表示目標(biāo)類連接點(diǎn)對象,@Around使用org.aspectj.lang.ProceedingJoinPoint表示連接點(diǎn)對象。兩個(gè)接口里面的方法也不復(fù)雜。主要方法如下。

JoinPoint接口主要方法如下

public interface JoinPoint {

    /**
     * 獲取代理對象本身
     */
    Object getThis();

    /**
     * 獲取連接點(diǎn)所在的目標(biāo)對象

     */
    Object getTarget();

    /**
     * 獲取連接點(diǎn)方法運(yùn)行時(shí)的入?yún)⒘斜?     */
    Object[] getArgs();

    /** 獲取連接點(diǎn)的方法簽名對象,進(jìn)而可以獲取方法的名字,方法修飾符這些
     */
    Signature getSignature();

    /**
     * 獲取連接點(diǎn)方法在文件中的信息,比如文件中第幾行啥的
     */
    SourceLocation getSourceLocation();

    /** 獲取連接點(diǎn)的方法的類型
     */
    String getKind();

    /**
     * 獲取封裝連接點(diǎn)的一個(gè)對象
     */
    JoinPoint.StaticPart getStaticPart();


}

ProceedingJoinPoint主要方法如下,ProceedingJoinPoint繼承自JoinPoint

public interface ProceedingJoinPoint extends JoinPoint {

    /**
     * 這個(gè)函數(shù)咱們不能直接調(diào)用,不管他
     */
    void set$AroundClosure(AroundClosure arc);

    /**
     * P通過反射執(zhí)行目標(biāo)對象的連接點(diǎn)處的方法
     */
    public Object proceed() throws Throwable;

    /**
     * 通過反射執(zhí)行目標(biāo)對象連接點(diǎn)處的方法,不過使用新的入?yún)⑻鎿Q原來的入?yún)?     */
    public Object proceed(Object[] args) throws Throwable;
}

三 通知(Advice)

? ? ? ?通知定義了切面是什么以及何時(shí)調(diào)用,何時(shí)調(diào)用。通知包含以下幾種:

Advice Spring注解 解釋
Before @Before 前置通知,在方法被調(diào)用之前調(diào)用
After @After 最終通知, 在方法完成之后調(diào)用,無論方法執(zhí)行是否成功
After-returning @AfterReturning 后置通知,在方法成功執(zhí)行之后調(diào)用
After-throwing @AfterThrowing 異常通知,在方法拋出異常后調(diào)用
Around @Around 環(huán)繞通知, 包圍一個(gè)連接點(diǎn)的通知

? ? ? ?@Before、@After、@AfterReturning、@AfterThrowing這幾個(gè)通知應(yīng)該都還好理解。就@Around稍稍復(fù)雜一點(diǎn)。

? ? ? ?環(huán)繞通知:包圍一個(gè)連接點(diǎn)的通知,如方法調(diào)用。這是最強(qiáng)大的一種通知類型。環(huán)繞通知可以在方法調(diào)用前后完成自定義的行為。它也會(huì)選擇是否繼續(xù)執(zhí)行連接點(diǎn)或直接返回它自己的返回值或拋出異常來結(jié)束執(zhí)行。

? ? ? ?環(huán)繞通知最麻煩,也最強(qiáng)大,其是一個(gè)對方法的環(huán)繞,具體方法會(huì)通過代理傳遞到切面中去,切面中可選擇執(zhí)行方法與否,執(zhí)行方法幾次等。

? ? ? ?環(huán)繞通知使用一個(gè)代理ProceedingJoinPoint類型的對象來管理目標(biāo)對象,所以此通知的第一個(gè)參數(shù)必須是ProceedingJoinPoint類型,在通知體內(nèi),調(diào)用ProceedingJoinPoint的proceed()方法會(huì)導(dǎo)致后臺(tái)的連接點(diǎn)方法執(zhí)行。proceed 方法也可能會(huì)被調(diào)用并且傳入一個(gè)Object[]對象-該數(shù)組中的值將被作為方法執(zhí)行時(shí)的參數(shù)。

四 切點(diǎn)(Pointcut)

? ? ? ?切點(diǎn)定義了何處,切點(diǎn)的定義會(huì)匹配通知所要織入的一個(gè)或多個(gè)連接點(diǎn),我們通常使用明確的類的方法名稱來指定這些切點(diǎn),或是利用正則表達(dá)式定義匹配的類和方法名稱來指定這些切點(diǎn)。

? ? ? ?切點(diǎn)(Pointcut)的使用關(guān)鍵在切點(diǎn)表達(dá)式,一般由下列方式來定義或者通過 &&、 ||、 !、 的方式進(jìn)行組合:

切入點(diǎn)表達(dá)式指示符 解釋
execution 匹配子表達(dá)式(匹配方法執(zhí)行)
within 匹配連接點(diǎn)所在的Java類或者包
this 用于向通知方法中傳入代理對象的引用
target 用于向通知方法中傳入目標(biāo)對象的引用
args 用于將參數(shù)傳入到通知方法中
@within 匹配在類一級(jí)使用了參數(shù)確定的注解的類,其所有方法都將被匹配(在類上添加注解)
@target 和@within的功能類似,但必須要指定注解接口的保留策略為RUNTIME
@args 傳入連接點(diǎn)的對象對應(yīng)的Java類必須被@args指定的Annotation注解標(biāo)注
@annotation 匹配當(dāng)前執(zhí)行方法持有指定注解的方法

咱們可以簡單的認(rèn)為,通知(Advice)定義了什么時(shí)候調(diào)用,切點(diǎn)(Pointcut)定義了哪個(gè)地方。一個(gè)指定了when,另一個(gè)指定了where。

4.1 execution

? ? ? ?"execution(方法表達(dá)式)":匹配方法執(zhí)行的連接點(diǎn)。

? ? ? ?execution方法表達(dá)式語法如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern.?name-pattern(param-pattern)throws-pattern?) 
  • 修飾符匹配(modifier-pattern?): 可選。修飾符:public、private、protected。

  • 返回值匹配(ret-type-pattern): 必填。匹配返回值類類型。也可以為*表示任何返回值。

  • 類路徑匹配(declaring-type-pattern.?): 可選。匹配類名。 (相當(dāng)于某個(gè)類下面的哪個(gè)方法。注意:后面是有一個(gè)點(diǎn)號(hào)的,然后才接的方法名)

  • 方法名匹配(name-pattern): 必填。匹配方法名 也可以為表示代表所有方法, 還可以類似set的形式,代表以set開頭的所有方法。

  • 參數(shù)匹配((param-pattern)): 必填。 匹配具體的參數(shù)類型,多個(gè)參數(shù)間用“,”隔開,各個(gè)參數(shù)也可以用“”來表示匹配任意類型的參數(shù),如(String)表示匹配一個(gè)String參數(shù)的方法;(,String) 表示匹配有兩個(gè)參數(shù)的方法,第一個(gè)參數(shù)可以是任意類型,而第二個(gè)參數(shù)是String類型;可以用(..)表示零個(gè)或多個(gè)任意參數(shù)。

  • 異常類型匹配(throws-pattern?): 可選。函數(shù)拋出異常類型匹配。

@Pointcut execution 表達(dá)式其實(shí)是很好理解的,咱們可以把他看成一個(gè)函數(shù)就好了,函數(shù)有修飾符、返回值、方法名、參數(shù)、異常。declaring-type-pattern? 和 name-pattern 共同組合匹配方法匹配方法。 如果寫了declaring-type-pattern注意,后面要帶一個(gè)點(diǎn)號(hào)。

? ? ? ?比如如下實(shí)例,匹配com.tuacy.microservice.framework.user.manage.controller包下面直接子類所有方法

/**
 * 日志AOP 切面類
 */

@Aspect
@Component("logAspect")
public class LoggingAspect {

    /**
     * 匹配com.tuacy.microservice.framework.user.manage.controller包下面直接子類所有方法。
     */
    @Pointcut("execution(* com.tuacy.microservice.framework.user.manage.controller.*.*(..))")
    public void operateLog() {
    }
    
    /**
     * 前置通知:目標(biāo)方法執(zhí)行之前執(zhí)行以下方法體的內(nèi)容
     */
    @Before(value = "operateLog()")
    public void beforeMethod(JoinPoint jp) {
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }
    
    
}

4.2 within

? ? ? ?"within(類型表達(dá)式)":匹配連接點(diǎn)所在的Java類或者包。within()函數(shù)定義的連接點(diǎn)是針對目標(biāo)類而言的。with所指定的連接點(diǎn)最小范圍是類。所以within能實(shí)現(xiàn)的功能,execution也能實(shí)現(xiàn)。

/**
 * 日志AOP 切面類
 */

@Aspect
@Component("logAspect")
public class LoggingAspect {


    /**
     * 匹配com.tuacy.microservice.framework.user.manage.controller包下直接子類所有方法。
     */
    @Pointcut("within(com.tuacy.microservice.framework.user.manage.controller.*)")
    public void operateLog() {
    }


    /**
     * 前置通知:目標(biāo)方法執(zhí)行之前執(zhí)行以下方法體的內(nèi)容
     */
    @Before(value = "operateLog()")
    public void beforeMethod(JoinPoint jp) {
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }


}

匹配 com.tuacy.microservice.framework.user.manage.controller包下面,子類的所有方法。(不包含子孫包,如果想包含子孫包需要改為:within(com.tuacy.microservice.framework.user.manage.controller..*))。

? ? ? ?在比如如果A繼承了接口B,則within("B")不會(huì)匹配到A,但是within("B+")可以匹配到A。

? ? ? ?在比如一個(gè),匹配 所有添加了com.tuacy.microservice.framework.user.manage.annotation.LogginAnnotation注解的方法。

    @Pointcut("within(@com.tuacy.microservice.framework.user.manage.annotation.LogginAnnotation *)")
    public void operateLog() {
    }

4.3 this

? ? ? ?this 向通知方法中傳入代理對象的引用。

/**
 * 日志AOP 切面類
 */

@Aspect
@Component("logAspect")
public class LoggingAspect {

    /**
     * 匹配UserController類所有的方法
     */
    @Pointcut("execution(* com.tuacy.microservice.framework.user.manage.controller.UserController.*(..))")
    public void operateLog() {
    }

    /**
     * 前置通知:目標(biāo)方法執(zhí)行之前執(zhí)行以下方法體的內(nèi)容, 通過this傳入了代理對象的引用
     */
    @Before(value = "operateLog() && this(param)")
    public void beforeMethod(JoinPoint jp, UserController param) {
        System.out.println(param.toString());
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }

}

4.4 target

? ? ? ?target 向通知方法中傳入目標(biāo)對象的引用。

/**
 * 日志AOP 切面類
 */

@Aspect
@Component("logAspect")
public class LoggingAspect {

    /**
     * 匹配UserController類所有的方法
     */
    @Pointcut("execution(* com.tuacy.microservice.framework.user.manage.controller.UserController.*(..))")
    public void operateLog() {
    }

    /**
     * 前置通知:目標(biāo)方法執(zhí)行之前執(zhí)行以下方法體的內(nèi)容, 通過target傳入了目標(biāo)對象的引用
     */
    @Before(value = "operateLog() && target(param)")
    public void beforeMethod(JoinPoint jp, UserController param) {
        System.out.println(param.toString());
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }

}

4.5 args

? ? ? ?args 將參數(shù)傳入到通知方法中。如果有多個(gè)參數(shù)逗號(hào)隔開。

切面

/**
 * 日志AOP 切面類
 */

@Aspect
@Component("logAspect")
public class LoggingAspect {

    /**
     * 匹配UserController類所有的方法
     */
    @Pointcut("execution(* com.tuacy.microservice.framework.user.manage.controller.UserController.*(..))")
    public void operateLog() {
    }

    /**
     * 前置通知:目標(biāo)方法執(zhí)行之前執(zhí)行以下方法體的內(nèi)容, 通過args傳入了目標(biāo)方法的參數(shù)
     */
    @Before(value = "operateLog() && args(param)")
    public void beforeMethod(JoinPoint jp, UserParam param) {
        System.out.println(param.toString());
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }

}

目標(biāo)

@RestController
public class UserController extends BaseController implements IUserControllerApi {

    private IUserService userService;

    @Autowired
    public void setUserService(IUserService userService) {
        this.userService = userService;
    }

    public ResponseDataEntity<UserInfoEntity> getUser(@RequestBody UserParam param) {
        ResponseDataEntity<UserInfoEntity> responseDataEntity = new ResponseDataEntity<>();
        try {
            responseDataEntity.setMsg(ResponseResultType.SUCCESS.getDesc());
            responseDataEntity.setStatus(ResponseResultType.SUCCESS.getValue());
            responseDataEntity.setData(userService.getUserInfo());
        } catch (Exception e) {
            e.printStackTrace();
        }

        return responseDataEntity;
    }
}

4.6 @within

? ? ? ?"@within(注解類型)":匹配類上添加了指定注解的該類的所有方法;注解類型也必須是全限定類型名。比如如下實(shí)例會(huì)匹配到所有添加了ClassAnnotation注解的類的所有方法。

/**
 * 日志AOP 切面類
 */

@Aspect
@Component("logAspect")
public class LoggingAspect {


    /**
     * 匹配所有添加了ClassAnnotation注解的方法
     */
    @Pointcut("@within(com.tuacy.microservice.framework.user.manage.annotation.ClassAnnotation)")
    public void operateLog() {
    }

    /**
     * 前置通知:目標(biāo)方法執(zhí)行之前執(zhí)行以下方法體的內(nèi)容
     */
    @Before(value = "operateLog()")
    public void beforeMethod(JoinPoint jp) {
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }

}

4.7 @target

? ? ? ?"@target(注解類型)":@within的功能類似,但必須要指定注解接口的保留策略為RUNTIME。注解類型也必須是全限定類型名。

? ? ? ?關(guān)于 @target的使用,編寫的實(shí)例代碼一直會(huì)報(bào)錯(cuò)。不曉得為啥。代碼如下:

/**
 * 日志AOP 切面類
 */

@Aspect
@Component("logAspect")
public class LoggingAspect {


    /**
     * 匹配所有添加了ClassAnnotation注解的方法
     */
    @Pointcut("@target(com.tuacy.microservice.framework.user.manage.annotation.ClassAnnotation)")
    public void operateLog() {
    }

    /**
     * 前置通知:目標(biāo)方法執(zhí)行之前執(zhí)行以下方法體的內(nèi)容
     */
    @Before(value = "operateLog()")
    public void beforeMethod(JoinPoint jp) {
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }

}
/**
 * 添加在類上的注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface ClassAnnotation {
    public String value() default "";
}

4.8 @args

? ? ? ?"@args(注解列表)":匹配當(dāng)前執(zhí)行的方法傳入的參數(shù)持有指定注解。注解類型也必須是全限定類型名。

? ? ? ?關(guān)于@args的使用,代碼也是報(bào)錯(cuò),不曉得為啥。

/**
 * 日志AOP 切面類
 */

@Aspect
@Component("logAspect")
public class LoggingAspect {


    /**
     * 匹配所有方法參數(shù)添加了RequestBody注解的方法
     */
    @Pointcut("@args(org.springframework.web.bind.annotation.RequestBody)")
    public void operateLog() {
    }

    /**
     * 前置通知:目標(biāo)方法執(zhí)行之前執(zhí)行以下方法體的內(nèi)容
     */
    @Before(value = "operateLog()")
    public void beforeMethod(JoinPoint jp) {
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }

}

4.9 @annotation

? ? ? ?"@annotation(注解類型)":匹配所有添加了指定注解類型的方法。注解類型也必須是全限定類型名。比如下面的實(shí)例會(huì)匹配到所有添加了OperateLogAnnotation注解的方法。

@Aspect
@Component("logAspect")
public class LoggingAspect {

    /**
     * 匹配所有添加了LoggingAnnotation注解的方法
     */
    @Pointcut("@annotation(com.tuacy.microservice.framework.user.manage.annotation.LoggingAnnotation)")
    public void operateLog() {
    }

    /**
     * 前置通知:目標(biāo)方法執(zhí)行之前執(zhí)行以下方法體的內(nèi)容
     */
    @Before(value = "operateLog()")
    public void beforeMethod(JoinPoint jp) {
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }

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

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

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