1. 程式人生 > 實用技巧 >深入淺出java反射應用一一動態代理

深入淺出java反射應用一一動態代理

Java高階之反射

反射應用之動態代理

目錄

問題的起源

  1. 適逢學生暑期,現在駕校裡有許多學生趁著假期開始學車,目前正在練習科目二,整體流程固定,如下:
/**
 * 駕校學生介面
 */
interface DrivingStudent{
    //準備科目二的考試
    void prepare();
}
/**
 * 正常駕校學生的考試流程
 */
class CommonStudents implements DrivingStudent{
    @Override
    public void prepare() {
        System.out.println("在駕校正常上班時間點,聽教練講要點、然後練習,熟練後去考試!");
    }
}
  1. 一切都在按部就班的進行著。這天,新加入了一個學員,她是駕校老闆的女兒,作為老闆的女兒,教練為了讓老闆開心好給自己加薪,自然要給她VVIP的練車待遇:在prepare()方法執行之前,先好好的跟她講一些注意事項,prepare()方法執行之後,也就是駕校下班後,單獨指導她練車技巧以及傳授經驗。

  2. 現在,科二整體流程固定,可以比喻為類CommonStudents已經載入到記憶體,成為執行時的類,眾所周知,java在執行時的類是不允許被修改的,那麼教練該如何實現給老闆女兒開小灶的功能?

問題的擴充套件

  • 回到java程式設計世界中,Spring框架在當下非常熱門,AOPIOC這些耳熟能詳的詞在java
    程式設計世界中經常出現,那麼AOP解決了碼農的什麼樣需求?
  • 聯想到開發應用程式過程中,我們或多或少都會遇到類似於這樣的需求:比如為方便排查問題,要在某些函式(例如上面的:prepare()函式)的呼叫前後加上相應的日誌記錄;為了保持資料的安全性,要給某些函式加上事務的支援等等。xml和註解的盛行,使我們程式設計師可以在xml和註解中宣告要在哪一類函式前後加上日誌以及日誌等,但是這些類已經是執行狀態,我們宣告了要在這些執行中的函式前後加功能,但是執行時的類java是不允許被修改的,那麼該如何實現此需求呢?

問題的思路

分析

  • 在上篇反射1中我們提到,java世界中有這麼一個類java.lang.Class
    ,它是描述類的類,它對應的便是那一個個執行時的類,我們可以通過它去拿到執行時的類的所有型別資訊,包括方法、屬性等。也可以通過它去建立一個執行時的類的物件。
  • 回到駕校的問題:教練希望能給老闆女兒開小灶,但是對外宣稱也必須是走的是駕校的正常流程,也就是呼叫的是prepare()方法;回到java程式設計世界,比如有個新增資料的函式add(),我們希望在add()函式前後加上日誌以打印出入參出參。但是在呼叫方看來,他應該只是呼叫了add()函式,他並不知道額外做了新增日誌其他的操作。
  • 換句話說,我們通過Class獲取到了執行時的prepare()add()方法,並在這些方法前後添加了自己的額外功能,而呼叫方還認為自己只是呼叫的是正常的方法。

方案整理

  • 為了實現了新的功能,我們可以在類執行時動態的去建立一個新的類,這個新的類便是我們要實現某個函式的類(即被代理類也是目標物件)的代理類。
  • 以教練為例,建立目標物件CommonStudents的代理類的需要我們考慮兩個問題:
    1. CommonStudents被載入到記憶體中,成為執行時的類,如何通過它建立一個代理類?
    2. CommonStudents類中的prepare()方法被呼叫時,如何讓它去呼叫代理類中在prepare()方法的基礎上添加了額外功能的方法?

問題的解決

解決流程

  • 解決問題1:Proxy 是專門完成代理的操作類,通過該類為一個或多個介面動態地生成實現類。它是所有動態代理類的父類;建立一個代理物件的方法原始碼如下:

    public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) throws IllegalArgumentException{
          **********
    }
    
  • 解決問題2:建立一個類實現介面InvocationHandler的類,在該類中實現invoke()方法,在該方法中新增上我們需要新增的邏輯。這樣在呼叫目標物件的方法時,呼叫的是代理類中的invoke()方法。

具體實現

  • 解決問題2的程式碼,也就是當呼叫'目標物件'CommonStudentsprepare()方法時,如何使其重定向到一個新的方法invoke(),也就是有額外功能的方法。

    class MyInvocationHandler implements InvocationHandler{
    		//目標物件,可以通過反射來呼叫目標物件的方法
        private Object target;
        public MyInvocationHandler(Object target){
            this.target = target;
        }
      //重寫invoke方法,新增上自己的功能
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("開始練車之前,好好跟你講一些注意事項,以及快速上手的技巧!");
            //反射機制呼叫目標物件的方法
            Object result = method.invoke(target, args);
            System.out.println("下班後,單獨對你做一些練車的指導以及加練!");
            return result;
        }
    }
    
  • 主方法,內含解決問題1的程式碼。

    public static void main(String[] args) {
            System.out.println("========正常學生流程開始===========");
            DrivingStudent student = new CommonStudents();
            //正常學生的流程
            student.prepare();
            System.out.println("========正常學生流程結束===========");
            System.out.println("*********************************");
            System.out.println("=======老闆女兒練車流程開始==========");
            //老闆女兒的練車流程
      			//當CommonStudents方法被呼叫時,走invoke()方法,因此要將CommonStudents當作引數傳入。
            InvocationHandler handler = new MyInvocationHandler(student);
            //java提供了動態代理來解決在執行時動態建立一個代理類的方案
            DrivingStudent bossStudent = (DrivingStudent)Proxy.newProxyInstance(student.getClass().getClassLoader(),
                    student.getClass().getInterfaces(),
                    handler);
            bossStudent.prepare();
            System.out.println("=======老闆女兒練車流程結束==========");
    
        }
    
  • 執行結果

問題解決的思考

  • jdk動態代理為什麼需要介面?

  • 簡單理解下原始碼:

    1. Proxy.newProxyInstance中幾行重要程式碼:
    //
    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h) throws IllegalArgumentException {
            Objects.requireNonNull(h);
    				......
           //這一步是查詢並生成代理類的Class
            Class<?> cl = getProxyClass0(loader, intfs);
      			......
              //生成的代理類中存在引數為InvocationHandler的建構函式
                final Constructor<?> cons = cl.getConstructor(constructorParams);
      			......
                //將實現了InvocationHandler的類當作引數傳入到newProxyInstance方法後,這一行是利用這個引數建立一個代理類
                return cons.newInstance(new Object[]{h});
            ......
        }
    
    1. getProxyClass0(loader, intfs)中邏輯:
    private static Class<?> getProxyClass0(ClassLoader loader,
                                               Class<?>... interfaces) {
            ......
            //重點在:ProxyClassFactory
            return proxyClassCache.get(loader, interfaces);
        }
    
    1. ProxyClassFactory的主要操作:
    private static final class ProxyClassFactory
            implements BiFunction<ClassLoader, Class<?>[], Class<?>>
        {
      		......
          {
          	//驗證引數、介面等  		
      			......
            		//生成byte型別的代理類,並將其返回
                byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                    proxyName, interfaces, accessFlags);
             ......
            }
        }
    
    1. 重點理解一下生成的byte[]型別的代理類。將byte[]型別的代理類寫出到檔案上,並且進行反編譯,程式碼如下:

      byte[] classFile = ProxyGenerator.generateProxyClass(
                      "$Proxy0", new Class[]{DrivingStudent.class});
      //自定義的目錄
      String path = "*****"+ "$Proxy0" + ".class";
      try (FileOutputStream fos = new FileOutputStream(path)) {
           fos.write(classFile);
           fos.flush();
           System.out.println("byte[]型別代理類class檔案寫入成功");
      } catch (Exception e) {
           System.out.println("byte[]型別代理類發生錯誤");
      }
      
    2. 寫入成功後,對生成的代理類進行反編譯後得到的類如下:

      public final class $Proxy0 extends Proxy implements DrivingStudent {
        private static Method m1;
        private static Method m2;
        private static Method m3;
        private static Method m0;
        //看到這裡我們能看到,自動生成的代理類物件有一個入參是InvocationHandler的建構函式,這樣在建立代理類時,通過該構造方法便將被代理類(實現了DrivingStudent)和InvocationHandler聯絡了起來。
        public $Proxy0(InvocationHandler paramInvocationHandler) {
          super(paramInvocationHandler);
        }
        //省略的是equals和toString方法
        ......
        //當呼叫介面中的prepare()方法時,實際上走的是invoke()方法。invoke()方法中有我們自己加的邏輯
        public final void prepare() {
          try {
            this.h.invoke(this, m3, null);
            return;
          } catch (Error|RuntimeException error) {
            throw null;
          } catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
          } 
        }
         //省略的是hashCode方法
        ......
        static {
          try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m3 = Class.forName("com.practice.reflect.DrivingStudent").getMethod("prepare", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            return;
          } catch (NoSuchMethodException noSuchMethodException) {
            throw new NoSuchMethodError(noSuchMethodException.getMessage());
          } catch (ClassNotFoundException classNotFoundException) {
            throw new NoClassDefFoundError(classNotFoundException.getMessage());
          } 
        }
      }
      
  • 回到最初的問題:jdk生成的動態代理類是繼承於Proxy類的,而java類是不允許多繼承的,為了使代理類和目標物件建立聯絡,就必須實現一個介面。

結論

  • java動態代理是根據被代理物件動態的建立了一個新類,當呼叫被代理物件的方法時,會跳轉到呼叫InvocationHandler中的invoke()方法,在建立代理類方法newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)的入參我們看到,它是根據位元組碼檔案自動生成的代理類,並且傳入了 InvocationHandler引數。
  • 通過反編譯生成的動態代理類得到的程式碼,我們看到代理類繼承了Proxy類以及實現了目標物件實現的介面,並且該代理類中的建構函式用到了上面傳入的 InvocationHandler引數,這樣便將目標物件和InvocationHandler聯絡了起來,InvocationHandler中的invoke()方法中定義了我們需要新新增的功能,當呼叫目標物件的方法時,使其跳轉到呼叫invoke()方法,這樣便實現了在執行時建立新的類以滿足我們新增功能的需求。

原創不易,歡迎轉載,轉載時請註明出處,謝謝!
作者:瀟~蕭下
原文連結:https://www.cnblogs.com/manongxiao/p/13449480.html