Spring Boot實踐——AOP實現

AOP介紹

一、AOP

  AOP(Aspect Oriented Programming),即面向切面編程,可以說是OOP(Object Oriented Programming,面向對象編程)的補充和完善。OOP引入封裝、繼承、多態等概念來建立一種對象層次結構,用於模擬公共行爲的一個集合。不過OOP允許開發者定義縱向的關係,但並不適合定義橫向的關係,例如日誌功能。日誌代碼往往橫向地散佈在所有對象層次中,而與它對應的對象的核心功能毫無關係對於其他類型的代碼,如安全性、異常處理和透明的持續性也都是如此,這種散佈在各處的無關的代碼被稱爲橫切(cross cutting),在OOP設計中,它導致了大量代碼的重複,而不利於各個模塊的重用。

AOP技術恰恰相反,它利用一種稱爲"橫切"的技術,剖解開封裝的對象內部,並將那些影響了多個類的公共行爲封裝到一個可重用模塊,並將其命名爲"Aspect",即切面。所謂"切面",簡單說就是那些與業務無關,卻爲業務模塊所共同調用的邏輯或責任封裝起來,便於減少系統的重複代碼,降低模塊之間的耦合度,並有利於未來的可操作性和可維護性。

使用"橫切"技術,AOP把軟件系統分爲兩個部分:核心關注點橫切關注點。業務處理的主要流程是核心關注點,與之關係不大的部分是橫切關注點。橫切關注點的一個特點是,他們經常發生在覈心關注點的多處,而各處基本相似,比如權限認證、日誌、事物。AOP的作用在於分離系統中的各種關注點,將核心關注點和橫切關注點分離開來。

  AOP(Aspect Orient Programming),我們一般稱爲面向方面(切面)編程,作爲面向對象的一種補充,用於處理系統中分佈於各個模塊的橫切關注點,比如事務管理、日誌、緩存等等。AOP實現的關鍵在於AOP框架自動創建的AOP代理,AOP代理主要分爲靜態代理和動態代理,靜態代理的代表爲AspectJ;而動態代理則以Spring AOP爲代表。

  與AspectJ的靜態代理不同,Spring AOP使用的動態代理,所謂的動態代理就是說AOP框架不會去修改字節碼,而是在內存中臨時爲方法生成一個AOP對象,這個AOP對象包含了目標對象的全部方法,並且在特定的切點做了增強處理,並回調原對象的方法。

  Spring AOP中的動態代理主要有兩種方式,JDK動態代理和CGLIB動態代理。JDK動態代理通過反射來接收被代理的類,並且要求被代理的類必須實現一個接口。JDK動態代理的核心是InvocationHandler接口和Proxy類。

  如果目標類沒有實現接口,那麼Spring AOP會選擇使用CGLIB來動態代理目標類。CGLIB(Code Generation Library),是一個代碼生成的類庫,是利用asm開源包,可以在運行時動態的生成某個類的子類。注意,CGLIB是通過繼承的方式做的動態代理,因此如果某個類被標記爲final,那麼它是無法使用CGLIB做動態代理的。

二、AOP核心概念

1、橫切關注點

對哪些方法進行攔截,攔截後怎麼處理,這些關注點稱之爲橫切關注點

2、切面(aspect)

類是對物體特徵的抽象,切面就是對橫切關注點的抽象

3、連接點(joinpoint)

被攔截到的點,因爲Spring只支持方法類型的連接點,所以在Spring中連接點指的就是被攔截到的方法,實際上連接點還可以是字段或者構造器

4、切入點(pointcut)

對連接點進行攔截的定義

5、通知(advice)

所謂通知指的就是指攔截到連接點之後要執行的代碼,通知分爲前置、後置、異常、最終、環繞通知五類

6、目標對象

代理的目標對象

7、織入(weave)

將切面應用到目標對象並導致代理對象創建的過程

8、引入(introduction)

在不修改代碼的前提下,引入可以在運行期爲類動態地添加一些方法或字段

 

三、Spring對AOP的支持

  Spring中AOP代理由Spring的IOC容器負責生成、管理,其依賴關係也由IOC容器負責管理。因此,AOP代理可以直接使用容器中的其它bean實例作爲目標,這種關係可由IOC容器的依賴注入提供。Spring創建代理的規則爲:

1、默認使用Java動態代理來創建AOP代理,這樣就可以爲任何接口實例創建代理了

2、當需要代理的類不是代理接口的時候,Spring會切換爲使用CGLIB代理,也可強制使用CGLIB

AOP編程其實是很簡單的事情,縱觀AOP編程,程序員只需要參與三個部分:

1、定義普通業務組件

2、定義切入點,一個切入點可能橫切多個業務組件

3、定義增強處理,增強處理就是在AOP框架爲普通業務組件織入的處理動作

所以進行AOP編程的關鍵就是定義切入點和定義增強處理,一旦定義了合適的切入點和增強處理,AOP框架將自動生成AOP代理,即:代理對象的方法=增強處理+被代理對象的方法。

實現方式

Spring除了支持Schema方式配置AOP,還支持註解方式:使用@AspectJ風格的切面聲明。

一、Aspectj介紹

@AspectJ 作爲通過 Java 5 註釋註釋的普通的 Java 類,它指的是聲明 aspects 的一種風格。

AspectJ是靜態代理的增強,所謂的靜態代理就是AOP框架會在編譯階段生成AOP代理類,因此也稱爲編譯時增強。

AspectJ: 基於字節碼操作(Bytecode Manipulation),通過編織階段(Weaving Phase),對目標Java類型的字節碼進行操作,將需要的Advice邏輯給編織進去,形成新的字節碼。畢竟JVM執行的都是Java源代碼編譯後得到的字節碼,所以AspectJ相當於在這個過程中做了一點手腳,讓Advice能夠參與進來。

而編織階段可以有兩個選擇,分別是加載時編織(也可以成爲運行時編織)和編譯時編織

  • 加載時編織(Load-Time Weaving):顧名思義,這種編織方式是在JVM加載類的時候完成的。
  • 編譯時編織(Compile-Time Weaving):需要使用AspectJ的編譯器來替換JDK的編譯器。

  詳情:AOP的兩種實現-Spring AOP以及AspectJ

1、添加spirng aop支持和AspectJ依賴

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.0.7.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.13</version>
</dependency>

或者

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.3.RELEASE</version>
</parent>


<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
    </dependency>
</dependencies>

 

2、啓用對@AspectJ的支持

  Spring默認不支持@AspectJ風格的切面聲明,爲了支持需要使用如下配置:

@Configuration
@ComponentScan("com.only.mate.springboot.aop")
@EnableAspectJAutoProxy//開啓AspectJ註解
public class CustomAopConfigurer {
}

3、聲明切面

@Aspect
@Component
public class CustomLogAspect {
}

4、聲明切入點

@AspectJ風格的命名切入點使用org.aspectj.lang.annotation包下的@Pointcut+方法(方法必須是返回void類型)實現。

@Pointcut(value="切入點表達式", argNames = "參數名列表")  
public void pointcutName(……) {}  

value:指定切入點表達式;

       argNames:指定命名切入點方法參數列表參數名字,可以有多個用“,”分隔,這些參數將傳遞給通知方法同名的參數,同時比如切入點表達式“args(param)”將匹配參數類型爲命名切入點方法同名參數指定的參數類型。

       pointcutName:切入點名字,可以使用該名字進行引用該切入點表達式。

案例:

@Pointcut(value="execution(* com.only.mate.springboot.controller.*.sayAdvisorBefore(..)) && args(param)", argNames = "param")  
public void pointCut(String param) {} 

定義了一個切入點,名字爲“pointCut”,該切入點將匹配目標方法的第一個參數類型爲通知方法實現中參數名爲“param”的參數類型。

5、聲明通知

@AspectJ風格的聲明通知也支持5種通知類型:

A、前置通知:使用org.aspectj.lang.annotation 包下的@Before註解聲明。

@Before(value = "切入點表達式或命名切入點", argNames = "參數列表參數名")  

       value:指定切入點表達式或命名切入點。

       argNames:與Schema方式配置中的同義。

B、後置返回通知:使用org.aspectj.lang.annotation 包下的@AfterReturning註解聲明。

@AfterReturning(  
value="切入點表達式或命名切入點",  
pointcut="切入點表達式或命名切入點",  
argNames="參數列表參數名",  
returning="返回值對應參數名")  

       value:指定切入點表達式或命名切入點。

       pointcut:同樣是指定切入點表達式或命名切入點,如果指定了將覆蓋value屬性指定的,pointcut具有高優先級。

       argNames:與Schema方式配置中的同義。

       returning:與Schema方式配置中的同義。

C、後置異常通知:使用org.aspectj.lang.annotation 包下的@AfterThrowing註解聲明。

@AfterThrowing (  
value="切入點表達式或命名切入點",  
pointcut="切入點表達式或命名切入點",  
argNames="參數列表參數名",  
throwing="異常對應參數名")

       value:指定切入點表達式或命名切入點。

       pointcut:同樣是指定切入點表達式或命名切入點,如果指定了將覆蓋value屬性指定的,pointcut具有高優先級。

       argNames:與Schema方式配置中的同義。

       throwing:與Schema方式配置中的同義。

D、後置最終通知:使用org.aspectj.lang.annotation 包下的@After註解聲明。

@After (  
value="切入點表達式或命名切入點",  
argNames="參數列表參數名") 

       value:指定切入點表達式或命名切入點。

       argNames:與Schema方式配置中的同義。

E、環繞通知:使用org.aspectj.lang.annotation 包下的@Around註解聲明。

@Around (  
value="切入點表達式或命名切入點",  
argNames="參數列表參數名")  

 value:指定切入點表達式或命名切入點。

       argNames:與Schema方式配置中的同義。

二、實踐

1、Schema方式配置AOP

A、定一個切入點

/**
 * 自定義一個切入點-權限校驗
 */
public class CustomAuthorityAspect {
    private Logger logger = LoggerFactory.getLogger(CustomLogAspect.class);
    /**
     * 加密
     */
    public void encode() {
        logger.info("CustomAuthorityAspect ==> encode method: encode data");
    }
    
    /**
     * 解密
     */
    public void decode() {
        logger.info("CustomAuthorityAspect ==> decode method: decode data");
    }
}

B、通過Schema方式配置AOP

<bean id="customAuthorityAspect" class="com.only.mate.springboot.aop.CustomAuthorityAspect" />
<aop:config proxy-target-class="false">
    <!-- AOP實現 -->
    <aop:aspect id="customAuthority" ref="customAuthorityAspect">
        <aop:pointcut id="addAllMethod" expression="execution(* com.only.mate.springboot.controller.*.*(..))" />
        <aop:before method="encode" pointcut-ref="addAllMethod" />
        <aop:after method="decode" pointcut-ref="addAllMethod" />
    </aop:aspect>
</aop:config>

前面說過Spring使用動態代理或是CGLIB生成代理是有規則的,高版本的Spring會自動選擇是使用動態代理還是CGLIB生成代理內容,當然我們也可以強制使用CGLIB生成代理,那就是<aop:config>裏面有一個"proxy-target-class"屬性,這個屬性值如果被設置爲true,那麼基於類的代理將起作用,如果proxy-target-class被設置爲false或者這個屬性被省略,那麼基於接口的代理將起作用

2、使用@AspectJ風格的切面聲明

A、定一個切入點

/**
 * @Description: 自定義切面
 *
 */
@Aspect
@Component
public class CustomLogAspect {
    private Logger logger = LoggerFactory.getLogger(CustomLogAspect.class);

    /**
     * @Description: 定義切入點
     */
    //被註解CustomAopAnnotation表示的方法
    //@Pointcut("@annotation(com.only.mate.springboot.annotation.CustomAopAnnotation")
    @Pointcut("execution(public * com.only.mate.springboot.controller.*.*(..))")
    public void pointCut(){
        
    }

    /**
     * @Description: 定義前置通知
     */
    @Before("pointCut()")
    public void before(JoinPoint joinPoint) throws Throwable {
        // 接收到請求,記錄請求內容
        logger.info("【註解:Before】------------------切面  before");
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 記錄下請求內容
        logger.info("【註解:Before】瀏覽器輸入的網址=URL : " + request.getRequestURL().toString());
        logger.info("【註解:Before】HTTP_METHOD : " + request.getMethod());
        logger.info("【註解:Before】IP : " + request.getRemoteAddr());
        logger.info("【註解:Before】執行的業務方法名=CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        logger.info("【註解:Before】業務方法獲得的參數=ARGS : " + Arrays.toString(joinPoint.getArgs()));

    }

    /**
     * @Description: 後置返回通知
     */
    @AfterReturning(returning = "ret", pointcut = "pointCut()")
    public void afterReturning(Object ret) throws Throwable {
        // 處理完請求,返回內容
        logger.info("【註解:AfterReturning】這個會在切面最後的最後打印,方法的返回值 : " + ret);
    }

    /**
     * @Description: 後置異常通知
     */
    @AfterThrowing("pointCut()")
    public void afterThrowing(JoinPoint jp){
        logger.info("【註解:AfterThrowing】方法異常時執行.....");
    }

    /**
     * @Description: 後置最終通知,final增強,不管是拋出異常或者正常退出都會執行
     */
    @After("pointCut()")
    public void after(JoinPoint jp){
        logger.info("【註解:After】方法最後執行.....");
    }

    /**
     * @Description: 環繞通知,環繞增強,相當於MethodInterceptor
     */
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint pjp) {
        logger.info("【註解:Around . 環繞前】方法環繞start.....");
        try {
            //如果不執行這句,會不執行切面的Before方法及controller的業務方法
            Object o =  pjp.proceed();
            logger.info("【註解:Around. 環繞後】方法環繞proceed,結果是 :" + o);
            return o;
        } catch (Throwable e) {
            e.printStackTrace();
            return null;
        }
    }

}

B、使用@AspectJ風格的切面聲明

/**
 * 自定義AOP配置類
 */
@Configuration
@ComponentScan("com.only.mate.springboot.aop")
@EnableAspectJAutoProxy//開啓AspectJ註解
public class CustomAopConfigurer {
}

總結

AspectJ在編譯時就增強了目標對象,Spring AOP的動態代理則是在每次運行時動態的增強,生成AOP代理對象,區別在於生成AOP代理對象的時機不同,相對來說AspectJ的靜態代理方式具有更好的性能,但是AspectJ需要特定的編譯器進行處理,而Spring AOP則無需特定的編譯器處理。

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