Spring系列.AOP使用

程式設計師自由之路發表於2020-06-16

AOP簡介

利用物件導向的方法可以很好的組織程式碼,也可以繼承的方式實現程式碼重用。但是專案中總是會出現一些重複的程式碼,並且不太方便使用繼承的方式把他們重用管理起來,比如說通用日誌列印,事務處理和安全檢查等。我們可以將這些程式碼封裝起來,做成通用模組,但是還是需要在程式碼中每處需要的地方進行顯示呼叫,使用起來不方便。這是時候就是利用AOP的時候。

AOP是一種程式設計正規化,用來解決特定的問題,不能解決所有問題,可以看做是OOP的補充,常見的程式設計正規化還有:

  • 程式導向程式設計;
  • 物件導向程式設計;
  • 面向函式程式設計(函數語言程式設計);
  • 事件驅動程式設計(GUI開發中比較常見);
  • 面向切面程式設計

AOP的常見使用場景

  • 效能監控,在方法呼叫前後記錄呼叫時間,方法執行太長或超時報警;
  • 快取代理,快取某方法的返回值,下次執行該方法時,直接從快取裡獲取;
  • 軟體破解,使用AOP修改軟體的驗證類的判斷邏輯;
  • 記錄日誌,在方法執行前後記錄系統日誌;
  • 工作流系統,工作流系統需要將業務程式碼和流程引擎程式碼混合在一起執行,那麼我們可以使用AOP將其分離,並動態掛接業務;
  • 許可權驗證,方法執行前驗證是否有許可權執行當前方法,沒有則丟擲沒有許可權執行異常,由業務程式碼捕捉;
  • 事務處理 。

Spring AOP相關概念

  • AOP:這種在執行時(或者編譯時或者載入時),動態地將某些公共程式碼切入到類的指定方法、指定位置上的程式設計思想就是面向切面的程式設計;
  • 切面(Aspect):A modularization of a concern that cuts across multiple classes。在Spring中切面就是一個標註@AspectJ的類,不要想得太複雜;
  • 連線點(Joinpoint):方法執行過程中的某個點,是在應用執行過程中能夠插入切面的一個點。這個點可以是呼叫方法時、丟擲異常時、甚至修改一個欄位時。切面程式碼可以利用這些點插入到應用的正常流程之中,並新增新的行為;
  • 通知(advice):描述切面要完成什麼工作,以及在什麼時間點進行工作;
  • Pointcut:用來匹配一組連線點,並且pointcut會關聯advice,在pointcut匹配的連線點執行的時候,advice程式碼會被執行;
  • Introduction
  • Target object:被織入切面的物件;
  • AOP proxy : 包裝了切面程式碼和target程式碼的物件,Spring中支援JDK動態代理和
    CGLIB,預設使用JDK動態代理,但是如果被代理的類沒有實現介面,或者使用者強制使用CGLIB,那麼Spring會使用CGLIB代理;
  • Weaving:將切面程式碼新增到目的碼的過程,織入的型別有編譯時織入,載入時織入和執行時織入(Spring是執行時織入)

SpringAOP可以應用5種型別的通知:

  • 前置通知(Before):在目標方法被呼叫之前呼叫通知功能。
  • 後置通知(After):在目標方法完成之後呼叫通知,此時不會關心方法的輸出是什麼。(不管執行是否成功都執行都執行)
  • 返回通知(After-returning):在目標方法成功執行之後呼叫通知。
  • 異常通知(After-throwing):在目標方法丟擲異常後呼叫通知。
  • 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法呼叫之前和呼叫之後執行自定義的行為。

Spring AOP相關

開啟Aop


//自動選擇合適的AOP代理
//傳統xml這樣配置:<aop:aspectj-autoproxy/>

//exposeProxy = true屬性設定成true,意思是將動態生成的代理類expose到AopContext的ThreadLocal執行緒
//可以通過AopContext.currentProxy();獲取到生成的動態代理類。

//proxyTargetClass屬性設定動態代理使用JDK動態代理還是使用CGlib代理,設定成true是使用CGlib代理,false的話是使用JDK動態代理

//注意:如果使用Spring Boot的話,下面的配置可以不需要。AopAutoConfiguration這個自動配置類中已經自動開啟了AOP
//預設使用CGLIB動態代理,Spring Boot配置的優先順序高於下面的配置

@Configuration
@EnableAspectJAutoProxy(exposeProxy = true,proxyTargetClass = false)
public class AopConfig {

}


如果使用傳統的配置方式的話,可按如下配置開啟AOP功能。

<aop:aspectj-autoproxy/>

定義一個Aspect

Aspects (classes annotated with @Aspect) can have methods and fields, the same as any other class. They can also contain pointcut, advice, and introduction (inter-type) declarations.

可以使用普通Bean的定義方式,或者加@Aspect註解的方式定義。一旦一個類被標註成切面類,它就不會成為其他切面的代理物件。

定義一個PointCut

切面表示式可以由指示器,萬用字元和運算子組成

  1. 指示器(Designators)
  • 匹配方法 execution() (重點掌握...)
  • 匹配註解 @target() @args() @within() @annotation()
  • 匹配包/型別 within()
  • 匹配物件 this() bean() target()
  • 匹配引數 args()
  1. Wildcards(萬用字元)
  • *匹配任意數量的字元
  • +匹配指定類及其子類
  • .. 一般用於匹配任意引數的子包或引數
  1. Operators(運算子)
  • && 與操作符
  • || 或操作符
  • ! 非操作符

下面給出一個定義PointCut的例子

package com.csx.demo.spring.boot.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class MyAspect {

    //PointCut匹配的方法必須是Spring中bean的方法
    //Pointcut可以有下列方式來定義或者通過&& || 和!的方式進行組合.
    //下面定義的這些切入點就可以通過&& ||組合

    private static Logger logger = LoggerFactory.getLogger(MyAspect.class);

    //*:代表方法的返回值可以是任何型別
    //整個表示式匹配controller包下面任何的的echo方法,方法入參樂意是任意
    @Pointcut("execution(* com.csx.demo.spring.boot.controller.*.echo(..))")
    public void pointCut1(){}

    //代表echo方法必須有一個引數 引數的型別可以是任意型別
    @Pointcut("execution(* com.csx.demo.spring.boot.controller.*.echo(*))")
    public  void pointCut2(){}

    //代表echo方法必須有兩個引數,第一個型別任意,第二個型別必須是String
    @Pointcut("execution(* com.csx.demo.spring.boot.controller.*.echo(*,String))")
    public void pointCut3(){}

    //contrller包及其子包下面的任意類的任意方法
    //需要注意的是with和@with都是正對包級別的
    @Pointcut("within(com.csx.demo.spring.boot.controller..*)")
    public void pointCut4(){}

    //使用RestController這個註解標註任意類的任意方法
    @Pointcut("@within(org.springframework.web.bind.annotation.RestController)")
    public void pointCut5(){}

    //用法和@Within類似
    @Pointcut("@target(org.springframework.web.bind.annotation.RestController)")
    public void pointCut10(){}

    //MyService這個介面實現類的任何方法
    //如果MyService是一個類的話,那匹配這個類內部的所有方法
    @Pointcut("this(com.csx.demo.spring.boot.service.MyService)")
    public void pointCut6(){}

    @Pointcut("this(com.csx.demo.spring.boot.service.MyServiceImpl)")
    public void pointCut7(){}

    //某個bean內部的所有方法
    @Pointcut("bean(myServiceImpl)")
    public void pointCut8(){}

    //@within和@target針對類的註解,@annotation是針對方法的註解
    //匹配任何標註GetMaping註解的方法
    @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
    public void pointCut9(){}

    //匹配只有一個引數,引數型別是String的方法
    @Pointcut("args(String)")
    public void pointCut11(){}


    @Before("pointCut1()")
    public void befor(){
        logger.info("前置通知vvvv...");
        logger.info("我要做些事情...");
    }

    @After("pointCut1()")
    public void after(){
        logger.info("後置通知");
    }

    @AfterReturning("pointCut1()")
    public void afterReturn(){
       logger.info("後置返回");
    }

     //目標方法丟擲相關異常後通知
    @AfterThrowing("pointCut1()")
    public void afterThrowing(){
        logger.info("後置異常");
    }

    @Around("pointCut1()")
    public void around(ProceedingJoinPoint point) throws Throwable {
        logger.info("環繞通知...");
        logger.info("我要做些事情...");
        point.proceed();
        logger.info("結束環繞通知");
    }

}

相關文章