Java-基礎-JDK動態代理


1. 簡介

代理模式的定義:為其他對象提供一種代理以控制對這個對象的訪問。在某些情況下,一個對象不適合或者不能直接引用另一個對象,而代理對象可以在客戶端和目標對象之間起到中介的作用。

比如:我們在調用底層框架方法時候,需要在調用方法的前后打印日志,或者做一些邏輯判斷。此時我們無法去修改底層框架方法,那我們可以通過封裝一個代理類,在代理類中實現對方法的處理,然后所有的客戶端通過代理類去調用目標方法。

其中這里有幾個對象:

  1. 抽象角色:通過接口或者抽象類聲明真實角色實現的業務方法,盡可能的保證代理對象的內部結構和目標對象一致,這樣我們對代理對象的操作最終都可以轉移到目標對象上。
  2. 代理角色/代理對象:實現抽象角色,是真實角色的代理,實現對目標方法的增強。
  3. 真實角色/目標對象:實現抽象角色,定義真實角色所要實現的業務邏輯,供代理角色調用。
  4. 調用角色/客戶端:調用代理對象。

2. 靜態代理

2.1 業務場景

假設現在有這么個場景:王淑芬打算相親,但是自己嘴笨,於是找到媒婆,希望媒婆幫自己找個帥哥,於是找到了張鐵牛

角色分析:

  • 王淑芬:目標對象(被代理的人)。
  • 媒婆:代理對象(代理了王淑芬,實現對目標方法的增強)。
  • 張鐵牛:客戶端(訪問代理對象,即找媒婆)。
  • 抽象角色(相親):媒婆王淑芬的共同目標-相親成功。

2.2 代碼實現

  1. 相親接口

    /**
     * 相親抽象類
     *
     * @author ludangxin
     * @date 2021/9/25
     */
    public interface BlindDateService {
       /**
        * 聊天
        */
       void chat();
    }
    
  2. 目標對象

    /**
     * 王淑芬 - 目標對象
     *
     * @author ludangxin
     * @date 2021/9/25
     */
    @Slf4j
    public class WangShuFen implements BlindDateService {
       @Override
       public void chat() {
          log.info("美女:王淑芬~");
       }
    }
    
  3. 代理對象

    /**
     * 媒婆 - 代理對象
     *
     * @author ludangxin
     * @date 2021/9/25
     */
    @Slf4j
    public class WomanMatchmaker implements BlindDateService {
       private BlindDateService bs;
    
       public WomanMatchmaker() {
          this.bs = new WangShuFen();
       }
    
       @Override
       public void chat() {
          this.introduce();
          bs.chat();
          this.praise();
       }
    
       /**
        * 介紹
        */
       private void introduce() {
          log.info("媒婆:她的工作是web前端~");
       }
    
       /**
        * 誇人
        */
       private void praise() {
          log.info("媒婆:她就是有點害羞~");
          log.info("媒婆:你看她人長的漂亮,而且溫柔賢惠,上的廳堂下的廚房~");
          log.info("媒婆:而且寫bug超厲害~");
       }
    }
    
  4. 客戶端

    /**
     * 張鐵牛 - client
     *
     * @author ludangxin
     * @date 2021/9/25
     */
    public class ZhangTieNiu {
       public static void main(String[] args) {
          WomanMatchmaker wm  = new WomanMatchmaker();
          wm.chat();
       }
    }
    
  5. 執行方法輸出內容如下:

    22:44:51.184 [main] INFO proxy.staticp.WomanMatchmaker - 媒婆:她的工作是web前端~
    22:44:51.191 [main] INFO proxy.staticp.WangShuFen - 美女:你好,我叫王淑芬~
    22:44:51.191 [main] INFO proxy.staticp.WomanMatchmaker - 媒婆:她就是有點害羞~
    22:44:51.191 [main] INFO proxy.staticp.WomanMatchmaker - 媒婆:你看她人長的漂亮,而且溫柔賢惠,上的廳堂下的廚房~
    22:44:51.191 [main] INFO proxy.staticp.WomanMatchmaker - 媒婆:而且寫bug超厲害~
    

2.3 小節

好處:

  1. 耦合性降低。因為加入了代理類,調用者只用關心代理類即可,降低了調用者與目標類的耦合度。
  2. 指責清晰,目標對象只關心真實的業務邏輯。代理對象只負責對目標對象的增強。調用者只關心代理對象的執行結果。
  3. 代理對象實現了對目標方法的增強。也就是說:代理對象 = 增強代碼 + 目標對象

缺陷:

每一個目標類都需要寫對應的代理類。如果當前系統已經有成百上千個類,工作量大太。所以,能不能不用寫那么多代理類,就能實現對目標方法的增強呢?

3. 動態代理

我們常見的動態代理一般有兩種:JDK動態代理CGLib動態代理,本章只講JDK動態代理

在了解JDK動態代理之前,先了解兩個重要的類。

3.1 Proxy

從JDK的幫助文檔中可知:

Proxy提供了創建動態代理和實例的靜態方法。即:

Proxy::newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

參數:

  • loader - 定義代理類的類加載器

  • interfaces - 代理類要實現的接口列表

  • h - 指派方法調用的調用處理程序

前兩個參數沒啥好說的,主要還需要了解下InvocationHandler

3.2 InvocationHandler

從JDK的幫助文檔中可知:

InvocationHandler是一個接口,並且是調用處理邏輯實現的接口。在調用代理對象方法時,實際上是調用實現接口的invoke方法。

Object InvocationHandler::invoke(Object proxy, Method method, Object[] args)

參數:

  • proxy - 調用方法的代理實例。

  • method - 對應於在代理實例上調用的接口方法的 Method 實例。 Method 對象的聲明類將是在其中聲明方法的接口,該接口可以是代理類賴以繼承方法的代理接口的超接口。

  • args - 包含傳入代理實例上方法調用的參數值的對象數組,如果接口方法不使用參數,則為 null。基本類型的參數被包裝在適當基本包裝器類(如 java.lang.Integerjava.lang.Boolean)的實例中。

返回值:

從代理實例的方法調用返回的值。如果接口方法的聲明返回類型是基本類型,則此方法返回的值一定是相應基本包裝對象類的實例;否則,它一定是可分配到聲明返回類型的類型。如果此方法返回的值為 null 並且接口方法的返回類型是基本類型,則代理實例上的方法調用將拋出 NullPointerException。否則,如果此方法返回的值與上述接口方法的聲明返回類型不兼容,則代理實例上的方法調用將拋出 ClassCastException

3.3 代碼實現

  1. 創建動態代理類

    import lombok.extern.slf4j.Slf4j;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.util.Arrays;
    
    /**
     * 媒婆 - 代理對象
     *
     * @author ludangxin
     * @date 2021/9/25
     */
    @Slf4j
    public class ProxyTest {
    
       public static Object getProxy(final Object target) {
          return Proxy.newProxyInstance(
             //類加載器
             target.getClass().getClassLoader(),
             //讓代理對象和目標對象實現相同接口
             target.getClass().getInterfaces(),
             //代理對象的方法最終都會被JVM導向它的invoke方法
             new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                   log.info("proxy ===》{}", proxy.getClass().toGenericString());
                   log.info("method ===》{}", method.toGenericString());
                   log.info("args ===》{}", Arrays.toString(args));
                   introduce();
                   // 調用目標方法
                   Object result = method.invoke(target, args);
                   praise();
                   return result;
                }
             });
       }
    
       /**
        * 介紹
        */
       private static void introduce() {
          log.info("媒婆:她的工作是web前端~");
       }
    
       /**
        * 誇人
        */
       private static void praise() {
          log.info("媒婆:她就是有點害羞~");
          log.info("媒婆:你看她人長的漂亮,而且溫柔賢惠,上的廳堂下的廚房~");
          log.info("媒婆:而且寫bug超厲害~");
       }
    }
    
  2. 客戶端

    /**
     * 張鐵牛 - client
     *
     * @author ludangxin
     * @date 2021/9/25
     */
    public class ZhangTieNiu {
       public static void main(String[] args) {
          BlindDateService proxy = (BlindDateService)ProxyTest.getProxy(new WangShuFen());
          proxy.chat();
       }
    }
    
  3. 執行方法輸出內容如下:

    21:29:22.222 [main] INFO proxy.dynamic.ProxyTest - proxy ===》public final class com.sun.proxy.$Proxy0
    21:29:22.229 [main] INFO proxy.dynamic.ProxyTest - method ===》public abstract void proxy.dynamic.BlindDateService.chat()
    21:29:22.229 [main] INFO proxy.dynamic.ProxyTest - args ===》null
    21:29:22.229 [main] INFO proxy.dynamic.ProxyTest - 媒婆:她的工作是web前端~
    21:29:22.229 [main] INFO proxy.dynamic.WangShuFen - 美女:你好,我叫王淑芬~
    21:29:22.230 [main] INFO proxy.dynamic.ProxyTest - 媒婆:她就是有點害羞~
    21:29:22.230 [main] INFO proxy.dynamic.ProxyTest - 媒婆:你看她人長的漂亮,而且溫柔賢惠,上的廳堂下的廚房~
    21:29:22.230 [main] INFO proxy.dynamic.ProxyTest - 媒婆:而且寫bug超厲害~
    

3.4 小節

其實代碼很簡單,只需要通過jdk提供的代理類創建一個代理類,再通過代理類去調用目標方法,並實現對方法的增強即可。

從打印的日志中可以看出JDK生成真實的代理對象為com.sun.proxy.$Proxy0,那我們能不能查看下生成的代理對象的源碼呢?答案肯定是可以的。我們可以借助JDK默認提供的ProxyGenerator::generateProxyClass()來輸出動態生成的代理對象。

改造下動態代理類如下:

添加輸出動態代理類的方法。

package proxy.dynamic;

import lombok.extern.slf4j.Slf4j;
import sun.misc.ProxyGenerator;
import java.io.File;
import java.io.FileOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

/**
 * 媒婆 - 代理對象
 *
 * @author ludangxin
 * @date 2021/9/25
 */
@Slf4j
public class ProxyTest {
   public static Object getProxy(final Object target) {
      return Proxy.newProxyInstance(
            //類加載器
            target.getClass().getClassLoader(),
            //讓代理對象和目標對象實現相同接口
            target.getClass().getInterfaces(),
            //代理對象的方法最終都會被JVM導向它的invoke方法
            new InvocationHandler() {
               @Override
               public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                  generateProxyClass(proxy);
                  log.info("proxy ===》{}", proxy.getClass().toGenericString());
                  log.info("method ===》{}", method.toGenericString());
                  log.info("args ===》{}", Arrays.toString(args));
                  introduce();
                  Object result = method.invoke(target, args);
                  praise();
                  return result;
               }
            });
   }

   /**
    * 輸出動態生成的代理類
    * @param proxy 代理實例
    */
   private static void generateProxyClass(Object proxy) {
      byte[] bytes = ProxyGenerator.generateProxyClass(proxy.getClass().getName(), proxy.getClass().getInterfaces());
      File file = new File("/Users/ludangxin/workspace/idea/test/target/classes/proxy/dynamic/proxy.class");
      try {
         FileOutputStream fileOutputStream = new FileOutputStream(file);
         fileOutputStream.write(bytes);
      } catch(Exception e) {
         e.printStackTrace();
      }
   }
   ...
}

再次執行調用方法,輸出類如下:

類信息如下:

看完代理類的方法是不是恍然大悟,我們再整理下調用過程:

動態代理:動態的生成代理對象。

4. 總結

其實無論是靜態代理還是動態代理本質都是最終生成代理對象,區別在於靜態代理對象需要人手動生成,而動態代理對象是運行時,JDK通過反射動態生成的代理類最終構造的對象,JDK生成的類在加載到內存之后就刪除了,所以看不到類文件。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM