mybatis 01: 靜態代理 + jdk動態代理

2022-08-07 18:01:04

背景

  • 有時目標物件不可直接存取,只能通過代理物件存取

  • 圖示:

  • 範例1:
    • 房東 ===> 目標物件
    • 房屋中介 ===> 代理物件
    • 你,我 ===> 使用者端物件
  • 範例2:
    • 運營商(電信,移動,聯通) ===> 目標物件
    • 第三方公司 ===> 代理物件
    • 開發的應用程式需要傳送簡訊的功能(或者需要支付功能) ===> 使用者端物件

代理模式的作用

  • 控制客戶對目標物件的存取
  • 增強存取功能

代理模式的分類

  • 靜態代理
  • 動態代理
    • JDK動態代理
    • CGLib動態代理

靜態代理

特點

  • 目標物件和代理物件實現同一個業務介面
  • 目標物件必須實現介面
  • 代理物件在程式執行前就已經存在

靜態代理範例與原理分析

業務背景

分析

  • 定義業務介面:面向介面程式設計,定義業務
  • 目標物件實現介面:業務的核心功能到底怎麼實現
  • 代理物件(擴充套件業務 + 核心業務)
    • 實現了目標物件所實現的介面,說明代理物件有資歷進行代理
    • 對核心業務進行擴充套件
    • 呼叫目標物件實現核心業務(只能目標物件自己完成)
  • 客戶:無法直接存取目標物件,要存取代理物件

程式碼實現

  • 面向介面程式設計

    • 成員變數是介面型別
    • 傳入目標物件,方法的引數設計為介面
    • 呼叫時,介面指向實現類
  • 靜態代理物件程式碼

    package com.example.service.impl;
    
    import com.example.service.Service;
    
    public class Agent implements Service {
    
        //定義介面物件
        public Service target;
    
        public Agent(){}
    
        //傳入介面物件
        public Agent(Service target){
            this.target = target;
        }
    
        @Override
        public void sing() {
            System.out.println("協商演出時間......");
            System.out.println("協商演出地點......");
    
            //目標物件完成核心業務,介面指向實現類,呼叫實現類的方法
            target.sing();
    
            System.out.println("協商演出費用......");
        }
    }
    

靜態代理優缺點

  • 優點:能夠靈活的進行目標物件的切換
    • 適用於業務固定,目標物件可靈活切換的場景
  • 缺點:無法進行功能的靈活處理,當業務發生改變時,所有涉及到的實現類程式碼和代理物件程式碼都要改變

動態代理

JDK動態代理

特點

  • 目標物件必須實現業務介面
  • JDK代理物件不需要實現業務介面
  • JDK代理物件在程式執行前不存在,程式執行時動態的在記憶體中構建(根據受代理的物件動態建立)
  • JDK動態代理可以靈活的進行業務功能的切換

JDK動態代理用到的類和介面

  • 使用現有的工具類完成JDK動態代理
  • 先了解兩個單詞的意思
    • InvocationHandler:呼叫處理程式
    • invoke:呼叫

Method類

  • 反射時用的類,用來進行目標物件的目標方法的反射呼叫
  • method物件,接住我們正在呼叫的方法 sing(),show()
    • method == sing(),show(),即:待呼叫的方法
    • method.invoke() ==> 相當於手工呼叫目標方法 sing(),show();

InvocationHandler介面

  • 用來實現代理和業務功能,我們在呼叫時使用匿名內部實現
    • 匿名內部實現:new介面的同時,重寫介面中的方法(相當於定義了該介面的一個實現類)

Proxy類

  • 位於:java.lang.reflect.Proxy包下

  • 有一個核心方法:Proxy.newProxyInstance(....),專門獲取動態代理物件,有三個引數

    • 引數1:ClassLoader loader

      • 目標物件的類載入器
      • 目的:獲取類方法等資訊,畢竟底層還是要呼叫受代理物件所實現的方法
      • 傳入:targetObj.getClass().getClassLoader();
    • 引數2:Class<?>[] interfaces

      • 目標物件實現的所有介面,類的介面可以有多個
      • 目的:獲取目標物件實現的所有介面以及介面的相關資訊,畢竟底層要知道目標物件都可以完成哪些業務操作
      • 傳入:targetObj.getClass().getInterfaces();
    • 上面兩個引數為代理物件動態的建立和呼叫目標物件的方法提供了資料支援,第3個引數相當於呼叫程式

    • 引數3:InvocationHandler

      • 實現代理功能的介面,這裡代理功能包括:擴充套件的功能 + 核心業務功能,傳入的匿名內部實現如下
      new InvocationHandler() {
          @Override
          public Object invoke(
                  Object obj,
                  //用來反射呼叫方法
                  Method method,
                  //待呼叫方法需要的引數
                  Object[] args)
                  throws Throwable {
      
              //擴充套件業務
              System.out.println("協商演出時間......");
              System.out.println("協商演出地點......");
      
              //核心業務,具體呼叫什麼方法根據外層業務來反射呼叫對應方法
              Object res = method.invoke(target, args);
      
              //擴充套件業務
              System.out.println("協商演出費用......");
      
              //目標物件執行的目標方法的返回值
              return res;
          }
      }
      

JDK動態代理範例

  • 代理工廠程式碼

    package com.example.proxy;
    
    import com.example.service.Service;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    public class ProxyFactory {
        //目標物件
        Service target;
    
        public ProxyFactory(){}
    
        public ProxyFactory(Service target){
            this.target = target;
        }
    
        //返回代理物件
        public Object getAgent(){
            return Proxy.newProxyInstance(
                    //需要知道受代理物件的類資訊
                    target.getClass().getClassLoader(),
                    //需要知道受代理物件實現的所有介面資訊
                    target.getClass().getInterfaces(),
                    //反射呼叫目標物件的目標方法
                    new InvocationHandler() {
                        @Override
                        public Object invoke(
                                Object obj,
                                //用來反射呼叫方法
                                Method method,
                                //待呼叫方法需要的引數
                                Object[] args)
                                throws Throwable {
    
                            //擴充套件業務
                            System.out.println("協商演出時間......");
                            System.out.println("協商演出地點......");
    
                            //核心業務,具體呼叫什麼方法根據外層業務來反射呼叫對應方法
                            Object res = method.invoke(target, args);
    
                            //擴充套件業務
                            System.out.println("協商演出費用......");
    
                            //目標物件執行的目標方法的返回值
                            return res;
                        }
                    }
            );
        }
    }
    
  • 測試程式碼範例

    package com.example.proxy;
    
    import com.example.service.Service;
    import com.example.service.impl.SuperStarZhou;
    import org.junit.Test;
    
    public class TestProxyFactory {
        @Test
        public void testGetProxy(){
            //確定客戶需求
            ProxyFactory factory = new ProxyFactory(new SuperStarZhou());
            //根據需求動態返回對應型別的代理物件
            Service agent = (Service) factory.getAgent();
            //依託對應型別的動態代理物件完成業務:擴充套件業務(動態代理物件完成) + 核心業務(目標物件完成)
            agent.sing();
        }
    
        @Test
        public void testGetProxy2(){
            ProxyFactory factory = new ProxyFactory(new SuperStarZhou());
            Service agent = (Service) factory.getAgent();
            String res = (String) agent.show(60);
            System.out.println(res);
        }
    }
    

注意

  • 可被代理的方法應該是受代理物件實現的所有介面中的方法與其所有實體方法的交集

    • 本類中獨有的方法不被代理
  • 型別的轉變

     @Test
     public void testGetProxy2() {
         ProxyFactory factory = new ProxyFactory(new SuperStarZhou());
         Service agent = (Service) factory.getAgent();
         Service liu = new SuperStarLiu();
         System.out.println("型別1: " + liu.getClass());
         System.out.println("型別2: " + agent.getClass());
     }
     /*
      輸出結果:
      型別1: class com.example.service.impl.SuperStarLiu
      型別2: class com.sun.proxy.$Proxy7
     */