1. 程式人生 > 其它 ># 二十、反射(完結)

# 二十、反射(完結)

二十、反射


20.1 類的載入


20.1.1 類的載入概述

程式執行後,某個類在第一次使用時,會將該類的 class 檔案讀取到記憶體,並將此類的所有資訊儲存到一個Class 物件中

20.1.2 類載入的時機

  1. 建立類的例項
  2. 訪問類的成員
  3. 使用反射方式來強制建立某個類或介面對應的 java.lang.Class物件。
  4. 初始化某個類的子類,父類會先載入。
  5. 直接使用 java.exe 命令來執行某個主類。
    總結:用到就載入,不用就不載入

20.1.3 類載入的過程介紹

當一個**Java**** 檔案要被載入到記憶體中使用執行的過程**

  1. 需要把當前的 Java 檔案通過 Javac 編譯成位元組碼檔案(.class
    檔案)
  2. 位元組碼檔案需要進行 載入 , 連線 , 初始化 三個動作
  3. 位元組碼資料載入到 jvm 方法區記憶體中, 在堆記憶體中建立此類的物件 , Java 程式進行使用

注意 : 把一個位元組碼檔案載入到記憶體過程 , 就需要用到類載入進行完成

20.2 類的載入過程各階段的任務


Java 程式中需要使用到某個類時,虛擬機器會保證這個類已經被載入、連線和初始化。而連線又包含驗證、準備和解析這三個子過程,這個過程必須嚴格按照順序執行

20.2.3 載入

  • 通過類的全類名(包名和類名) , 查詢此類的位元組碼檔案,把類的.class 檔案中的二進位制資料流讀入到記憶體中,並存放在執行時資料區的方法區內,然後利用位元組碼檔案建立一個 Class
    物件,用來封裝類在方法區內的資料結構並存放在堆區內。
  • 簡單來說 : 就是把硬碟中的位元組碼檔案 , 載入到 jvm 中的方法區以位元組碼物件的形式存在 , 並在堆記憶體中建立此類物件

20.2.2 連線

  1. 驗證 : 確保被載入類的正確性。class 檔案的位元組流中包含的資訊符合當前虛擬機器要求,不會危害虛擬機器自身的安全。
  2. 準備 : 為類的靜態變數分配記憶體,並將其設定為預設值。此階段僅僅只為靜態類變數(即 static 修飾的欄位變數)分配記憶體,並且設定該變數的初始值。(比如 static int num = 5 ,這裡只將 num 初始化為0,5的值將會在初始化時賦值)。對於 final static
    修飾的變數,編譯的時候就會分配了,也不會分配例項變數的記憶體。
  3. 解析 : 把類中的符號引用轉換為直接引用。符號引用就是一組符號來描述目標,而直接引用就是直接指向目標在記憶體的位置,即地址,如果引用的這個類沒載入進記憶體就會先載入,載入了就直接替換。
  • 簡單理解就是如果當前類中用到了其他類, 就他符號引用替換成其他類

20.2.3 初始化

  • 類載入最後階段,為靜態變數賦值,靜態程式碼塊的程式碼也將被初始化
  • 若該類具有父類,則先對父類進行初始化
  • 底層的初始化方法加了鎖,做了執行緒同步校驗:如果多個執行緒同時對一個類繼續初始化,一次只有一個執行緒會執行,其他執行緒會阻塞等待。

20.3 類載入器


20.3.1 Java虛擬機器自帶的類載入器 (瞭解)

類載入器:是負責將磁碟上的某個class檔案讀取到記憶體並生成Class的物件。

Java 中有三種類載入器,它們分別用於載入不同種類的 class

  • 啟動類載入器( Bootstrap ClassLoader ):用於載入系統類庫 <JAVA_HOME>\bin目錄下的class ,例如:rt.jar
  • 擴充套件類載入器( Extension ClassLoader ):用於載入擴充套件類庫 <JAVA_HOME>\lib\ext 目錄下的 class
  • 應用程式類載入器( Application ClassLoader ):用於載入我們自定義類的載入器。
public class Test{
  public static void main(String[] args){
      //通過Class物件,獲取類載入器的方法 --> public ClassLoader getClassLoader() : 返回該類的類載入器
      //如果該類由啟動類載入器載入(例如核心類庫中的類String 等),則將返回 null。 
      
    System.out.println(Test.class.getClassLoader());//sun.misc.Launcher$AppClassLoader
    System.out.println(String.class.getClassLoader());//null
  }
}

總結:

  • 在程式開發中,類的載入幾乎是由上述3種類載入器相互配合執行的,同時我們還可以自定義類載入器
  • 需要注意的是,java 虛擬機器對class 檔案採用的是按需載入的方式,也就是說當需要使用該類時才會將它的 class 檔案載入到記憶體中生成 class 物件
  • 而且載入某個類的 class 檔案時,Java 虛擬機器採用的是雙親委派模式,即把載入類的請求交由父載入器處理,它是一種任務委派模式

20.3.2 類載入器--雙親委派機制介紹


上圖展示了"類載入器"的層次關係,這種關係稱為類載入器的 "雙親委派模型":

  • "雙親委派模型"中,除了頂層的啟動類載入器外,其餘的類載入器都應當有自己的"父級類載入器"。
  • 這種關係不是通過"繼承"實現的,通常是通過"組合"實現的。通過"組合"來表示父級類載入器。
  • "雙親委派模型"的工作過程
    • 某個"類載入器"收到類載入的請求,它首先不會嘗試自己去載入這個類,而是把請求交給父級類載入器。
    • 因此,所有的類載入的請求最終都會傳送到頂層的"啟動類載入器"中。
    • 如果"父級類載入器"無法載入這個類,然後子級類載入器再去載入。
public class ClassLoaderDemo4 {
    /*
        當前是根據自己定義的類進行獲取的類載入器物件
        getParent方法獲取父類載入器
        第一條輸出語句列印的是 : 系統類載入器
        第二條輸出語句列印的是 : 擴充套件類載入器
        第三條輸出語句列印的是 : null是根類載入器
     */
public static void main(String[] args) {
        ClassLoader classLoader = ClassLoaderDemo4.class.getClassLoader();
        System.out.println(classLoader);// sun.misc.Launcher$AppClassLoader@b4aac2
        System.out.println(classLoader.getParent());// sun.misc.Launcher$ExtClassLoader@16d3586
        System.out.println(classLoader.getParent().getParent());// null
}

20.3.3 雙親委派模型的優點

  • 避免類的重複載入
    • 當父類載入器已經載入了該類時,就沒有必要子 ClassLoader 再載入一次 , 位元組碼檔案只加載一次
  • 考慮到安全因素,java 核心 api 中定義型別不會被隨意替換,假設通過網路傳遞一個名為 java.lang.Object 的類,通過雙親委託模式傳遞到啟動類載入器,而啟動類載入器在核心 Java API 發現這個名字的類,發現該類已被載入,並不會重新載入網路傳遞過來的 java.lang.Object ,而直接返回已載入過的 Object.class這樣便可以防止核心 **API** 庫被隨意篡改!!!
package java.lang;

/**
 * @author: Carl Zhang
 * @create: 2022-01-07 13:59
 *  舉例 : 如果我們自己定義一個包名字叫做java.lang , 在當前這個包下建立一個類叫做MyObject類
 *  因為java.lang包屬於核心包,只能由根類載入器進行載入,而根據類載入的雙親委派機制,根類載入不到這個MyObject類的(自定義的)**
 *  所以只能由AppClassLoader進行載入,而這又是不允許的,因為java.lang下的類需要使用根載入器進行載入**
 *  所以會報出"Prohilbited package name:java.lang"(禁止的包名)異常"
 */
public class MyObject {
    public static void main(String[] args) {
        System.out.println(MyObject.class.getClassLoader()); //java.lang.SecurityException: Prohibited package name: java.lang
    }
}

20.4 反射的介紹


20.4.1 反射的引入

  • 需求:如何根據配置檔案 re.properties 裡的不同的資訊,建立指定的物件,呼叫各自的方法
  • 以前的方式:讀取配置檔案裡的資訊,通過 switch 格式判斷class 鍵對應的值,建立不同的物件,呼叫方法
  • 問題:程式碼冗餘,不方便
  • 引入反射:實現動態建立物件,動態呼叫方法,這樣的需求在學習框架時特別多,即通過外部檔案配置,在不修改原始碼情況下,來控制程式,也符合設計模式的 ocp 原則(開閉原則:不修改原始碼,擴容功能)

20.4.2 反射的概念

  • 反射是一種機制,利用該機制可以在程式執行過程中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意方法和屬性
  • 利用反射可以無視修飾符獲取類裡面所有的屬性和方法。先獲取配置檔案中的資訊,動態獲取資訊並建立物件和呼叫方法

20.4.3 反射的使用前提和場景

  • 使用反射操作類成員的前提
    • 要獲得該類位元組碼檔案物件,就是 Class 物件
  • 反射在實際開發中的應用:
    • 開發 IDE (整合開發環境),比如 IDEAEclipse
    • 各種框架的設計和學習 比如 SpringHibernateStructMybaits ....

20.5 Class物件的獲取方式


20.5.1 三種獲取方法

  • 方式1: 類名.class 獲得

  • 方式2:物件名.getClass() 方法獲得,該方法是繼承 Object 類的

  • 方式3:Class 類的靜態方法獲得: static Class forName("類全名")

    • 每一個類的 Class 物件都只有一個。
  • 示例程式碼

package com.itheima._03反射;
public class Student{
    
}
public class ReflectDemo01 {
    public static void main(String[] args) throws ClassNotFoundException {
        // 獲得Student類對應的Class物件
        Class c1 = Student.class;

        // 建立學生物件
        Student stu = new Student();
        // 通過getClass方法
        Class c2 = stu.getClass();
        System.out.println(c1 == c2);

        // 通過Class類的靜態方法獲得: static Class forName("類全名")
        Class c3 = Class.forName("com.itheima._03反射.Student");
        System.out.println(c1 == c3); //true
        System.out.println(c2 == c3); //true
    }
}

20.5.2 Class類常用方法

  • String getSimpleName(); 獲得類名字串:類名
  • String getName(); 獲得類全名:包名+類名
  • T newInstance() ; 建立 Class物件關聯類的物件

示例程式碼

public class ReflectDemo02 {
    public static void main(String[] args) throws Exception {
        // 獲得Class物件
        Class c = Student.class;
        // 獲得類名字串:類名
        System.out.println(c.getSimpleName());
        // 獲得類全名:包名+類名
        System.out.println(c.getName());
        // 建立物件
        //此方式相當於通過類的空參構造建立物件,如果目標類沒有空參構造,會報錯 InstantiationException
        Student stu = (Student) c.newInstance();
        System.out.println(stu);
    }
}

20.6 反射之操作構造方法


20.6.1 Constructor類概述

  • 反射之操作構造方法的目的
    • 獲得 Constructor 物件來建立類的物件。
  • Constructor 類概述
    • 類中的每一個構造方法都是一個 Constructor 類的物件

20.6.2 Class類中與Constructor相關的方法

  • Constructor getConstructor(Class... parameterTypes) :返回單個公共構造方法物件
  • Constructor getDeclaredConstructor(Class... parameterTypes) :返回單個構造方法物件,不受修飾符影響
  • Constructor[] getConstructors() :返回所有公共構造方法物件的陣列,只能獲得 public
  • Constructor[] getDeclaredConstructors() :返回所有構造方法物件的陣列,不受修飾符影響

20.6.3 Constructor物件常用方法

  • T newInstance(Object... initargs) —— 根據指定的引數建立物件
  • void setAccessible(true) :設定"暴力反射"——是否取消許可權檢查,true 取消許可權檢查,false 表示不取消

示例程式碼

public class Student{
   private String name;
   private String sex;
   private int age;
   
   //公有構造方法
   public Student(String name,String sex,int age){
       this.name = name;
       this.sex = sex;
       this.age = age;
   }
   //私有構造方法
   private Student(String name,String sex){
       this.name = name;
       this.sex = sex;
   }
}
public class ReflectDemo03 {

    /*
      Constructor[] getConstructors()
           獲得類中的所有構造方法物件,只能獲得public的
      Constructor[] getDeclaredConstructors()
            獲得類中的所有構造方法物件,包括private修飾的
    */
    @Test
    public void test03() throws Exception {
        // 獲得Class物件
        Class c = Student.class;
        //  獲得類中的所有構造方法物件,只能獲得public的
        // Constructor[] cons = c.getConstructors();
        // 獲取類中所有的構造方法,包括public、protected、(預設)、private的
        Constructor[] cons = c.getDeclaredConstructors();
        for (Constructor con:cons) {
            System.out.println(con);
        }
    }

    /*
       Constructor getDeclaredConstructor(Class... parameterTypes)
           根據引數型別獲得對應的Constructor物件
    */
    @Test
    public void test02() throws Exception {
        // 獲得Class物件
        Class c = Student.class;
        // 獲得兩個引數構造方法物件
        Constructor con = c.getDeclaredConstructor(String.class,String.class);
        // 取消許可權檢查(暴力反射)
        con.setAccessible(true);
        // 根據構造方法建立物件
        Object obj = con.newInstance("rose","女");
        System.out.println(obj);
    }

    /*
        Constructor getConstructor(Class... parameterTypes)
            根據引數型別獲得對應的Constructor物件
     */
    @Test
    public void test01() throws Exception {
        // 獲得Class物件
        Class c = Student.class;
        // 獲得無引數構造方法物件
        Constructor con = c.getConstructor();
        // 根據構造方法建立物件
        Object obj = con.newInstance();
        System.out.println(obj);

        // 獲得有引數的構造方法物件
        Constructor con2 = c.getConstructor(String.class, String.class,int.class);
        // 建立物件
        Object obj2 = con2.newInstance("jack", "男",18);
        System.out.println(obj2);
    }
}

20.7 反射之操作成員方法


20.7.1 Method類概述

  • 反射之操作成員方法的目的
    • 操作 Method 物件來呼叫成員方法
  • Method 類概述
    • 每一個成員方法都是一個 Method 類的物件。

20.7.2 Class類中與Method相關的方法

  • Method getMethod(String name,Class...args); 返回單個公共的成員方法物件
  • Method getDeclaredMethod(String name,Class...args); 返回單個的成員方法物件,不受訪問修飾符限制
  • Method[] getMethods(); 返回所有公共的成員方法物件,包括繼承的
  • Method[] getDeclaredMethods(); 返回所有的成員方法物件,不包括繼承的

20.7.3 Method物件常用方法

  • Object invoke(Object obj, Object... args)
    • 呼叫指定物件 obj 的該方法
    • args: 呼叫方法時傳遞的引數
    • 沒有返回值,則返回 null
  • void setAccessible(true)
    設定"暴力訪問"——是否取消許可權檢查,true 取消許可權檢查,false 表示不取消

示例程式碼

public class Student{
    private void eat(String str){
        System.out.println("我吃:" + str);
    }
    
    private void sleep(){
        System.out.println("我睡覺...");
    }
    
    public void study(int a){
        System.out.println("我學習Java,引數a = " + a);
    }
}
public class ReflectDemo04 {

    // 反射操作靜態方法
    @Test
    public void test04() throws Exception {
        // 獲得Class物件
        Class c = Student.class;
        // 根據方法名獲得對應的公有成員方法物件
        Method method = c.getDeclaredMethod("eat",String.class);
        // 通過method執行對應的方法
        method.invoke(null,"蛋炒飯");
    }

    /*
     * Method[] getMethods();
        * 獲得類中的所有成員方法物件,返回陣列,只能獲得public修飾的且包含父類的
     * Method[] getDeclaredMethods();
        * 獲得類中的所有成員方法物件,返回陣列,只獲得本類的,包含private修飾的
   */
    @Test
    public void test03() throws Exception {
        // 獲得Class物件
        Class c = Student.class;
        // 獲得類中的所有成員方法物件,返回陣列,只能獲得public修飾的且包含父類的
        // Method[] methods = c.getMethods();
        // 獲得類中的所有成員方法物件,返回陣列,只獲得本類的,包含private修飾的
        Method[] methods = c.getDeclaredMethods();
        for (Method m: methods) {
            System.out.println(m);
        }

    }

    /*
       Method getDeclaredMethod(String name,Class...args);
           * 根據方法名和引數型別獲得對應的構造方法物件,
    */
    @Test
    public void test02() throws Exception {
        // 獲得Class物件
        Class c = Student.class;

        // 根據Class物件建立學生物件
        Student stu = (Student) c.newInstance();
        // 獲得sleep方法對應的Method物件
        Method m =  c.getDeclaredMethod("sleep");
        // 暴力反射
        m.setAccessible(true);

        // 通過m物件執行stuy方法
        m.invoke(stu);
    }

    /*
        Method getMethod(String name,Class...args);
            * 根據方法名和引數型別獲得對應的構造方法物件,
     */
    @Test
    public void test01() throws Exception {
        // 獲得Class物件
        Class c = Student.class;
        
        // 根據Class物件建立學生物件
        Student stu = (Student) c.newInstance();
        // 獲得study方法對應的Method物件
        Method m =  c.getMethod("study");
        // 通過m物件執行stuy方法
        m.invoke(stu);


        /// 獲得study方法對應的Method物件
        Method m2  = c.getMethod("study", int.class);
        // 通過m2物件執行stuy方法
        m2.invoke(stu,8);
    }
}

20.8 反射之操作成員變數


20.8.1 Field類概述

  • 反射之操作成員變數的目的
    • 通過 Field 物件給對應的成員變數賦值和取值
  • Field 類概述
    • 每一個成員變數都是一個 Field 類的物件。

20.8.2 Class類中與Field相關的方法

  • Field getField(String name); 返回單個公共的成員變數物件
  • Field getDeclaredField(String name); 返回單個成員變數,不受修飾符限制
  • Field[] getFields(); 返回所有的公共的成員變數物件
  • Field[] getDeclaredFields(); 返回所有的成員變數物件

20.8.3 Field物件常用方法

  • void set(Object obj, Object value) :給物件obj 的屬性設定值
  • Object get(Object obj) :獲取物件obj 對應的屬性值
  • void setAccessible(true); 暴力反射,設定為可以直接訪問私有型別的屬性。
  • Class getType(); 獲取屬性的型別,返回 Class 物件。

示例程式碼

public class Student{
    public String name; 
    private String gender;
    
    public String toString(){
        return "Student [name = " + name + " , gender = " + gender + "]";
    }
}
public class ReflectDemo05 {
    /*
        Field[] getFields();
            * 獲得所有的成員變數對應的Field物件,只能獲得public的
        Field[] getDeclaredFields();
            * 獲得所有的成員變數對應的Field物件,包含private的
     */
    @Test
    public void test02() throws Exception {
        // 獲得Class物件
        Class c  = Student.class;
        // 獲得所有的成員變數對應的Field物件
        // Field[] fields = c.getFields();
        // 獲得所有的成員變數對應的Field物件,包括private
        Field[] fields = c.getDeclaredFields();
        for (Field f: fields) {
            System.out.println(f);
        }
    }

    /*
        Field getField(String name);
            根據成員變數名獲得對應Field物件,只能獲得public修飾
        Field getDeclaredField(String name);
            *  根據成員變數名獲得對應Field物件,包含private修飾的
     */
    @Test
    public void test01() throws Exception {
        // 獲得Class物件
        Class c  = Student.class;
        // 建立物件
        Object obj = c.newInstance();
        // 獲得成員變數name對應的Field物件
        Field f = c.getField("name");
        // 給成員變數name賦值
        // 給指定物件obj的name屬性賦值為jack
        f.set(obj,"jack");

        // 獲得指定物件obj成員變數name的值
        System.out.println(f.get(obj)); // jack
        // 獲得成員變數的名字
        System.out.println(f.getName()); // name


        // 給成員變數gender賦值
        // 獲得成員變數gender對應的Field物件
        Field f1 = c.getDeclaredField("gender");
        // 暴力反射
        f1.setAccessible(true);
        // 給指定物件obj的gender屬性賦值為男
        f1.set(obj,"男");

        System.out.println(obj);

    }
}

20.9 使用反射解析註解

注:註解的基本介紹見 11.6 註解介紹

20.9.1 AnnotatedElement介面介紹

AnnotatedElement : 是一個介面,定義瞭解析註解的方法

1. boolean isAnnotationPresent(Class<Annotation> annotationClass)
   引數 : 註解的位元組碼物件
   作用 : 判斷當前物件是否使用了指定的註解,如果使用了則返回true,否則false
  
2. T getAnnotation(Class<T> annotationClass) 
   引數 : 註解的位元組碼物件
   返回值 : 根據註解型別獲得對應註解物件 , 有了註解物件就可以呼叫屬性(抽象方法),獲取屬性值

20.9.2 註解的解析原理

前提:Class,Constructor,Method,Field都實現了AnnotatedElement 介面。

解析註解的原理:獲取註解作用位置的物件,來呼叫方法解析註解

  • 解析類上的註解:藉助位元組碼物件(Class物件)
  • 解析構造方法上的註解 :藉助構造器物件(Constructor物件)
  • 解析方法上的註解 :藉助方法物件(Method物件)
  • 解析欄位上的註解 :藉助欄位物件(Field物件)

相關方法:

  • isAnnotationPresent() :判斷是否存在註解
  • getAnnotation():如果存在獲取註解物件

20.9.3 註解的解析案例

需求如下:

1. 定義註解 `Book,要求如下:
   - 包含屬性:String value()   書名
   - 包含屬性:double price()  價格,預設值為 100
   - 包含屬性:String[] authors() 多位作者  
   - 限制註解使用的位置 :類和成員方法上 【Target】
   - 指定註解的有效範圍 :RUNTIME  【Retention】
2. 定義BookStore類,在類和成員方法上使用Book註解
3. 定義TestAnnotation測試類獲取Book註解上的資料

給成員注入四大名著資訊 :
    西遊記     --- 施耐庵
    水滸傳     --- 吳承恩
    三國演義   --- 羅貫中
    紅樓夢     --- 曹雪芹 , 高鶚

Book註解 :

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

//定義一個書的註解,包含屬性書名,價格(預設100),作者。作者要求可以有多名作者
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Book {

    String value();

    double price() default 100;

    String[] author();

}

BookStore類 :

@Book(value = "水滸傳", author = "施耐庵")
public class BookStore {

    @Book(value = "三國演義", author = {"羅貫中"})
    public void buy() {

    }
}

**TestAnnotation**** 類:**

import java.lang.reflect.Method;
import java.util.Arrays;
/**
	思路:
	1. 型別上的註解,使用Class物件解析
	2. 方法上的註解,使用Method物件解析
*/

public class TestAnnotation {
    public static void main(String[] args) throws NoSuchMethodException {

        //解析型別上的註解
        Class<BookStore> cls = BookStore.class;
        //判斷是否有Book註解,如果有進行解析
        if (cls.isAnnotationPresent(Book.class)) {
            //有
            Book book = cls.getAnnotation(Book.class);
            String name = book.value();
            double price = book.price();
            String[] author = book.author();
            System.out.println(name);
            System.out.println(price);
            System.out.println(Arrays.toString(author));
        }

        //解析方法上面的註解
        Method buyMethod = cls.getMethod("buy");
        if (buyMethod.isAnnotationPresent(Book.class)) {
            Book book = buyMethod.getAnnotation(Book.class);
            String name = book.value();
            double price = book.price();
            String[] author = book.author();
            System.out.println(name);
            System.out.println(price);
            System.out.println(Arrays.toString(author));
        }
    }
}

20.10 設計模式 - 代理模式


20.10.1 代理模式介紹

為什麼要有 “代理” ?

  • 生活中就有很多例子,比如委託業務,黃牛(票販子)等等
  • 代理就是被代理者沒有能力或者不願意去完成某件事情,需要找個人代替自己去完成這件事,這才是“代理”存在的原因。
  • 例如要租房子,房產中介可以在我們住房前代理我們找房子。中介就是代理,而自己就是被代理了。

在程式碼設計中,代理模式作用主要就是讓 "被代理物件" 的某個方法執行之或者執行之加入其他增強邏輯。

前增強 : 例如獲取當前時間

被代理物件呼叫方法

後增強 : 例如獲取當前時間

計算方法執行的時間

20.10.2 代理的前提條件

  • 抽象角色 :宣告功能 ,相當於父介面
  • 代理角色 :實現抽象功能 , 完成代理邏輯,相當於介面的實現類
  • 被代理角色 :實現抽象功能,相當於介面的實現類

意味著被代理角色和代理角色有著共同的父型別(既抽象角色) , 例如我要租房子, 我只能找房產中介, 不能找票販子

20.10.3 代理模式的兩種實現方式

  • 靜態代理 (瞭解 , 用於對動態代理做鋪墊)
  • 動態代理 (為後面學習的框架做鋪墊)

20.11 靜態代理模式


20.11.1 靜態代理模式的介紹

  • 靜態代理是由程式設計師建立 或 工具生成代理類的原始碼,再編譯代理類。在程式執行前就已經存在代理類的位元組碼檔案,代理類和被代理類的關係在執行前就確定了。
  • 簡單理解 : 在程式執行之前 , 代理類就存在了,這就是靜態代理 ; 動態代理是程式執行時動態生成代理類

20.11.2 靜態代理實現的步驟

  • 存在一個抽象角色
  • 定義被代理角色
  • 定義代理,增強被代理角色的功能

20.11.3 靜態代理案例

案例:以現實中經紀人代理明星

已知存在介面:

// 1.抽象角色
interface Star {
    // 真人秀方法
    double liveShow(double money);

    void sleep();
}

定義被代理類:

  • 定義王寶強類,實現Star方法

// 定義被代理角色(寶強)
class BaoQiang implements Star {

    @Override
    public double liveShow(double money) {
        System.out.println("參加了真人秀節目 , 賺了" + money + "元");
        return money;
    }

    @Override
    public void sleep() {
        System.out.println("寶強累了 , 睡覺了...");
    }
}

定義代理類:

  • 定義宋喆經紀人類
// 定義代理角色(宋喆),增強被代理角色的功能
class SongZhe implements Star {
    BaoQiang baoQiang = new BaoQiang();

    @Override
    public double liveShow(double money) {// 1000
        // 前增強
        System.out.println("宋喆幫寶強接了一個真人秀的活動,獲取佣金" + money * 0.8 + "元");

        double v = baoQiang.liveShow(money * 0.2);

        // 後增強
        System.out.println("宋喆幫寶強把賺的錢存起來...");

        return v;
    }

    @Override
    public void sleep() {
        System.out.println("宋喆幫寶強找了一家五星級大酒店...");
        baoQiang.sleep();
        System.out.println("宋喆幫寶強退房..");
    }
}

定義測試類進行測試

public class StaticAgentDemo {
    public static void main(String[] args) {
        // 被代理角色
        BaoQiang baoQiang = new BaoQiang();
        double v = baoQiang.liveShow(1000);
        System.out.println(v);
        baoQiang.sleep();

        System.out.println("===========================");

        SongZhe songZhe = new SongZhe();
        double v1 = songZhe.liveShow(1000);
        System.out.println(v1);
        songZhe.sleep();
    }
}

關係圖 :宋喆和寶強都有共同的父型別。他們的業務方法都是一樣。

20.11.4 靜態代理和裝飾模式的區別

相同:

  • 都要實現與目標類相同的業務介面
  • 在倆個類中都要宣告目標物件

不同

  • 目標不同 :
    • 裝飾者模式考慮的是物件某個功能的擴充套件,是在原有功能基礎上增加
    • 靜態代理模式考慮的是物件某個功能的呼叫,對這個功能的流程把控和輔助
  • 物件獲取方式不同
    • 裝飾者模式是通過構造方法的傳參來獲取被裝飾的物件
    • 靜態代理模式是內部直接建立被裝飾的物件

注意:設計模式本身是為了提升程式碼的可擴充套件性,靈活應用即可,不必生搬硬套,非要分出個所以然來,裝飾器模式和代理模式的區別也是如此

20.12 動態代理模式

20.12.1 動態代理模式介紹

  • 在實際開發過程中往往我們自己不會去建立代理類而是通過JDK 提供的Proxy類在程式執行時,運用反射機制動態建立而成這就是我們所謂的動態代理
  • 與靜態代理之間的區別,在於不用自己寫代理類
  • 雖然我們不需要自己定義代理類建立代理物件,但是我們要定義對被代理物件直接訪問方法的攔截,原因就是對攔截的方法做增強
  • 動態代理技術在框架中使用居多,例如:很快要學到的資料庫框架 MyBatis 框架等後期學的一些主流框架技術(SpringSpringMVC )中都使用了動態代理技術。

20.12.2 動態代理相關API

  • Proxy類
    • java.lang.reflect.Proxy類提供了用於建立動態代理類和物件的靜態方法
    • 它還是由這些方法建立的所有動態代理類的超類(代理類的父類是 Proxy )。
//獲取代理物件的方法
public static Object newProxyInstance (ClassLoader loader, Class<?>[] interfaces, InvocationHandler h )  

/**解析:
- 返回值:該方法返回就是動態生成的代理物件

- 引數列表說明:
  1. ClassLoader loader 	- 定義代理類的類載入器 = 被代理物件.getClass().getClassLoader();
  2. Class<?>[] interfaces 	- 代理類要實現的介面列表,要求與被代理類的介面一樣。 = 被代理物件.getClass().getInterfaces();
  3. InvocationHandler h 	- 呼叫處理器:就是具體實現代理邏輯的介面
*/
  • **InvocationHandler**** 介面**
    • java.lang.reflect.InvocationHandler是代理物件實際處理代理邏輯的介面,具體代理實現邏輯在其 invoke 方法中。
    • 所有代理物件呼叫的方法,執行是都會經過**invoke**。因此如果要對某個方法進行代理增強,就可以在這個 invoke 方法中進行定義。
interface InvocationHandler{
	public Object invoke(Object proxy, Method method, Object[] args);  //代理邏輯
    
    1. 返回值:方法被代理後執行的結果。
    2. 引數列表:
   		1. proxy  - 就是代理物件
  		2. method - 
  	 	3. args   - 代理物件呼叫方法傳入引數值的物件陣列.
}

20.12.3 動態代理案例

需求:將經紀人代理明星的案例使用動態代理實現

分析

  1. 把父介面定義
  2. 定義被代理類:寶強
  3. 動態生成代理類物件
  4. 建立執行代理邏輯的呼叫處理器
  5. 通過代理物件執行代理方法
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @author Carl Zhang
 * @description 動態代理模式對BaoQiang的show方法進行處理
 * @date 2022/1/8 14:37
 */
public class DynamicProxy {
    public static void main(String[] args) throws ClassNotFoundException {
        //1.獲取被代理的物件
        BaoQiang baoQiang = new BaoQiang();

        Class<?> aClass = aClass = baoQiang.getClass(); //被代理類的位元組碼物件
        ClassLoader classLoader = aClass.getClassLoader(); //被代理類的類載入器
        Class<?>[] interfaces = aClass.getInterfaces(); //被代理類實現的所以的介面的陣列

        //獲取自定義的呼叫處理器物件 -- 即真正執行代理邏輯的物件
        MyInvocationHandler myInvocationHandler = new MyInvocationHandler(baoQiang);

        //2. 獲取被代理類的代理物件
        Star songZhe = (Star) Proxy.newProxyInstance(classLoader,
                interfaces, myInvocationHandler);

        //3. 通過代理物件執行要代理的方法 -- 此處會執行呼叫處理器的invoke方法
        //double v = songZhe.liveShow(1000); -- 如果方法執行完返回null,會報空指標異常
        songZhe.liveShow(1000);
        songZhe.sleep();
    }
}

/**
 * 建立代理物件的呼叫處理器 -- 用來執行代理的邏輯
 */
@SuppressWarnings("ALL")
class MyInvocationHandler implements InvocationHandler {
    BaoQiang baoQiang; //被代理的物件

    public MyInvocationHandler(BaoQiang baoQiang) {
        this.baoQiang = baoQiang;
    }

    /**
     * 代理行為 - 代理物件的所有方法都會執行此處
     * @param proxy  代理物件,即songZhe
     * @param method 被代理的方法 , 即BaoQiang的show方法的Method物件
     * @param args   被代理方法的引數
     * @return 代理方法執行後的結果
     * @throws Throwable 異常
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object invoke = null; //用來儲存被代理的方法執行的結果

        //如果是show方法就執行代理邏輯
        if (method.getName().equals("liveShow")) {
            double money = (double) args[0];

            //前增強
            System.out.println("宋喆幫寶強接真人秀,賺了" + money * 0.2);

            //檢視proxy的執行時型別 -- com.heima.agent.$Proxy0 ,匿名內部類 即動態建立的代理類
            //System.out.println(proxy.getClass().getName());

            //被代理物件執行被代理方法 -- 即baoqiang執行liveShow方法
            //Object money = method.invoke(proxy, args[0]);
            invoke = method.invoke(baoQiang, money * 0.8);

            //後增強
            System.out.println("宋哲幫寶強存錢");
            return invoke;
        }

        invoke = method.invoke(baoQiang, args);
        return invoke;
    }
}


//1.抽象角色
interface Star {
    double liveShow(double money);

    void sleep();
}

//2.被代理角色
class BaoQiang implements Star {

    @Override
    public double liveShow(double money) {
        System.out.println("寶強參加了一個真人秀節目,賺了" + money + "元");
        return money;
    }

    @Override
    public void sleep() {
        System.out.println("寶強累了,睡覺了!!");
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

20.12.4 動態代理呼叫流程

20.12.5 動態代理的缺點

只能針對介面的實現類做代理物件,普通類是不能做代理物件的。後面框架學習的時候會接觸到 CGLibCode Genneration Library )可以實現對類的代理