深入淺出java反射應用一一動態代理
Java高階之反射
反射應用之動態代理
目錄
問題的起源
- 適逢學生暑期,現在駕校裡有許多學生趁著假期開始學車,目前正在練習科目二,整體流程固定,如下:
/** * 駕校學生介面 */ interface DrivingStudent{ //準備科目二的考試 void prepare(); } /** * 正常駕校學生的考試流程 */ class CommonStudents implements DrivingStudent{ @Override public void prepare() { System.out.println("在駕校正常上班時間點,聽教練講要點、然後練習,熟練後去考試!"); } }
-
一切都在按部就班的進行著。這天,新加入了一個學員,她是駕校老闆的女兒,作為老闆的女兒,教練為了讓老闆開心好給自己加薪,自然要給她VVIP的練車待遇:在
prepare()
方法執行之前,先好好的跟她講一些注意事項,prepare()
方法執行之後,也就是駕校下班後,單獨指導她練車技巧以及傳授經驗。 -
現在,科二整體流程固定,可以比喻為類
CommonStudents
已經載入到記憶體,成為執行時的類,眾所周知,java在執行時的類是不允許被修改的,那麼教練該如何實現給老闆女兒開小灶的功能?
問題的擴充套件
- 回到
java
程式設計世界中,Spring
框架在當下非常熱門,AOP
、IOC
這些耳熟能詳的詞在java
- 聯想到開發應用程式過程中,我們或多或少都會遇到類似於這樣的需求:比如為方便排查問題,要在某些函式(例如上面的:
prepare()
函式)的呼叫前後加上相應的日誌記錄;為了保持資料的安全性,要給某些函式加上事務的支援等等。xml
和註解的盛行,使我們程式設計師可以在xml
和註解中宣告要在哪一類函式前後加上日誌以及日誌等,但是這些類已經是執行狀態,我們宣告了要在這些執行中的函式前後加功能,但是執行時的類java
是不允許被修改的,那麼該如何實現此需求呢?
問題的思路
分析
- 在上篇反射1中我們提到,
java
世界中有這麼一個類java.lang.Class
- 回到駕校的問題:教練希望能給老闆女兒開小灶,但是對外宣稱也必須是走的是駕校的正常流程,也就是呼叫的是
prepare()
方法;回到java
程式設計世界,比如有個新增資料的函式add()
,我們希望在add()
函式前後加上日誌以打印出入參出參。但是在呼叫方看來,他應該只是呼叫了add()
函式,他並不知道額外做了新增日誌其他的操作。 - 換句話說,我們通過
Class
獲取到了執行時的prepare()
、add()
方法,並在這些方法前後添加了自己的額外功能,而呼叫方還認為自己只是呼叫的是正常的方法。
方案整理
- 為了實現了新的功能,我們可以在類執行時動態的去建立一個新的類,這個新的類便是我們要實現某個函式的類(即被代理類也是目標物件)的代理類。
- 以教練為例,建立目標物件
CommonStudents
的代理類的需要我們考慮兩個問題:CommonStudents
被載入到記憶體中,成為執行時的類,如何通過它建立一個代理類?- 當
CommonStudents
類中的prepare()
方法被呼叫時,如何讓它去呼叫代理類中在prepare()
方法的基礎上添加了額外功能的方法?
問題的解決
解決流程
-
解決問題1:Proxy 是專門完成代理的操作類,通過該類為一個或多個介面動態地生成實現類。它是所有動態代理類的父類;建立一個代理物件的方法原始碼如下:
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) throws IllegalArgumentException{ ********** }
-
解決問題2:建立一個類實現介面InvocationHandler的類,在該類中實現
invoke()
方法,在該方法中新增上我們需要新增的邏輯。這樣在呼叫目標物件的方法時,呼叫的是代理類中的invoke()
方法。
具體實現
-
解決問題2的程式碼,也就是當呼叫'目標物件'
CommonStudents
的prepare()
方法時,如何使其重定向到一個新的方法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動態代理為什麼需要介面?
-
簡單理解下原始碼:
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}); ...... }
getProxyClass0(loader, intfs)
中邏輯:
private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) { ...... //重點在:ProxyClassFactory return proxyClassCache.get(loader, interfaces); }
ProxyClassFactory
的主要操作:
private static final class ProxyClassFactory implements BiFunction<ClassLoader, Class<?>[], Class<?>> { ...... { //驗證引數、介面等 ...... //生成byte型別的代理類,並將其返回 byte[] proxyClassFile = ProxyGenerator.generateProxyClass( proxyName, interfaces, accessFlags); ...... } }
-
重點理解一下生成的
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[]型別代理類發生錯誤"); }
-
寫入成功後,對生成的代理類進行反編譯後得到的類如下:
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