Spring AOP——基礎使用

一、AOP思想概述

AOP(Aspect Oriented Programming),即面向切面編程,可以說是OOP(Object Oriented Programming,面向對象編程)的補充和完善。OOP引入封裝、繼承、多態等概念來建立一種對象層次結構,用於模擬公共行爲的一個集合。

不過OOP允許開發者定義縱向的關係,但並不適合定義橫向的關係,例如日誌功能。日誌代碼往往橫向地散佈在所有對象層次中,而與它對應的對象的核心功能毫無關係對於其他類型的代碼,如安全性、異常處理和透明的持續性也都是如此,這種散佈在各處的無關的代碼被稱爲橫切(cross cutting),在OOP設計中,它導致了大量代碼的重複,而不利於各個模塊的重用。

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

優勢:

  1. AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。
  2. 可以在不修改源代碼的前提下,對程序進行增強。

切面:Aspect,由切入點額外功能(增強)組成
作用:提供了新的編程角度,不再只是考慮類、對象,而可以考慮切面。切面和目標形成 代理,解決項目業務中額外功能冗餘的問題

AOP核心概念

  • 連接點(Joinpoint)

    連接點是程序執行的某一個特定位置,如類開始初始化前,類初始化後,類某一個方法調用前/調用後,方法拋出異常後,一個類或一段程序代碼擁有一些具有邊界性質的特定點,這寫代碼中的特定點就稱爲"連接點",Spring僅支持方法連接點,即僅能在方法調用前,方法調用後,方法拋出異常時,以及方法調用前後這些程序執行點織入增強.

  • 切點(Pointcut)

    每個程序類都擁有多個連接點,如一個擁有兩個方法的類,這兩個方法都是連接點,但在衆多連接點中,我們把需要使用AOP切入的連接點叫切點。
    切入點是指我們要對哪些Joinpoint進行攔截的定義。

  • 增強、通知(Advice)

    增強是織入目標類連接點上的一段程序代碼,在Spring中,增強不僅可以描述程序代碼,還擁有另一個和連接點相關的信息,這便是執行點的方位,結合執行點的方位信息和切點信息,就可以找到特定的連接,正因爲增強既包含了用於添加到目標連接點上的一段執行邏輯,又包含用於定位連接點的方位信息,所以Spring所提供的增強接口都是帶方位名的.如BeforeAdvice,AfterReturningAdvice,ThrowsAdvice等.BeforeAdvice表示方法調用前的位置.而AfterReturningAdivce表示訪問返回後的位置,所以只有結合切點和增強,才能確定特定的連接點並實施增強邏輯.

  • 目標對象(Target)

    增強邏輯的織入目標類.如果沒有AOP,那麼目標業務類需要自己實現所有邏輯,如果使用AOP可以把一些非邏輯性代碼通過AOP織入到主程序代碼上。

  • 引介(Introduction)

    引介是一種特殊的增強,它爲類添加一些屬性和方法.這樣,即使一個業務類原本沒有實現某一個接口,通過AOP的引介功能,也可以動態地爲該業務類添加接口的實現邏輯.讓業務類成爲這個接口的實現類

  • 織入(Weaving)

    織入是將增強添加到目標類具體鏈接點上的過程,AOP就像一臺織布機,將目標類,增強,或者引介天衣無縫的編織到一起,我們不能不說"織入"這個詞太精闢了,根據不同的實現技術,AOP有3種織入方式:
    (1)編譯期織入,這要求使用特殊的Java編譯器.
    (2)類裝載期織入,這要求使用特殊的類裝載器.
    (3)動態代理織入,在運行期爲目標類添加增強生成子類的方式.
    Spring採用動態代理織入,而AspectJ採用編譯期織入和類裝載器織入.

  • 代理(Proxy)

    一個類被AOP織入增強後,就產生了一個結果類.它是融合了原類和增強邏輯的代理類,根據不同的代理方式,代理類既可能是和原類具有相同接口的類,也可能就是原類的子類,所以可以採用與調用原類相同的方法調用代理類.

  • 切面(Aspect)

    切面由切點和增強(通知)組成,它既包括增強的定義,也包括連接點的定義,Spring AOP就是負責實施切面的框架,它將切面所定義的橫切所定義的橫切邏輯織入切面所指定的連接點中.
    AOP的工作重心在於如何將增強應用於目標對象的連接點中上。


二、Spring中AOP開發

Spring的AOP思想是通過【動態代理】實現了,底層有兩種方式,一種是jdk動態代理,一種是CGLib動態代理

Spring-AOP 是對 AOP框架之一。其他比如還有AspectJ,Spring-AOP僅僅實現了AspectJ中的編織部分weave

2.1 導入依賴

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>4.3.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>4.3.6.RELEASE</version>
</dependency>

2.2 創建Advice增強,添加額外功能

必須繼承MethodBeforeAdvice,後面會介紹很多的Advice

/**
* 在覈心功能執行前執行,必須繼承MethodBeforeAdvice,後面會介紹很多的Advice
* method 方法對象
* args 方法參數表
* target 目標對象
*/
public class MyBeforeAddvice implements MethodBeforeAdvice {
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("額外的功能before....." + method.getName());
    }
}

2.3 創建目標,service類

//接口
public interface UserService {
    void add(User user);
    User findById(Integer id);
}

//實現類
public class UserServiceImpl implements UserService {
    @Override
    public void add(User user) {
        System.out.println("核心功能添加user");
    }

    @Override
    public User findById(Integer id) {
        System.out.println("核心功能,查詢user");
        return new User();
    }
}

2.4 配置文件,將增強Advice編織入目標對象service

<?xml version="1.0" encoding="UTF-8"?>
<!--添加aop路徑,其實就是複製beans的,然後修改名稱爲aop,並在前面起一個別名xmlns:aop-->
<!--同樣,在schemaLocation中也複製修改aop和xsd即可-->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"    
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd">

	<!--目標-->
    <bean id="userService" class="service.impl.UserServiceImpl"/>
    
    <!--額外功能:Advice-->
    <bean id="before04" class="advice.MyBeforeAddvice"/>
    <bean id="beforelog" class="advice.MyLogAdvice"/>
    <bean id="after04" class="advice.MyAfterAdvice"/>
    <bean id="Interceptor04" class="advice.MyMethodInterceptor"/>
    <bean id="exception04" class="advice.MyException"/>
	
     <!--默認JDK代理優先-->
     <!-- 強制使用cglib代理 -->
	 <aop:config proxy-target-class="true">
 
	 <!--編織:通過目標信息,額外功能信息,組件一個新的類:Proxy-->
     <aop:config>
    	<!--
        切入點:目標中的方法
                execution表達式,後面會講到常用表達式,執行
                execution(修飾符 返回值 包.類.方法名(參數表))
                execution(public Integer com.rj.service.UserServiceImpl.findUser(Integer,com.rj.pojo.User))
                execution(* com.rj.service.UserServiceImpl.*(..))
        -->
        <aop:pointcut id="pc04" expression="execution(* service.impl.UserServiceImpl.*(..))"/>
        <aop:pointcut id="pc11" expression="within(service.impl.UserServiceImpl) and not args(pojo.User)"/>

        <!--將某個額外的功能(代理)編織到某些切入點-->
        <aop:advisor advice-ref="before04" pointcut-ref="pc04"/>
        <aop:advisor advice-ref="beforelog" pointcut-ref="pc04"/>
        <aop:advisor advice-ref="after04" pointcut-ref="pc04"/>
        <aop:advisor advice-ref="Interceptor04" pointcut-ref="pc04"/>
        <aop:advisor advice-ref="exception04" pointcut-ref="pc04"/>
        <aop:advisor advice-ref="after04" pointcut-ref="pc11"/>
    </aop:config>
</beans>

整個過程,就是遍歷bean節點,處理目標,注入屬性,在後處理器後置過程中,進行判斷,是否需要代理,是選擇JDK代理還是CGLib代理,然後返回原始目標,或者返回代理對象。

面向對象的封裝、繼承和多態更多是縱向延伸,而AOP則是將對象橫向解刨,保留核心功能,增加額外功能,豐富對象。

三、多種Advice

3.1 前置額外功能

public class MyBeforeAdvice implements MethodBeforeAdvice{

    /**
     * @param method 當前執行的方法
     * @param args   當前執行的方法中的參數
     * @param target 目標對象
     * @throws Throwable
     */
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("額外的功能before....." + method.getName());
    }
}

//打印:
額外的功能before.....add
核心功能添加user

3.2 後置額外功能

public class MyAfterAdvice implements AfterReturningAdvice{

    /**
     * 在覈心功能返回後執行
     * @param returnValue 核心功能返回值
     * @param method 方法對象
     * @param args 方法參數表
     * @param target 目標對象
     * @throws Throwable
     */
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("After...ret " + returnValue + " method:" + method.getName());
    }
}
//打印
核心功能添加user
After...ret null method:add

3.3 環繞額外功能

public class MyMethodInterceptor implements MethodInterceptor{

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("before:" + invocation.getMethod().getName());
        Object ret = invocation.proceed();//執行目標業務方法,獲得執行結果
        System.out.println("after:" + invocation.getMethod().getName());
        return ret;//返回目標業務方法返回值
    }
}

//打印
before:add
核心功能添加user
after: add

3.4 異常額外功能(瞭解)

public class MyThrows implements ThrowsAdvice{
    //目標業務方法中拋出異常時,執行此方法。ex=拋出的異常對象
    /**
     * 當核心功能中拋出異常時執行
     * @param ex 異常對象
     */
    public void afterThrowing(Exception ex){
        System.out.println(ex.getMessage()+"~~~拋出異常");
    }
}

3.5 最終額外功能(瞭解)

AfterAdvice

在覈心之後執行( 即使核心中出現了異常,依然執行額外 )

3.6 編織

<!-- 聲明 target+advice -->
<bean id="userService" class="xxxxx"></bean>
<bean id="myXXAdvice" class="xxxxx"></bean>
<aop:config>
    <aop:pointcut id="pc" expression="execution(* com..UserService*.*(..))"/>
    <aop:advisor advice-ref="advice的BeanId" pointcut-ref="pc"/>
</aop:config>

四、切入點表達式

切入點表達式有很多,比較常用的主要有execution、within、args

4.1 execution

1> * com.service.UserServiceImpl.queryUser(..)
    修飾符:任意
    返回值:任意
    包:com.service
    類:UserServiceImpl
    方法:queryUser
    參數表:任意
2> * com.service.UserServiceImpl.*(..)
    修飾符:任意
    返回值:任意
    包:com.service
    類:UserServiceImpl
    方法:所有,任意
    參數表:任意
3> * com..UserServiceImpl.*(..)
    修飾符:任意
    返回值:任意
    包:com包,及其子包
    類:UserServiceImpl
    方法:所有,任意
    參數表:任意
4> * com.service.*.*(..)
    修飾符:任意
    返回值:任意
    包:com.service
    類:所有,任意
    方法:所有,任意
    參數表:任意
5> * *(..)    不建議
    修飾符:任意
    返回值:任意
    包:任意
    類:所有,任意
    方法:所有,任意
    參數表:任意
6> * com.service.UserServiceImpl.query*(..)  【技巧:批量切入】
    修飾符:任意
    返回值:任意
    包:com.service
    類:UserServiceImpl
    方法:所有,任意
    參數表:任意
  *注意:儘量精確,避免不必要的切入

4.2 within

描述中所有方法都切入

within(com.service.UserServiceImpl) 類中的所有方法
within(com..UserServiceImpl) com包和com子包下的類中的所有方法
<aop:pointcut id="pc" expression="within(com..UserServiceImpl)"/>

4.3 args

描述參數表,符合的方法都切入

args(int,String,com.entity.User) 參數表如此的方法
<aop:pointcut id="pc" expression="args(int,String,com.entity.User)"/>

4.4 三者一般聯用

不同種類的表達式之間,可以使用邏輯運算:

and or not

<aop:pointcut id="pc" expression="execution(* com.zhj.service.UserServiceImpl.*(..)) and args(com.User)"/>
<aop:pointcut id="pc" expression="within(com.service.UserServiceImpl) or args(com.User)"/>
<aop:pointcut id="pc" expression="within(com.service.UserServiceImpl) and not args(com.User)"/>

4.5 切入順序

<!-- order值越小,切入順序就越優先 -->
<aop:advisor advice-ref="before05" pointcut-ref="pc05" order="2"/>
<aop:advisor advice-ref="before04" pointcut-ref="pc05" order="1"/>

五、第二種書寫格式,不用直接遵從接口

/*
* MyAdvice
*/
public class MyAdvice {
    public void before() {
        System.out.println("前置通知");
    }
    public void afterReturning() {
        System.out.println("後置通知");
    }
    public Object arround(ProceedingJoinPoint point) throws Throwable {
        System.out.println("環繞通知之前的部分");
        Object proceed = point.proceed(); //調用目標方法
        System.out.println("環繞之後的部分");
        return proceed;
    }
    public void afterException() {
        System.out.println("異常通知");
    }
    public void after() {
        System.out.println("最終通知,無論是否異常");
    }
}

/**
* serviceimpl
*/
public class UserServiceImpl  implements UserService {
    @Override
    public void add(User user) {
        System.out.println("增加用戶");
    }

    @Override
    public User findById(Integer id) {
        System.out.println("根據id查找用戶");
        return new User(2,"王者");
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--目標,用於servlet調用-->
    <bean id="userservice" class="com.rj.service.impl.UserServiceImpl"/>
    <!--增加功能,切面-->
    <bean id="myadvice" class="com.rj.service.impl.MyAdvice"/>

    <!--將增加的功能添加到目標-->
    <aop:config>
        <!--切入點,解剖userservice,在其哪個點切入-->
        <aop:pointcut id="pc1" expression="execution(* com.rj.service.impl.UserServiceImpl.add(..))"/>
        <aop:pointcut id="pc2" expression="execution(* com.rj.service.impl.UserServiceImpl.findById(..))"/>
        <!--設置切面,主要是前置、後置、環繞、異常、最終,注意引入使用的都是ref-->
        <aop:aspect ref="myadvice">
            <aop:before method="before" pointcut-ref="pc1"/>
            <aop:after-returning method="afterReturning" pointcut-ref="pc2"/>
            <aop:around method="arround" pointcut-ref="pc1"/>
            <aop:after-throwing method="afterException" pointcut-ref="pc2"/>
            <aop:after method="after" pointcut-ref="pc2"/>
        </aop:aspect>
    </aop:config>
</beans>

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