【Spring】Spring複習之AOP

如果對此文感興趣,可以繼續看下一篇博文,持續更新,新手學習,有問題歡迎指正:
https://blog.csdn.net/renjingjingya0429/article/details/90215902

一、AOP的相關概念

1.1 AOP概述

1.1.1 什麼是AOP?

AOP:全稱是Aspect Oriented Programming 即:面向切面編程。
在軟件業,AOP通過預編譯的方式和運行期動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,是軟件開發中的一個熱點,也是Spring框架的一個重要內容,是函數式編程的一種衍生泛型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。

簡單地說,它就是把我們程序重複的代碼抽取出來,在需要執行的時候,使用動態代理的技術,在不修改源碼的基礎上,對我們已有方法進行增強。

1.2 AOP的作用及優勢

  • 作用:在程序運行期間,不修改源碼對已有方法進行增強。
  • 優勢:
    • 減少重複代碼
    • 提高開發效率
    • 維護方便

1.3 AOP的實現方式

使用動態代理技術

2. AOP的具體應用

2.1 一些問題

我們在業務層中實現事務控制的時候,業務層的方法中存在着很多的重複代碼。並且業務層方法和事務控制方法中耦合了。我們可以使用下面的知識來解決這些問題。

2.2 動態代理回顧

2.2.1 動態代理的特點

字節碼隨用隨創建,隨用隨加載。
他與靜態代理的區別也在於此。因爲靜態代理是一上來字節碼就創建好,並加載完成。

2.2.2 動態代理常用的兩種方式

  • 基於接口的動態代理
    • 提供者:JDK官方的Proxy類。
    • 要求:被代理類最少實現一個接口。
  • 基於子類的動態代理
    • 提供者:第三方的CGLib,如果報asmxxxx異常,需要導入asm.jar。
    • 要求:被代理類不能用final修飾的類。

2.2.3 使用JDK官方的Proxy 類創建代理對象

/**
 * 一個經紀公司的要求:
 * 能做基本的表演和危險的表演
 */

public interface IActor {
    /**
     * 基本的演出
     *
     */
    public void basicAct(float money);

    /**
     * 危險演出
     */
    public void dangerAct(float money);
}

public class Actor implements IActor {

    /**
     * 基本演出
     * @param money
     */
    @Override
    public void basicAct(float money) {
        System.out.println("拿到錢,開始基本的表演:"+money);
    }

    /**
     * 危險演出
     * @param money
     */
    @Override
    public void dangerAct(float money) {
        System.out.println("拿到錢,開始危險的表演:"+money);
    }
}
public class Client {
    public static void main(String[] args) {
        //一個劇組找演員
        Actor actor = new Actor();//直接

        /**
         * 代理:間接。
         * 獲取代理對象:
         *  要求:被代理類最少實現一個接口。
         * 創建的方式:Proxy.newProxyInstance(三個參數)
         * 參數含義:
         * ClassLoader:和被代理對象使用相同的類加載器。
         * Interfaces:和被代理對象具有相同的行爲。實現相同的接口。
         * InvocationHandler:如何代理。
         *      策略模式:使用場景是:
         *          數據有了,目的明確。
         *          達成目標的過程,就是策略。
         */
        IActor proxyActor = (IActor) Proxy.newProxyInstance(actor.getClass().getClassLoader(), actor.getClass().getInterfaces(), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                /**
                 * 執行被代理對象的任何方法,都會經過該方法。
                 * 此方法有攔截的功能。
                 *  參數:
                 *      proxy:代理對象的引用。不一定每次都用得到。
                 *      method:當前執行的方法對象。
                 *      args:執行方法所需的參數。
                 *  返回值:
                 *      當前執行方法的返回值。
                 */
                String name = method.getName();
                Float money = (Float)args[0];
                Object rtValue = null;
                //每個經紀公司對不同演出收費不一樣,此處開始判斷
                if ("basicAct".equals(name)) {
                    //基本演出
                    if (money > 2000) {
                        //看上去劇組是給了 8000,實際到演員手裏只有 4000
                        //這就是我們沒有修改原來 basicAct 方法源碼,對方法進行了增強
                        rtValue = method.invoke(actor, money/2);
                    }
                }
                if ("dangerAct".equals(name)) {
                    //基本演出
                    if (money > 5000) {
                        //看上去898劇組是給了 8000,實際到演員手裏只有 4000
                        //這就是我們沒有修改原來 basicAct 方法源碼,對方法進行了增強
                        rtValue = method.invoke(actor, money/2);
                    }
                }
                return rtValue;
            }
        });
        //沒有經濟公司的時候,直接找演員。
        actor.basicAct(100f);
        actor.dangerAct(500f);

        //劇組無法直接聯繫演員,而是由經紀公司找的演員
        proxyActor.basicAct(8000f);
        proxyActor.dangerAct(50000f);
    }
}

2.2.3 使用CGLib的Enhancer類創建代理對象

public class Actor {
    /**
     * 基本演出
     * @param money
     */
    public void basicAct(float money) {
        System.out.println("CGLIB拿到錢,開始基本的表演:"+money);
    }

    /**
     * 危險演出
     * @param money
     */
    public void dangerAct(float money) {
        System.out.println("CGLIB拿到錢,開始危險的表演:"+money);
    }
}

public class Client {
    public static void main(String[] args) {
        //一個劇組找演員
        final Actor actor = new Actor();//直接(注意在匿名內部類中只能引用final類型的外部變量)

        /**
         * 基於子類的動態代理
         *  要求:被代理的對象不能是最終類。
         *  用到的類:Enhancer
         *  用到的方法:create(Class,Callback)
         *  方法的參數:
         *      Class:被代理對象的字節碼。
         *      Callback:如何代理。
         */
        Actor cglibActor = (Actor) Enhancer.create(actor.getClass(), new MethodInterceptor() {
            /**
             * 執行被代理對象的任何方法,都會經過該方法。在此方法內部就可以對被代理對象的任何方法進行增強。
             *
             * 參數:
             *      前三個和基於接口的動態代理是一樣的。
             *      MethodProxy:當前執行方法的代理對象。
             *      返回值:當前執行方法的返回值。
             */
            @Override
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                String name = method.getName();
                Float money = (Float) args[0];
                Object rtValue = null;
                //每個經紀公司對不同演出收費不一樣,此處開始判斷
                if ("basicAct".equals(name)) {
                    //基本演出
                    if (money > 2000) {
                        //看上去劇組是給了 8000,實際到演員手裏只有 4000
                        //這就是我們沒有修改原來 basicAct 方法源碼,對方法進行了增強
                        rtValue = method.invoke(actor, money / 2);
                    }
                }
                if ("dangerAct".equals(name)) {
                    //基本演出
                    if (money > 5000) {
                        //看上去898劇組是給了 8000,實際到演員手裏只有 4000
                        //這就是我們沒有修改原來 basicAct 方法源碼,對方法進行了增強
                        rtValue = method.invoke(actor, money / 2);
                    }
                }
                return rtValue;
            }
        });**加粗樣式**
        cglibActor.basicAct(10000);
        cglibActor.dangerAct(100000);
    }
}

二、Spring中的AOP

1. Spring中AOP的細節

1.1 AOP相關術語

  • JoinPoint(連接點):所謂連接點是指那些被攔截到的點。在Spring中,這些點指的是方法,因爲Spring只支持方法類型的連接點。
  • PointCut(切入點):是指我們要對哪些JoinPoint 進行攔截的定義。
  • Advice(通知/增強):所謂通知是指攔截到JoinPoint之後所要做的事情就是通知(增強的方法抽取而形成的類)
    通知的類型:前置通知、後置通知、異常通知、最終通知、環繞通知。
  • Introduction(引介):引介是一種特殊的通知在不修改類代碼的前提下,Introduction可以在運行期爲類動態的添加一些方法或者Field。
  • Target(目標對象):代理的目標對象(被代理對象)。
  • Weaving(織入):是指把增強應用到目標對象來創建新的代理對象的過程。
    Spring採用動態代理織入,而AspectJ採用編譯器織入和類裝載期織入。
  • Proxy(代理):一個類被AOP織入增強後,就產生一個結果代理類。
  • Aspect(切面):是切入點和通知(引介)的結合。

1.2 學習Spring中AOP要明確的事

  • 開發階段(我們做的)

    • 編寫業務核心代碼(開發主線):大部分程序員來做,要求熟悉業務需求。
    • 公用代碼抽取出來,製作成通知。(開發階段最後在做):AOP編程人員來做。
    • 在配置文件中,聲明切入點與通知間的關係,即切面:AOP編程人員來做。
  • 運行階段(Spring框架完成的)

Spring框架監控切入點方法的執行。一旦監控到切入點方法被執行,使用代理機制,動態創建目標對象的代理對象,根據通知類別,在代理對象的對應位置,將通知對應的功能織入,完成完整的代碼邏輯運行。

1.3 關於代理的選擇

在Spring中,框架會根據目標類是否實現了接口來決定採用哪種動態代理的方式。

2. 基於XML的AOP配置

2.1 導入需要的jar包。
在這裏插入圖片描述
2.2 在src下創建如下結構的目錄。

在這裏插入圖片描述
2.3 核心代碼展示。

/**
 * 模擬業務層的實現類
 */
public class CustomerServiceImpl implements ICustomerService {
    @Override
    public void saveCustomer() {
        System.out.println("保存了客戶");
//        int i = 1/0;//用於模擬異常
    }

    @Override
    public void updateCustomer(int i) {
        System.out.println("更新了客戶");
    }

    @Override
    public int deleteCustomer() {
        System.out.println("刪除了客戶");
        return 0;
    }
}
/**
 * main方法
 */
public class client {
    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        ICustomerService cs = (ICustomerService) ac.getBean("customerService");
        cs.saveCustomer();
    }
}
/**
 * 一個用於記錄日誌的類
 */
public class Logger {
    /**
     * 記錄日誌的操作
     * 計劃讓其在業務核心方法(切入點方法)執行之前執行
     */
    /**
     * 前置通知
     */
    public void beforePrintLog(){
        System.out.println("記錄日誌beforePrintLog");
    }
    /**
     * 後置通知
     */
    public void afterReturningPrintLog(){
        System.out.println("記錄日誌afterReturningPrintLog");
    }
    /**
     * 異常通知
     */
    public void afterThrowingPrintLog(){
        System.out.println("記錄日誌afterThrowingPrintLog");
    }
    /**
     * 最終通知
     */
    public void afterPrintLog(){
        System.out.println("記錄日誌afterPrintLog");
    }

    /**
     * 環繞通知
     * 問題:
     * 當我們配置了環繞通知之後,切入點方法沒有執行,而環繞通知裏的代碼執行了。
     * 分析:
     * 由動態代理可知,環繞通知指的是invoke方法,並且裏面有明確的方法調用。
     * 而我麼現在的環繞通知沒有明確的切入點方法調用。
     * 解決:
     * Spring爲我們提供了一個接口:ProceedingJoinPoint。該接口可以作爲環繞通知的方法參數來使用。
     * 在程序運行時,Spring框架會爲我們提供該接口的實現類,供我們使用。
     * 該接口中有一個方法,proceed(),它的作用就等同於method.invoke方法,就是明確調用業務層核心方法。
     *
     * 環繞通知:他是Spring框架爲我們提供的一種可以在代碼中手動控制通知方法什麼時候執行的方式。
     */
    public Object aroundPrintlog(ProceedingJoinPoint point) {
        Object rtValue = null;
        try {
            System.out.println("記錄日誌aroundPrintlog---前置");
            rtValue = point.proceed();
            System.out.println("記錄日誌aroundPrintlog---後置");
        } catch (Throwable throwable) {
            System.out.println("記錄日誌aroundPrintlog---異常");
            throwable.printStackTrace();
        }finally {
            System.out.println("記錄日誌aroundPrintlog---最終");
        }
        return rtValue;
    }
}

2.4 配置步驟。

①第一步:把通知類用bean標籤配置起來。

<!--配置Service-->
    <bean id="customerService" class="com.renjing.service.impl.CustomerServiceImpl"></bean>

② 第二步:使用aop:config聲明aop配置
aop:config:用於聲明開始aop的配置。

<aop:config>
	<!-- 配置的代碼都寫在此處 -->
</aop:config>

③ 第三步:使用aop:aspect配置切面
aop:aspect:

  • 作用:用於配置切面。
  • 屬性:
    • id:給切面提供一個唯一標識。
    • ref:引用配置好的通知類bean的id。
<aop:aspect id="Advice" ref="customerService">
<!--配置通知的類型要寫在此處-->
</aop:aspect>

④ 第四步:使用aop:aspect配置切入點表達式。
aop:pointcut:

  • 作用:用於配置切入點表達式。就是指定對哪些類的哪些方法進行增強。
  • 屬性:
    • expression:用於定義切入點表達式。
    • id:用於給切入點表達式提供一個唯一標識。
<aop:pointcut id="pt1" expression="execution(* com.renjing.service.impl..*.*(..))"></aop:pointcut>

⑤ 使用aop:xxx配置對應的通知類型
aop:before

  • 作用:用於配置前置通知。
  • 屬性:
    • method:用於指定通知類中的增強方法名稱。
    • point:用於指定切入點表達式。
    • pointcut-ref:用於指定切入表達式的引用。
  • 執行時間點:切入點方法執行之前執行。
 <aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>

aop:after-returning

  • 作用:用於配置後置通知。
  • 屬性:
    • method:用於指定通知類中的增強方法名稱。
    • point:用於指定切入點表達式。
    • pointcut-ref:用於指定切入表達式的引用。
  • 執行時間點:切入點方法正常執行之後執行。它和異常通知只能有一個執行。
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>

aop:after-throwing

  • 作用:用於配置異常通知。
  • 屬性:
    • method:用於指定通知類中的增強方法名稱。
    • point:用於指定切入點表達式。
    • pointcut-ref:用於指定切入表達式的引用。
  • 執行時間點:切入點方法產生異常後執行。它和後置通知只能有一個執行。
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>

aop:after

  • 作用:用於配置最終通知。
  • 屬性:
    • method:用於指定通知類中的增強方法名稱。
    • point:用於指定切入點表達式。
    • pointcut-ref:用於指定切入表達式的引用。
  • 執行時間點:無論切入點方法執行是否有異常,它都會在其後面執行。
<aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>
-->

2.5 切入點表達式說明。

切入點表達式:
                關鍵字:execution(表達式)
                表達式寫法:
                    訪問修飾符 返回值 包名.包名...類名.方法名(參數列表)
                全匹配方式:
                    public void com.renjing.service.impl.CustomerServiceImpl.saveCustomer()
                訪問修飾符可以省略:
                    void com.renjing.service.impl.CustomerServiceImpl.saveCustomer()
                返回值可以使用通配符:
                    * com.renjing.service.impl.CustomerServiceImpl.saveCustomer()
                包名可以使用通配符,表示任意包。但是,有幾個包就寫幾個** *.*.*.*.CustomerServiceImpl.saveCustomer()
                包名可以使用..表示當前包及其子包:
                    * com..CustomerServiceImpl.saveCustomer()
                類名和方法名都可以使用通配符:
                    * com..*.*()
                參數列表可以使用具體類型,來表示參數類型:
                    基本類型直接寫類型名稱:int
                    引用類型必須是包名.類名。java.lang.Integer
                參數列表可以使用通配符,表示任意參數類型,但是必須有參數
                    * com..*.*(*)
                參數列表可以使用..表示有無參數均可,有參數可以是任意類型。
                    * com..*.*(..)
                全通配方式:
                    * *..*.*(..)
                實際開發中,我們一般情況下,都是對業務層方法進行增強:
                    寫法:* com.renjing.service.impl.*.*(..)

2.6 環繞通知。
配置方式:

<aop:config>
        <!--定義通用的切入點表達式,如果寫在aop:aspect標籤外部的前面(必須寫在前面,否則報錯),則表示所有切面可用,-->
        <aop:pointcut id="pt1" expression="execution(* com.renjing.service.impl..*.*(..))"></aop:pointcut>
        <!--配置切面-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置環繞通知-->
            <aop:around method="aroundPrintlog" pointcut-ref="pt1"></aop:around>
        </aop:aspect>
    </aop:config>

aop:around:

  • 作用:用於配置環繞通知。
  • 屬性:
    • method:用於指定通知類中的增強方法名稱。
    • point:用於指定切入點表達式。
    • pointcut-ref:用於指定切入表達式的引用。
  • 說明:它是Spring框架爲我們提供的一種可以在代碼中手動控制增強代碼什麼時候執行的方式。
  • 注意:通常情況下,環繞通知都是獨立使用的。
/**
 * 環繞通知
 */
public Object aroundPrintlog(ProceedingJoinPoint point) {
        Object rtValue = null;
        try {
            System.out.println("記錄日誌aroundPrintlog---前置");
            rtValue = point.proceed();
            System.out.println("記錄日誌aroundPrintlog---後置");
        } catch (Throwable throwable) {
            System.out.println("記錄日誌aroundPrintlog---異常");
            throwable.printStackTrace();
        }finally {
            System.out.println("記錄日誌aroundPrintlog---最終");
        }
        return rtValue;
    }

3. 基於註解的AOP配置
3.1環境搭建
(1)導入需要的jar包。
在這裏插入圖片描述
(2)在配置文件中導入context的名稱空間。
(3)把資源使用註解配置

/**
 * 模擬業務層的實現類
 */
@Service("customerService")
public class CustomerServiceImpl implements ICustomerService {
    @Override
    public void saveCustomer() {
        System.out.println("保存了客戶");
//        int i = 1/0;
    }

    @Override
    public void updateCustomer(int i) {
        System.out.println("更新了客戶");
    }

    @Override
    public int deleteCustomer() {
        System.out.println("刪除了客戶");
        return 0;
    }
}

(4)第四步:在配置文件中指定Spring要掃描的包

<!--初始化Spring容器時要掃描的包-->
<context:component-scan base-package="com.renjing"></context:component-scan>

3.2 配置步驟
(1)把通知類也使用註解配置
(2)在通知類上使用@Aspect 註解聲明爲切面
(3)在增強的方法上使用註解配置通知
@Before

  • 作用:
    把當前方法看成是前置通知。
  • 屬性:
    value:用於指定切入點表達式,還可以指定切入點表達式的引用。

@AfterReturning

  • 作用:
    把當前方法看成是後置通知。
  • 屬性:
    value:用於指定切入點表達式,還可以指定切入點表達式的引用

@AfterThrowing

  • 作用:
    把當前方法看成是異常通知。
  • 屬性:
    value:用於指定切入點表達式,還可以指定切入點表達式的引用。

@After

  • 作用:
    把當前方法看成是最終通知。
  • 屬性:
    value:用於指定切入點表達式,還可以指定切入點表達式的引用

@Around

  • 作用:
    把當前方法看成是環繞通知。
  • 屬性:
    value:用於指定切入點表達式,還可以指定切入點表達式的引用。
/**
 * 一個用於記錄日誌的類
 */
@Component("logger")//放入Spring容器(1)
@Aspect//配置切面(2)
public class Logger {
    /**
     * 記錄日誌的操作
     * 計劃讓其在業務核心方法(切入點方法)執行之前執行
     */
    /**
     * 切入點表達式
     */
    @Pointcut("execution(* com.renjing.service.impl.*.*(..))")
    public void pt1(){

    }
    /**
     * 前置通知
     */
//    @Before("pt1()")
    public void beforePrintLog(){
        System.out.println("記錄日誌beforePrintLog---前置");
    }
    /**
     * 後置通知
     */
//    @AfterReturning("pt1()")
    public void afterReturningPrintLog(){
        System.out.println("記錄日誌afterReturningPrintLog---後置");
    }
    /**
     * 異常通知
     */
//    @AfterThrowing("pt1()")
    public void afterThrowingPrintLog(){
        System.out.println("記錄日誌afterThrowingPrintLog---異常");
    }
    /**
     * 最終通知
     */
//    @After("pt1()")
    public void afterPrintLog(){
        System.out.println("記錄日誌afterPrintLog---最終");
    }
    /**
     * 環繞通知
     * 問題:
     * 當我們配置了環繞通知之後,切入點方法沒有執行,而環繞通知裏的代碼執行了。
     * 分析:
     * 由動態代理可知,環繞通知指的是invoke方法,並且裏面有明確的方法調用。
     * 而我麼現在的環繞通知沒有明確的切入點方法調用。
     * 解決:
     * Spring爲我們提供了一個接口:ProceedingJoinPoint。該接口可以作爲環繞通知的方法參數來使用。
     * 在程序運行時,Spring框架會爲我們提供該接口的實現類,供我們使用。
     * 該接口中有一個方法,proceed(),它的作用就等同於method.invoke方法,就是明確調用業務層核心方法。
     *
     * 環繞通知:他是Spring框架爲我們提供的一種可以在代碼中手動控制通知方法什麼時候執行的方式。
     */
    @Around("pt1()")
    public Object aroundPrintlog(ProceedingJoinPoint point) {
        Object rtValue = null;
        try {
            System.out.println("記錄日誌aroundPrintlog---前置");
            rtValue = point.proceed();
            System.out.println("記錄日誌aroundPrintlog---後置");
        } catch (Throwable throwable) {
            System.out.println("記錄日誌aroundPrintlog---異常");
            throwable.printStackTrace();
        }finally {
            System.out.println("記錄日誌aroundPrintlog---最終");
        }
        return rtValue;
    }
}

補充:切入點表達式註解
@Pointcut

  • 作用:
    指定切入點表達式
  • 屬性:
    value:指定表達式的內容
    (4)在 spring 配置文件中開啓 spring 對註解 AOP 的支持
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       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
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd">
    <!--初始化Spring容器時要掃描的包-->
    <context:component-scan base-package="com.renjing"></context:component-scan>
    <!--開啓Spring對註解AOP的支持-->
    <aop:aspectj-autoproxy/>
</beans>
  1. 不使用 XML 的配置方式
@Configuration//讓當前類變成一個配置類
@ComponentScan("com.renjing")//初始化Spring容器時要掃描的包
@EnableAspectJAutoProxy//開啓Spring對註解aop的支持
public class SpringConfiguration {

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