# / 代理模式 /java
近期在研究Hook技術,須要用到動態代理,說到動態代理就會聊到它的兄弟靜態代理,那它們究竟是怎麼一回事呢?實現方式有哪些呢?一塊兒來看下node
代理在咱們生活中隨處可見,好比咱們生活中的各類中介公司,以買一輛二手車爲例:若是我買車,我能夠本身去網上找車源,而後作質量檢測,車輛過戶等一系列的流程,可是這太費時間和精力了,我就是想花錢買個車而已,最後我竟然吭哧吭哧的幹了這麼多活;因而我能夠經過中介買車,我只用給錢,選車就好了,其它活都交給中介幹,這就是代理的一種實現;還有網絡代理(無論是正向仍是反向代理),房屋租賃公司等都是代理的具體實現程序員
在Java編程裏就有一種設計模式,即代理模式,提供了一種對目標對象的訪問方式,即經過代理對象訪問目標對象,代理對象是指具備與被代理對象相同的接口的類,客戶端必須經過代理對象與被代理的目標類進行交互面試
代理類主要負責爲目標類預處理消息、過濾消息、把消息轉發給目標類,以及過後對返回結果的處理等。編程
代理類自己並不真正實現服務,而是同過調用目標類的相關方法,來提供特定的服務。真正的業務功能仍是由目標類來實現,可是能夠在業務功能執行的先後加入一些公共的服務。例如咱們想給項目加入緩存、日誌這些功能,咱們就可使用代理類來完成,而不必打開已經封裝好的目標類。設計模式
代理實現方式:數組
若是按照代理建立的時期來進行分類的話, 能夠分爲靜態代理、動態代理緩存
如圖:網絡
開源框架應用:架構
Spring框架是時下很流行的Java開源框架,Spring之全部如此流行,跟它自身的特性是分不開的,一個是IOC,一個是AOP
AOP爲Aspect Oriented Programming的縮寫,意爲:面向切面編程,經過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,是軟件開發中的一個熱點,也是Spring框架中的一個重要內容,是函數式編程的一種衍生範型。
利用AOP能夠對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度下降,提升程序的可重用性,同時提升了開發的效率。
/ 靜態代理 /
靜態代理是代理模式實現方式之一,比較簡單,主要分爲三個角色:客戶端,代理類,目標類;而代理類須要與目標類實現同一個接口,並在內部維護目標類的引用,進而執行目標類的接口方法,並實如今不改變目標類的狀況下前攔截,後攔截等所需的業務功能。
假設如今項目經理給你提了一個需求:在項目全部類的有關用戶操做的方法先後打印日誌;這種狀況下就能夠經過靜態代理實現
作法就是:爲每一個相關類都編寫一個代理類(業務重合的能夠共用一個代理類),而且讓它們實現相同的接口
public interface ILogin { void userLogin(); }
定義目標類:
public class UserLogin implements ILogin { @Override public void userLogin() { System.out.print("用戶登陸"); } }
定義代理類:
public class UserLoginProxy implements ILogin { private UserLogin mLogin; public UserLoginProxy() { mLogin = new UserLogin(); } @Override public void userLogin() { System.out.print("登陸前。。。"); mLogin.userLogin(); System.out.print("登陸後。。。"); } }
客戶端:
public class UserLoginProxy implements ILogin { public class Test { public static void main(String[] args){ ILogin loginProxy = new UserLoginProxy(); loginProxy.userLogin(); } }
這樣咱們就在儘可能不修改現有類的基礎上實現了這個需求,同時客戶端只用跟代理類打交道,徹底不用瞭解代理類與目標類的實現細節,也不須要知道目標類是誰;固然你若是想指定目標類,將在外部建立目標類,傳給代理類
這裏只是在同步狀況下的一種實現,若是是異步操做,就須要進行一些改動
靜態代理總結:
從靜態代理的實現過程能夠知道工做量太大,若是是在項目早期同步作還好,要是在接手老項目或者項目晚期再作,你可能要爲成百上千個類建立對應的代理對象,那真的挺讓人崩潰的;因此咱們就須要想有沒有別的方法:如何少寫或者不寫代理類卻能完成這些功能
作Java的都知道一個.java文件會先被編譯成.class文件,而後被類加載器加載到JVM的方法區,存在形式是一個Class對象(所謂的Class對象就是Class文件在內存中的實例);
咱們構造出的任何對象實例是保存在Heap中,而實例是由其Class對象建立的(能夠經過任意實例的getClass方法獲取對應的Class對象),好比平時使用new關鍵字加構造方法Person p = new Person()就是將這個Class對象在Heap中建立一個實例;
能夠看出要建立一個實例,最關鍵的是獲得對應的Class對象,而獲得Class對象追根溯源須要一個Java文件;那麼咱們要作的就是不寫Java文件,而是直接獲得代理Class對象,而後根據它經過反射建立代理實例
Class對象裏面包含了一個類的全部信息,好比構造器,方法,字段等;那咱們怎麼在不寫代理類的前提下獲取到這些信息呢?
從靜態代理的實現知道目標類和代理類實現了同一個接口,這是爲了儘量保證代理對象的內部結構和目標對象一致,這樣代理對象只須要專一於代碼加強部分的編寫,因此接口擁有代理對象和目標對象共同的類信息,那麼咱們就能夠從接口獲取原本應該由代理類提供的信息,可是要命的是接口是不能實例化的啊
這就要講到動態代理了:在動態代理中,不須要咱們再手動建立代理類,只須要編寫一個動態處理器及指定要代理的目標對象實現的接口,真正的代理對象由JDK在運行時爲咱們建立;JDK提供了java.lang.reflect.InvocationHandler和java.lang.reflect.Proxy來實現動態代理
Proxy.getProxyClass(ClassLoader, interfaces)方法只須要接收一個類加載器和一組接口就能夠返回一個代理Class對象,而後就能夠經過反射建立代理實例;其原理就是從傳入的接口Class中,拷貝類結構信息到一個新的Class對象中,並繼承Proxy類,擁有構造方法;站在咱們的角度就是經過接口Class對象建立代理類Class對象
這裏經過一個很騷的比喻來講明下:一個東廠太監(接口Class對象)有一家子財產,可是爲了侍奉皇帝這一偉大事業,毅然決然的割了DD(沒有構造方法),雖然實現了本身的理想,可是不能繁育下一代(不能構造器建立對象),也就沒有後人繼承本身的家業;可是好在華佗在世,江湖上有一個高人(Proxy),發明了一個克隆大法(getProxyClass),不只克隆出了幾乎和太監同樣的下一代(新的Class),還擁有本身的小DD(構造方法),這樣這個下一代就能繼承太監的家產(類結構信息,實際上是實現了該接口),同時還能娶妻生子,傳給下一代(建立實例)
用一副圖展現動態代理和靜態代理實現思路:
那這樣就很簡單了
public static Object loadProxy(Object target) throws Exception { //經過接口Class對象建立代理Class對象 Class<?> proxyClass = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces()); //拿到代理Class對象的有參構造方法 Constructor<?> constructors = proxyClass.getConstructor(InvocationHandler.class); //反射建立代理實例 Object proxy = constructors.newInstance(new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("執行前日誌..."+"\n"); //執行目標類的方法 Object result = method.invoke(target, args); System.out.println("執行後日志..."+"\n"); return result; } }); return proxy; } public static void main(String[] args) throws Exception { ILogin proxy = (ILogin) loadProxy(new UserLogin()); proxy.userLogin(); }
看看打印結果
這樣不管系統有多少目標類,經過傳進來的目標類均可以獲取到對應的代理對象,就達到咱們在執行目標類先後加日誌的效果了,同時還不須要編寫代理類
Proxy類還有個更簡單的方法newProxyInstance,直接返回代理對象,以下
public static Object loadProxy(Object object) { return Proxy.newProxyInstance( object.getClass().getClassLoader(), //和目標對象的類加載器保持一致 object.getClass().getInterfaces(), //目標對象實現的接口,由於須要根據接口動態生成代理對象 new InvocationHandler() { //事件處理器,即對目標對象方法的執行 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("執行前日誌..."); Object result = method.invoke(object, args); System.out.println("執行後日志..."); return result; } }); }
JDK動態代理總結:
Java的單繼承機制決定了沒法支持class的動態代理,也就意味着你拿到動態生成的代理對象,只能調用其實現的接口裏的方法,沒法像靜態代理中的代理類能夠在內部擴展更多的功能
動態生成代理對象原理
在動態代理的過程當中,咱們不能清晰的看到代理類的實際樣子,並且被代理對象和代理對象是經過InvocationHandler來完成的代理過程,其中代理對象是如何生成的,具體什麼樣,爲何代理對象執行的方法都會走到InvocationHandler中的invoke方法,要想了解這些就須要對Java是如何動態生成代理對象源碼進行分析,繼續往下看
大家有沒有好奇getProxyClass這個方法是怎麼經過【目標類實現的接口】生成代理Class對象的呢?
若是你在第一個方法輸入以下代碼
System.out.println("執行日誌..."+(proxy instanceof Proxy)+"\n"); System.out.println("執行日誌..."+(proxyClass.getName())+"\n"); System.out.println("執行日誌..."+(proxyClass.getSuperclass().getName())+"\n"); System.out.println("執行日誌..."+(proxyClass.getSuperclass().getSimpleName())+"\n"); System.out.println("執行日誌..."+(proxyClass.getInterfaces()[0].getSimpleName())+"\n");
獲得的結果是
從這四點是否是能知道點啥了:動態生成的代理對象的父類是Proxy,實現了ILogin接口;這也就是爲何能將代理對象強轉成ILogin,從而調用其接口方法;
也說明了爲何只能支持動態生成接口代理,不能動態生成class代理,由於最終生成的代理對象確定會繼承Proxy類,若是咱們提供的類已經繼承了其它類,那就不能繼承Proxy類了,動態代理也就無從談起了
接下來再從源碼分析下其原理,鑑於篇幅緣由,這裏只分析重點代碼
private static final WeakCache<ClassLoader, Class<?>[], Class<?>> proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory()); /** * 若是存在實現給定接口的給定加載器定義的代理類,則只返回緩存副本; 不然,它將經過ProxyClassFactory建立代理類 */ private static Class<?> getProxyClass0(ClassLoader loader,Class<?>... interfaces) { if (interfaces.length > 65535) { throw new IllegalArgumentException("interface limit exceeded"); } return proxyClassCache.get(loader, interfaces); } private static final class ProxyClassFactory implements BiFunction<ClassLoader, Class<?>[], Class<?>> { // 全部代理類名稱的前綴 private static final String proxyClassNamePrefix = "$Proxy"; // 用於生成惟一代理類名稱的下一個數字 private static final AtomicLong nextUniqueNumber = new AtomicLong(); @Override public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) { Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length); for (Class<?> intf : interfaces) {//經過傳入的接口clone後的接口 ...... String proxyPkg = null; // 定義代理class的包名 int accessFlags = Modifier.PUBLIC | Modifier.FINAL; for (Class<?> intf : interfaces) { int flags = intf.getModifiers();//獲取接口修飾符 if (!Modifier.isPublic(flags)) {//咱們定義的接口修飾符都是public,因此這裏不會進去 accessFlags = Modifier.FINAL; String name = intf.getName(); int n = name.lastIndexOf('.'); String pkg = ((n == -1) ? "" : name.substring(0, n + 1)); if (proxyPkg == null) { proxyPkg = pkg; } else if (!pkg.equals(proxyPkg)) { throw new IllegalArgumentException( "non-public interfaces from different packages"); } } } if (proxyPkg == null) { // 若是沒有非public代理接口,請使用com.sun.proxy包 proxyPkg = ReflectUtil.PROXY_PACKAGE + "."; } /* * 生成代理class名稱 * 好比com.sun.proxy.$Proxy0 */ long num = nextUniqueNumber.getAndIncrement(); String proxyName = proxyPkg + proxyClassNamePrefix + num; /* * 經過接口和class名稱生成代理class數據 * 若是繼續看generateProxyClass,會發現裏面是生成class數據,包括寫入Object的三個初始方法、寫入實現的接口、寫入繼承Proxy類等到ByteArrayOutputStream中,而後轉換成byte數組返回,至因而否會將Byte數據寫到class文件保存在本地,視狀況而定(默認不保存到硬盤) */ byte[] proxyClassFile = ProxyGenerator.generateProxyClass( proxyName, interfaces, accessFlags); try { //將動態生成的class數據加載到內存中 生成一個代理class對象 return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); } catch (ClassFormatError e) { throw new IllegalArgumentException(e.toString()); } } }
ProxyGenerator.generateProxyClass()方法屬於sun.misc包下,Oracle並無提供源代碼,可是咱們可使用JD-GUI這樣的反編譯軟件打開jrelibrt.jar來一探究竟,如下是其核心代碼的分析:
public static byte[] generateProxyClass(final String var0, Class<?>[] var1, int var2) { ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2); final byte[] var4 = var3.generateClassFile(); //是否將生成的class數據保存到硬盤,默認不保存 if (saveGeneratedFiles) { ...... } return var4; } private byte[] generateClassFile() { //添加hashCode方法 this.addProxyMethod(hashCodeMethod, Object.class); //添加equals方法 this.addProxyMethod(equalsMethod, Object.class); //添加toString方法 this.addProxyMethod(toStringMethod, Object.class); Class[] var1 = this.interfaces; int var2 = var1.length; int var3; Class var4; //遍歷接口數組 for(var3 = 0; var3 < var2; ++var3) { var4 = var1[var3]; Method[] var5 = var4.getMethods(); int var6 = var5.length; //添加接口裏的方法,此時方法體還爲空 for(int var7 = 0; var7 < var6; ++var7) { Method var8 = var5[var7]; this.addProxyMethod(var8, var4); } } Iterator var11 = this.proxyMethods.values().iterator(); List var12; while(var11.hasNext()) { var12 = (List)var11.next(); checkReturnTypes(var12); } Iterator var15; try { //添加一個帶有InvocationHandler的構造方法 this.methods.add(this.generateConstructor()); var11 = this.proxyMethods.values().iterator(); //生成方法體代碼 while(var11.hasNext()) { var12 = (List)var11.next(); var15 = var12.iterator(); while(var15.hasNext()) { ProxyGenerator.ProxyMethod var16 = (ProxyGenerator.ProxyMethod)var15.next(); this.fields.add(new ProxyGenerator.FieldInfo(var16.methodFieldName, "Ljava/lang/reflect/Method;", 10)); //方法體裏生成調用InvocationHandler的invoke方法代碼 this.methods.add(var16.generateMethod()); } } this.methods.add(this.generateStaticInitializer()); } catch (IOException var10) { throw new InternalError("unexpected I/O Exception", var10); } if (this.methods.size() > 65535) { throw new IllegalArgumentException("method limit exceeded"); } else if (this.fields.size() > 65535) { throw new IllegalArgumentException("field limit exceeded"); } else { this.cp.getClass(dotToSlash(this.className)); this.cp.getClass("java/lang/reflect/Proxy"); var1 = this.interfaces; var2 = var1.length; //生成實現接口,繼承Proxy類代碼 for(var3 = 0; var3 < var2; ++var3) { var4 = var1[var3]; this.cp.getClass(dotToSlash(var4.getName())); } this.cp.setReadOnly(); ByteArrayOutputStream var13 = new ByteArrayOutputStream(); DataOutputStream var14 = new DataOutputStream(var13); try { var14.writeInt(-889275714); var14.writeShort(0); var14.writeShort(49); this.cp.write(var14); var14.writeShort(this.accessFlags); var14.writeShort(this.cp.getClass(dotToSlash(this.className))); var14.writeShort(this.cp.getClass("java/lang/reflect/Proxy")); var14.writeShort(this.interfaces.length); Class[] var17 = this.interfaces; int var18 = var17.length; for(int var19 = 0; var19 < var18; ++var19) { Class var22 = var17[var19]; var14.writeShort(this.cp.getClass(dotToSlash(var22.getName()))); } var14.writeShort(this.fields.size()); var15 = this.fields.iterator(); while(var15.hasNext()) { ProxyGenerator.FieldInfo var20 = (ProxyGenerator.FieldInfo)var15.next(); var20.write(var14); } var14.writeShort(this.methods.size()); var15 = this.methods.iterator(); while(var15.hasNext()) { ProxyGenerator.MethodInfo var21 = (ProxyGenerator.MethodInfo)var15.next(); var21.write(var14); } var14.writeShort(0); return var13.toByteArray(); } catch (IOException var9) { throw new InternalError("unexpected I/O Exception", var9); } } }
從源碼能夠得出:
1. JDK幫咱們生成了這樣一個class數據,它繼承了Proxy類,添加了一個帶InvocationHandler參數的構造方法,這樣也就明白了爲何使用構造方法反射建立代理對象的時候傳入了一個InvocationHandler參數,由於默認會調用到Proxy類的構造方法,其參數正好是InvocationHandler,賦值給內部的成員變量h
protected Proxy(InvocationHandler h) { Objects.requireNonNull(h); this.h = h; }
2. 實現了咱們指定的接口,並實現了接口裏的方法,同時接口中的方法調用了InvocationHandler的invoke方法
3. 當代理對象執行方法的時候,方法裏面都會執行InvocationHandler的invoke方法,咱們就能夠在這裏執行目標類方法
這樣能夠說動態生成代理class對象實際上是動態生成代理class數據
使用以下代碼將動態生成的class數據保存到硬盤
public static void write(){ byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0", UserLogin.class.getInterfaces()); String path = "D:/Workspaces/com/sun/proxy/LoginProxy.class"; try(FileOutputStream fos = new FileOutputStream(path)) { fos.write(classFile); fos.flush(); System.out.println("代理類class文件寫入成功"); } catch (Exception e) { System.out.println("寫文件錯誤"); } }
接下來將這個class文件反編譯成Java文件看看:若是你沒有jad工具,能夠經過JAD Java Decompiler(https://varaneckas.com/jad/)下載,使用以下命令
/** * -d :用戶指定輸出文件保存目錄 * d:\ :具體目錄目錄 * -sjava :輸出文件擴展名 這裏保存成Java文件 * D:\Workspaces\com\sun\proxy\LoginProxy.class :class文件 */ jad -d d:\ -sjava D:\Workspaces\com\sun\proxy\LoginProxy.class /** * 上面的命令沒有指定保存的Java文件名,下面的命令能夠指定保存文件名 * -p 輸出詳細文件信息 */ jad -p D:\Workspaces\com\sun\proxy\LoginProxy.class > D:\Workspaces\com\sun\proxy\LoginProxy.java
代理類:
public final class $Proxy0 extends Proxy implements ILogin { public $Proxy0(InvocationHandler invocationhandler) { super(invocationhandler); } public final boolean equals(Object obj) { try { return ((Boolean)super.h.invoke(this, m1, new Object[] { obj })).booleanValue(); } catch(Error _ex) { } catch(Throwable throwable) { throw new UndeclaredThrowableException(throwable); } } public final String toString() { try { return (String)super.h.invoke(this, m2, null); } catch(Error _ex) { } catch(Throwable throwable) { throw new UndeclaredThrowableException(throwable); } } public final void userLogin() { try { super.h.invoke(this, m3, null); return; } catch(Error _ex) { } catch(Throwable throwable) { throw new UndeclaredThrowableException(throwable); } } public final int hashCode() { try { return ((Integer)super.h.invoke(this, m0, null)).intValue(); } catch(Error _ex) { } catch(Throwable throwable) { throw new UndeclaredThrowableException(throwable); } } private static Method m1; private static Method m2; private static Method m3; private static Method m0; 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.mango.demo.proxy.ILogin").getMethod("userLogin", new Class[0]); m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]); } catch(NoSuchMethodException nosuchmethodexception) { throw new NoSuchMethodError(nosuchmethodexception.getMessage()); } catch(ClassNotFoundException classnotfoundexception) { throw new NoClassDefFoundError(classnotfoundexception.getMessage()); } } }
至此就解答了這一節【動態生成代理對象原理】剛開始提出的幾個疑問了
JDK動態代理是實現AOP編程的一種途徑,能夠在執行指定方法先後貼入本身的邏輯,像Spring、Struts2就有用到該技術(攔截器設計就是基於AOP的思想);只不過JDK動態代理只能實現基於接口的動態代理,也算是一個遺憾,還有這種實現方式也不能避免類數量增長,由於你必需要爲每一個業務類編寫業務接口;那麼有沒有不用寫代理類、也不用寫業務接口的代理方法呢?
方法是有的,須要用到CGLib了:
CGLIB(Code Generation Library)是一個強大的,高性能,高質量的Code生成類庫,它能夠在運行期擴展Java類與實現Java接口CGLIB比JDK的代理更增強大,不僅能夠實現接口,還能夠擴展類,經過字節碼技術爲一個類建立子類,並在子類中採用方法攔截的技術攔截全部父類方法的調用,順勢植入橫切邏輯。但由於採用的是繼承,因此不能對final修飾的類進行代理
CGLIB底層封裝了ASM,經過對字節碼的操做來生成類,具備更高的性能,可是CGLIB建立代理對象時所花費的時間卻比JDK多;ASM是一套JAVA字節碼生成架構,可以動態生成.class文件並在加載進內存以前進行修改
使用CGLIB須要引用jar包cglib-nodep-3.2.5.jar(若是引入cglib.jar,還須要引入asm的jar包)
像Spring框架都支持這兩種動態代理方式,但Spring默認使用JDK動態代理,在須要代理類而不是代理接口的時候,Spring會自動切換爲使用CGLIB代理,不過如今的項目都是面向接口編程,因此JDK動態代理相對來講用的仍是多一些。
至於在咱們本身的業務裏須要選擇哪一種方式實現AOP,仍是須要視狀況而定,兩種方式組合使用或許能支持更多的業務需求。