1. 程式人生 > >Java框架-代理模式詳細介紹、Spring的AOP

Java框架-代理模式詳細介紹、Spring的AOP

1. 代理模式詳介

1.1 分類和作用

  • 分類:靜態代理、jdk動態代理(介面代理)、cglib動態代理(子類代理)技術
  • 使用代理的原因:實際開發中通常都會呼叫別人編寫的程式碼/框架來完成業務需求。很多情況是需要對這些程式碼/框架進行微調或擴充套件,而如果修改原始碼很容易出現錯誤。這時候就需要使用代理。
  • 作用:通過代理訪問目標物件。在目標物件實現的基礎上,增強額外的功能操作,即在不修改原始碼的基礎上擴充套件目標物件的功能。

1.2 靜態代理(不推薦)

  • 原理:
    1. 代理類實現與目標物件相同的介面。通過構造器或set方法給代理物件注入目標物件;
    2. 實現代理物件介面方法時,內部呼叫目標物件真正實現方法,並且可新增額外的業務控制。
  • 實現要求:
    1. 代理物件需要實現與目標物件一樣的介面;
    2. 代理物件需要維護一個目標物件(需要目標物件呼叫目標物件自身方法);
    3. 代理物件一定會呼叫目標物件的方法。
  • 優點:在不修改原有程式碼的基礎上,對指定的目標物件進行擴充套件
  • 缺點:
    1. 對於代理物件而言,介面的所有方法都要重寫,即便需要增強的方法僅是其中的少數幾個;
    2. 目標物件的介面修改後,目標物件和代理物件都得相應修改,不便於維護
    3. 每個目標物件至少有一個代理物件,導致代理類過多;

1.2.1 程式碼實現概述

代理類的程式碼實現:

  1. 代理類實現目標物件的介面;
  2. 代理類中建立目標物件;
  3. 重寫介面方法,呼叫目標物件的方法,同時新增額外的業務邏輯。

1.3 jdk動態代理(介面代理)

  • 概念:代理類在程式執行時動態建立

  • 特點

    1. 目標物件必須實現介面;
    2. 程式執行期間,利用jdk的proxy相關api,在記憶體中動態構建位元組碼物件,從而生成代理物件,同時讓代理物件實現目標物件的介面。
    3. jdk動態代理物件在記憶體的名稱是$Proxy加上數字,如 P
      r o x y 3 Proxy3、
      Proxy4等
  • 生成動態代理的API:

    static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h)

    • 引數1 loader:當前使用哪個類載入器生成代理物件
    • 引數2 interfaces:目標物件實現的介面型別陣列
    • 引數3 h:事件處理程式(當執行代理方法時候會觸發),需要傳入一個InvocationHandler介面實現類物件。(詳見案例程式碼)

1.3.1 jdk動態代理的入門案例

1.3.1.1 目標物件介面
public interface IStar {
    /*明星的功能*/
    void singing(double money);
    void act(double money);
}
1.3.1.2 目標物件類,實現介面
public class Star implements IStar {
    @Override
    public void singing(double money) {
        System.out.println("[開演唱會!收費標準:]" + money);
    }

    @Override
    public void act(double money) {
        System.out.println("[拍戲!收費標準:]" + money);
    }
}
1.3.1.3 測試類中實現動態代理
public class Test_jdk {
    public static void main(String[] args) {
        //1.定義目標物件
        IStar target = new Star();
        //2.對目標物件動態生成代理物件
        /**
         * jdk動態代理
         *
         * 1.執行時期,利用jdk的api,在記憶體中動態構建位元組碼物件,從而生成代理物件。
         * 2.要求:目標物件一定要實現介面
         * 3.生成代理的Api
         * |-->Proxy
         *   |--> static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h)
         *      引數1:loader       當前使用哪個類載入器生成代理物件
         *      引數2:interfaces   目標物件實現的介面型別陣列
         *      引數3:h            事件處理程式(當執行代理方法時候會觸發),需要傳入一個InvocationHandler介面實現類物件
         *          InvocationHandler介面只有一個方法Object invoke(Object proxy, Method method, Object[] args)
         *              其中:proxy指當前的代理物件
         *                   method指代理物件呼叫的方法,使用反射的invoke()來執行方法程式碼
         *                   args指傳入的引數陣列
         * 4.原理
         *     生成代理物件:class $Proxy3 implements IStar
         *     通過類載入器生成動態代理物件,通過介面型別陣列確定代理物件實現的介面,通過處理程式程式碼確定代理所要實現的功能
         * 5.API的寫法套路如下,可以另行定義事件處理程式方法,再把方法傳入
         */
        IStar proxy = (IStar) Proxy.newProxyInstance(
                Test_jdk.class.getClassLoader(),
                new Class[]{IStar.class},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 獲取方法引數
                        Double money = (Double) args[0];
                        // 新增額外的業務判斷
                        if (money > 100000) {
                            // 訪問目標物件的方法
                            return method.invoke(target,args);
                        } else {
                            System.out.println("檔期忙,沒空!");
                        }
                        return null;
                    }
                }
        );
        //實行代理方法
        proxy.singing(100001);
        proxy.act(1);
    }
}
1.3.1.4 代理工廠類優化代理類
/**
 * 代理工廠
 * 1. 對所有的目標物件生成代理物件,使用泛型
 * 2. 要求:目標物件一定要實現介面。因為用的是jdk介面代理
 */
public class ProxyFactory<T> {
    //建立泛型目標物件
    private T target;
    //代理工廠建構函式,工廠物件建立時會傳入目標物件
    public ProxyFactory(T target) {
        this.target = target;
    }

    // 針對傳入的目標物件生成代理物件並返回
    public T createProxy() {
        return (T)Proxy.newProxyInstance(
                this.getClass().getClassLoader(),   //根據工廠類獲取類載入器
                target.getClass().getInterfaces(),  //根據目標物件獲取所實現的所有介面的陣列
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 獲取方法引數
                        Double money = (Double) args[0];
                        // 判斷
                        if (money > 100000) {
                            // 呼叫目標物件的方法
                            // 使用method物件的invoke()執行方法程式碼
                            return method.invoke(target, args);
                        } else {
                            System.out.println("沒有呼叫目標物件方法!不滿足條件");
                            return null;
                        }
                    }
                });
    }
}

1.4 cglib動態代理(子類代理)

  • 使用場景:如果目標物件沒有實現任何介面,無法使用介面代理。此時就要用到CGLIB子類代理。
  • 原理:在記憶體中動態構建一個子類物件,從而實現對目標物件功能的擴充套件
  • 使用要求:需要匯入CGLIB的jar包,或者直接引入Spring-Core核心包。
  • 注意事項:
    1. 代理的類不能為final,否則報錯;
    2. **目標物件的方法如果為final/static,那麼就不會被攔截,即不會執行目標物件額外的業務方法!!
  • 特點:
    1. cglib動態代理物件在記憶體的名稱是class com.azure.proxy3_cglib.Star$$EnhancerByCGLIB$$6e7ec13a,格式是:類全名$$EnhancerByCGLIB$$記憶體地址

1.4.1 cglib動態代理的入門案例

1.4.1.1 引入依賴

新增spring-core依賴

1.4.1.2 目標類
public class Star {
    public void singing(double money) {
        System.out.println("[開演唱會!收費標準:]" + money);
    }

    public void act(double money) {
        System.out.println("[拍戲!收費標準:]" + money);
    }
}
1.4.1.3 測試類中實現動態代理
public class Test_cglib {
    public static void main(String[] args) {
        //1.定義目標物件
        Star target = new Star();

        //2.對目標物件動態生成cglib代理物件
        Star proxy = (Star) Enhancer.create(
                Star.class,
                new MethodInterceptor() {
                    @Override
                    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                        // 獲取方法名稱
                        String methodName = method.getName();
                        // 獲取方法引數
                        double money = (double) args[0];
                        // 方法返回值
                        Object reValue = null;
                        // 判斷
                        if ("singing".equals(methodName)){
                            // 判斷
                            if (money > 100000) {
                                // 呼叫唱歌方法
                                reValue = method.invoke(target,args);
                            }else{
                                System.out.println("忙,沒空開演唱會!");
                            }
                        }
                        else if ("act".equals(methodName)) {
                            if (money > 1000000) {
                                // 呼叫目標物件
                                reValue = method.invoke(target,args);
                            } else {
                                System.out.println("忙,不接新戲!");
                            }
                        }
                        return reValue;
                    }
                }
        );
        //System.out.println(proxy);
        //實行代理方法
        proxy.singing(1);
        proxy.act(1234567);
    }
}

1.5 jdk動態代理和CGLIB動態代理的區別

  1. JDK動態代理只能對介面或實現了介面的類生成代理,而不能針對類;
  2. CGLIB是針對類實現代理,主要是對指定的類生成一個子類,重寫其中的方法。因為是繼承,所以該類或方法不能宣告成final 。

2. AOP概念

  • 官方概念:AOP(Aspect Oriented Programming):面向切面程式設計,通過預編譯方式和執行期動態代理實現程式功能的統一維護的一種技術。
  • 建議理解:使用動態代理技術,實現在不修改java原始碼的情況下,執行時實現方法功能的增強。

2.1 AOP作用及優勢

  • 作用:在程式執行期間,不修改原始碼對已有方法進行增強。
  • 優勢:減少重複程式碼,提高開發效率;統一管理統一呼叫,方便維護。

2.2 AOP的實現原理

  • 原理:動態代理技術
  • 在 spring 中,框架會根據目標類是否實現了介面來決定採用哪種動態代理的方式。如果目標物件實現介面,使用jdk代理;目標物件沒有實現介面使用cglib代理。

2.3 AOP的基本概念

  • Joinpoint(連線點)

    被增強功能的候選方法,spring只支援方法型別的連線點

  • Pointcut(切入點)

    指要攔截的方法;

    切入點表示式: 攔截方法,對滿足表示式的類,自動生成代理物件;

  • Advice(通知/增強)

    所謂通知是指攔截到 Joinpoint 之後所要做的事情就是通知。

    簡單而言,就是對原有功能新增新功能

    通知的型別: 前置通知、後置通知、異常通知、最終通知、環繞通知。

  • Target(目標物件)

    被代理的物件

  • Weaving(織入)

    指把增強功能用於目標物件,建立代理物件的過程;spring採用動態代理織入。

    簡單而言,將新功能新增到目標物件原有功能;

  • Proxy(代理)

    被織入增強後,產生一個結果代理類

  • Aspect(切面)

    切面指的是切入點和通知的結合

    簡單而言,就是專案中重複使用的程式碼

2.4 spring的AOP需要明確的事情【掌握】

2.4.1開發階段(我們做的)

  • 根據業務需求,編寫核心業務程式碼

  • 把公用程式碼抽取出來,製作成通知,通知所在的類就是切面類

  • 通過配置的方式,建立切入點(業務功能)和通知的關係

2.4.2 執行階段(spring框架完成)

  • spring框架監控切入點方法執行。
  • 一旦監控到切入點方法被執行,使用動態代理機制,建立目標物件的代理物件,根據通知型別,在代理物件當前執行方法的對應位置,織入通知功能,完成完整的程式碼邏輯執行。

3. AOP程式設計

案例需求:執行方法時自動記錄日誌資訊

3.1 基於xml的AOP配置【重點】

  • 先建立一個入門案例

3.1.1 建立專案、新增依賴

  • 新增ioc基礎框架包和AOP支援包(aspectjweaver包)

pom.xml檔案

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.7</version>
</dependency>

3.1.2 service層模擬實現儲存賬戶功能

3.1.2.1 service介面
public interface IAccountService {
    /*模擬儲存賬戶功能*/
    void save();
}
3.1.2.2 service實現類
public class AccountServiceImpl implements IAccountService {
    @Override
    public void save() {
        System.out.println("賬戶儲存!");
    }
}

3.1.3 建立記錄日誌的工具類Logger

/**
 * 由於記錄日誌是每個被呼叫方法都要執行的程式碼(重複程式碼),所以可以將程式碼抽取出來編寫成通知方法
 * 其他方法被執行的時會都會執行通知方法,也就是日誌記錄會被自動執行(需要配置)
 * 建立一個工具類用來記錄日誌(也可以成為切面類/通知類)
 */
public class Logger {
    /*將重複程式碼編寫記錄日誌方法*/
    public void printLog(){
        System.out.println("記錄使用者操作日誌...");
    }
}

3.1.4 bean.xml配置(重點)

<?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"
       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">

    <!--建立service物件-->
    <bean id="accountService" class="com.azure.service.impl.AccountServiceImpl"></bean>
    <!--建立記錄日誌的工具具類(切面類、通知類)-->
    <bean id="logger" class="com.azure.utils.Logger"></bean>
    <!--
        Aop 配置
        1.aop:pointcut 配置切入點表示式
          作用:spring在建立容器時候,對符合切入點表示式的類自動生成代理物件。
        2.aop:aspect 配置切面類
           ref 引用的切面類(日誌工具類)
           aop:before 前置通知,在執行目標物件方法之前執行
               method 對用logger切面類的方法
               pointcut-ref 引用的切入點表示式物件
        -->
    <aop:config>
        <!--設定切入點表示式-->
        <aop:pointcut id="pt" expression="execution(* com.azure.service.impl.AccountServiceImpl.save())"></aop:pointcut>
        <!--設定切面,裡面包含切入點表示式和通知-->
        <aop:aspect ref="logger">
            <!--前置通知,在執行目標物件方法之前執行-->
            <!--切入點表示式可以在通知標籤中定義,但為了複用性,故將切入點表示式抽取出來-->
            <aop:before method="printLog" pointcut-ref="pt"/>
        </aop:aspect>
    </aop:config>
    
</beans>

3.1.5 測試類

public class App {
    public static void main(String[] args) {
        // 建立容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        // 從容器中獲取service物件
        IAccountService accountService = (IAccountService) ac.getBean("accountService");
        // 檢視物件型別:class com.sun.proxy.$Proxy4,使用jdk代理。
        // 符合切入點表示式的類生成代理物件
        System.out.println(accountService.getClass());

        // 執行代理方法
        accountService.save();
    }
}

3.2 切入點表示式(重點)

  • 作用:對符合切入點表示式的類,會自動生成代理物件

3.2.1官方語法

在這裡插入圖片描述

3.2.2 九(種寫法)+一(種常用寫法)

  1. 最全的寫法
    假設攔截返回void,指定類的指定方法,引數有2個:int、String
    execution(public void com.azure.service.impl.AccountServiceImpl.save(int,java.lang.String))
  2. 省略訪問修飾符,返回值任意的指定類的save方法,無引數
    execution(* com.azure.service.impl.AccountServiceImpl.save())
  3. 返回值void,攔截com包下所有的類、以及其子包下所有的類的save()方法
    execution(void com..*.save()) 包名與類名或方法名稱都可以使用 *
  4. 返回值任意型別,攔截save()方法/攔截所有方法
    execution(* save()) 攔截save()
    execution(* *()) 攔截所有方法
  5. 不攔截save()方法
    !execution(* save())或者not execution(* save()) 注意not前要有空格!!
  6. 返回值任意型別,攔截save()方法或者update()方法,注意從邏輯角度,不能寫and
    execution(* save()) || execution(* update())"
    execution(* save()) or execution(* update())"
  7. 攔截所有方法,引數任意,但必須有引數
    execution(* *(*))
  8. 攔截所有方法,引數任意,引數可有可無
    execution(* *(..))*
  9. 對ioc容器中以Service結尾的類,生成代理物件
    bean(*Service)
  10. 最常用(重要)
    execution(* com.azure..*ServiceImpl.*(..))
    表示com.azure包及其所有子包下所有的以ServiceImpl結尾的類生成代理物件
  • 注意事項:

    ..:如果出現在類中表示包及其子包;如果出現在方法引數中,表示可以有引數也可以沒有引數

    *ServiceImpl:在類中,表示以ServiceImpl結尾的類。注意,*與ServiceImpl之間沒有空格

    *在返回值型別上,表示返回值為任意型別;在包名中,見上;在方法名上,表示所有方法。

3.2.2.1 例項程式碼
<?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"
       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">

    <!--建立service物件-->
    <bean id="accountService" class="com.azure.service.impl.AccountServiceImpl"></bean>
    <!--建立記錄日誌的工具具類(切面類、通知類)-->
    <bean id="logger" class="com.azure.utils.Logger"></bean>
    <!--
        Aop 配置
        1.aop:pointcut 配置切入點表示式
          作用:spring在建立容器時候,對符合切入點表示式的類自動生成代理物件。
        2.aop:aspect 配置切面類
           ref 引用的切面類(日誌工具類)
           aop:before 前置通知,在執行目標物件方法之前執行
               method 對用logger切面類的方法
               pointcut-ref 引用的切入點表示式物件
        -->
    <aop:config>
        <!--設定切入點表示式-->
        <!--1.最全的寫法
            攔截返回void,指定類的指定方法,引數必須有2個:int、String
            execution(public void com.azure.service.impl.AccountServiceImpl.save(int,java.lang.String))
        
            2.省略訪問修飾符,返回值任意的指定類的save方法,無引數
            execution(* com.azure.service.impl.AccountServiceImpl.save())
        
            3.攔截com包下所有的類、以及其子包下所有的類的save()方法
            execution(void com..*.save())  包名與類名或方法名稱都可以使用 *
        
            4.攔截save()方法/攔截所有方法
            execution(* save()) 攔截save()
            execution(* *())    攔截所有方法
        
            5.不攔截save()方法
            !execution(* save())
              not execution(* save()) 注意not前要有空格
        
            6.攔截save()方法或者update()方法
            execution(* save()) || execution(* update())"
            execution(* save()) or execution(* update())"
        
            7.攔截所有方法,引數任意,但必須有引數
            execution(* *(*))
            8.攔截所有方法,引數任意,引數可有可無
            execution(* *(..))
            9.對ioc容器中以Service結尾的類,生成代理物件
            bean(*Service)
            10.最常用
            execution(* com.azure..*ServiceImpl.*(..))
            表示com.azure包及其所有子包下所有的以ServiceImpl結尾的類生成代理物件。          
        -->
        <aop:pointcut id="pt" expression="execution(* com.azure..*ServiceImpl.*(..))"></aop:pointcut>
        <!--設定切面,裡面包含切入點表示式和通知-->
        <aop:aspect ref="logger">
            <!--前置通知,在執行目標物件方法之前執行-->
            <!--切入點表示式可以在通知標籤中定義,但為了複用性,故將切入點表示式抽取出來-->
            <aop:before method="printLog" pointcut-ref="pt"/>
        </aop:aspect>
    </aop:config>

</beans>

3.3 常用標籤說明

3.3.1 <aop:config>

  • 作用:宣告aop配置。

3.3.2 <aop:aspect>

  • 作用:配置切面。

  • 屬性:

    • id:唯一標識切面的名稱
    • ref:引用通知類bean的id

3.3.3 <aop:pointcut>

  • 作用:配置切入點表示式。

  • 屬性:

    • id:唯一標識切入點表示式名稱
    • expression:定義切入點表示式

3.3.4 <aop:before>

  • 作用:配置前置通知

  • 屬性:

    • method:指定通知方法名稱
    • pointcut:定義切入點表示式
    • pointcut-ref:引用切入點表示式的id

3.3.5 <aop:after-returning>

  • 作用:配置後置通知

  • 屬性:

    • method:指定通知方法名稱
    • pointcut:定義切入點表示式
    • pointcut-ref:引用切入點表示式的id

3.3.6 <aop:after-throwing>

  • 作用:配置異常通知

  • 屬性:

    • method:指定通知方法名稱
    • pointcut:定義切入點表示式
    • pointcut-ref:引用切入點表示式的id

3.3.7 <aop:after>

  • 作用:配置最終通知

  • 屬性:

    • method:指定通知方法名稱
    • pointcut:定義切入點表示式
    • pointcut-ref:引用切入點表示式的id

3.3.8 <aop:around>

  • 作用:配置環繞通知

  • 屬性:

    • method:指定通知方法名稱
    • pointcut:定義切入點表示式
    • pointcut-ref:引用切入點表示式的id

3.4 通知型別(重點)

3.4.1 通知型別

  • 前置通知:在目標方法執行前執行

  • 後置通知:在目標方法正常返回後執行。它和異常通知只能執行一個

  • 異常通知:在目標方法發生異常後執行。它和後置通知只能執行一個