這一篇我們說說反射和動態代理,為什么這兩個要一起說呢?因為動態代理中會用到反射,而且java中反射的用處太多了,基本上無處不在,而且功能十分強大;
1.反射簡介
反射是什么呢?一般都是很專業的說法:在運行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;我最初看這句話我是沒看出來什么厲害的地方,運行狀態?什么是運行狀態啊?
簡單看看下面這個圖,類的加載機制以前說過了,這里就隨意看看,一個類的字節碼文件通過類加載器加載到jvm的堆中最終會生成一個該類的Class對象,注意,一個類只有一個Class對象,而且通過該類的所有實例對象都可以得到這個Class對象,反過來說通過Class對象我們也可以實例化對象;
那么假如我們通過程序可以獲取到java堆中的Class對象,那么我們不就可以自由的實例化對象,而不需要總是依靠new關鍵字了么!這就是所謂的java反射,而運行狀態指的是該類的字節碼文件必須加載並且在java堆中生成對應的Class對象!
那么現在我們就要想辦法從外部程序怎么獲取這個Class對象,一般有三種方法,個人感覺對應於三個階段比較好記一點,下圖所示,有自己獨特的記憶方法是最好的;
我感覺最好把Class對象看作是student.java在內存中另外一種表現形式,這樣你才能更好理解反射的各種用法。。。
2.反射的簡單使用
我們既然得到了一個類的Class對象,這個Class對象中肯定包含了該類的屬性和方法的所有信息,換句話說就是可以調用里面的各種方法(公共方法和私有方法)、獲取修飾符、獲取構造器、得到類名和方法名等等,簡單列舉一下最基本的方法:
getName():獲得類的完整名字。
getFields():獲得類的public類型的屬性。
getDeclaredFields():獲得類的所有屬性。包括private 聲明的和繼承類
getMethods():獲得類的public類型的方法。
getDeclaredMethods():獲得類的所有方法。包括private 聲明的和繼承類
getMethod(String name, Class[] parameterTypes):獲得類的特定方法,name參數指定方法的名字,parameterTypes 參數指定方法的參數類型。
getConstructors():獲得類的public類型的構造方法。
getConstructor(Class[] parameterTypes):獲得類的特定構造方法,parameterTypes 參數指定構造方法的參數類型。
newInstance():通過類的不帶參數的構造方法創建這個類的一個對象。
基於這些方法我們下面我們就寫個最簡單的例子來使用一下這些方法;
package com.wyq.day527; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; //這個類有私有屬性,私有方法,公開屬性,公開方法,無參構造器,有參構造器,get/set方法 public class Student { private String name = "小花"; public int age = 4; public Student() { } public Student(String name, int age) { this.name = name; this.age = age; } public void say(String man){ System.out.println(man + "大聲說話。。。"); } private void listen(String man){ System.out.println(man + "小聲聽歌"); } @Override public String toString() { return "Student [name=" + name + ", age=" + age + "]"; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public static void main(String[] args) throws NoSuchFieldException, SecurityException, InstantiationException, IllegalAccessException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException { //獲取Student的Class對象,后面的所有操作都是根據這個來的 Class<Student> clazz = Student.class; //獲取全類名 String className = clazz.getName(); System.out.println("1:"+className); //獲取類中所有屬性全名 Field[] fields = clazz.getDeclaredFields(); for (int i = 0; i < fields.length; i++) { System.out.println("2:"+fields[i]); } //獲取類中所有方法全名 Method[] methods = clazz.getDeclaredMethods(); for (int i = 0; i < methods.length; i++) { System.out.println("3:"+methods[i]); } //根據空構造器來實例化對象,並調用其中的listen方法 Student instance = clazz.newInstance(); System.out.print("4:"); instance.listen("小王"); //首先通過空構造器實例對象,然后獲取指定方法名(這里會指定方法參數類型),然后通過invoke方法來調用該指定方法 //注意這種調用方法和上面這種的區別,好像是這種可以捕捉異常,更安全吧! Student instance3 = clazz.newInstance(); Method method = clazz.getDeclaredMethod("say", String.class); System.out.print("5:"); method.invoke(instance3, "張三"); //調用空構造器實例對象,獲取指定屬性名,注意,假如該屬性是私有的,一定要調用field.setAccessible(true),不然會報錯 //然后就是設置屬性值 Student instance4 = clazz.newInstance(); Field field = clazz.getDeclaredField("age"); field.setAccessible(true); field.set(instance4, 20); System.out.println("6:"+instance4.getAge()); //調用有參構造器傳入參數來實例化對象,並調用其中的toString()方法 Constructor<Student> constructor = clazz.getDeclaredConstructor(String.class,int.class); Student instance2 = constructor.newInstance("java小新人",18); System.out.println("7:"+instance2.toString()); } }
測試結果為:
補充一點小東西:能不能直接通過反射實例化一個類的對象,然后去調用父類中的方法或屬性呢?假如不用反射的話是可以直接調用父類的方法的,但是這里不能,所以我們可以想辦法獲取父類的Class對象,比如上面的Student類有個父類是Person類,那么可以通過
Class clazz = Student.class;
Class personclazz = clazz.getSuperclass();
這樣我們就得到了父類的Class對象了,然后就可以跟前面一樣的用了,很簡單吧!
3.代理
代理應該很熟悉了,大白話說就是中介,比如找工作、買房買車等,都可以找找中介,因為這樣可以省很多時間;
在java代碼中的代理其實很容易,就是用一個代理類將目標類封裝起來,我們調用代理類的方法就行了,不需要直接和目標類打交道,畫個簡單的圖:可以看到代理類和目標類的方法名最好要一樣,這樣的好處就是我們使用代理類就和使用目標類一樣;另外,我們可以在代理類的方法中再調用一下其他類的方法,這樣做有個什么好處呢?可以實現給目標類擴展新功能而不需要改變目標類的代碼(專業一點就叫做解耦合)
在java中的代理分為兩種,靜態代理和動態代理:靜態代理就是在源碼階段我們手動的寫個代理類將目標類給包裝起來,這種方式比較水,因為要自己寫代碼,最好可以自動生成這個代理類就最好了;於是就有了動態代理,動態代理就是在運行階段有jvm自動生成這個代理類,我們直接用就好。顯而易見,動態代理才是我們的主菜;
下面就分別說說靜態代理和動態代理:
3.1.靜態代理
這個沒什么好說的,我們看一個最簡單的例子就一目了然了,在這里,我們要思考一下怎么包裝目標類最好呢?我們最好可以讓代理類和目標類都實現同一個接口,那么兩個類的方法名就是一樣的了,然后就是把目標類傳入代理類中
接口:
package com.wyq.day527; public interface Animal { public void run(); public void eat(); }
目標類:
package com.wyq.day527; public class Dog implements Animal{ @Override public void run() { System.out.println("狗----run"); } @Override public void eat() { System.out.println("狗----eat"); } }
代理類及擴展Dog類中eat方法:
package com.wyq.day527; public class DogAgent implements Animal{ //這里就是將通過構造器傳進來的目標類給保存起來 private Dog dog; public DogAgent(Dog dog) { this.dog = dog; } @Override public void run() { dog.run(); } @Override public void eat() { System.out.println("擴展------->這里可以進行日志或者事務處理。。。。。"); dog.eat(); } public static void main(String[] args) { Dog dog1 = new Dog(); DogAgent agent = new DogAgent(dog1); //我們想對eat方法進行擴展,而不用修改Dog類中的源代碼,直接在代理類中進行擴展即可 agent.eat(); } }
測試結果如下,這樣擴展起來很容易,而且對於那些不清楚源代碼的程序員來說完全感覺不到Dog代理類的存在,還以為就是使用Dog類(在很多的框架中大量用到代理的這個思想)。。。
3.2.動態代理
靜態代理有個很大的缺陷,就是代理類需要自己去寫,假如實際項目中用到的類跟我們這里測試的一樣的簡單就好了,那自己寫就自己寫吧!然而實際中一個類中的方法可能有幾十個幾百個,來,你去試試寫個代理類。。。簡直坑爹,而且寫的代碼還都差不多,這就意味着又要為另外一個類寫代理的時候再重復寫一遍,簡直太糟糕了!
為了彌補這個缺陷,一些大佬就設計出了可以自動生成代理類的手段,這就很舒服了,這個手段是比較厲害的,但是有點兒不好理解,要仔細想想!而動態代理有兩種方式,JDK動態代理和CGLib動態代理,下面說的是JDK動態代理。。。。
首先JDK動態代理就不止有代理類和目標類了,還有一個中間類,這個中間類有什么用呢?我們可以畫個圖看看;
上圖可以簡單的知道調用代理類中的所有方法實際上都是調用中間類的invoke方法,而在invoke方法中才是真正去調用對應的目標類的目標方法;這個比靜態代理多了一層結構而已,好好理解一下還是很容易的。。。
在這里java已經為我們提供了Proxy代理類了,我們可以看看這個類中主要的東西:有參構造是傳遞進去一個InvocationHandler類型的參數然后復制給屬性h;然后就是一個方法,這個方法最主要的是其中的三個參數,第一個參數是類加載器,任意類加載器都行,通常用目標類的類加載器即可;第二個參數是目標類實現的接口,跟靜態代理差不多,這里是為了讓代理類和目標類的方法名一樣;第三個參數是一個InvocationHandler類型的參數,注意,這個h是我們要自己寫代碼實現的,而不是屬性中的那個h哦~~
上面的InvocationHandler接口的實現類就是中間類,這個接口中只有一個invoke方法,我們可以用匿名類的形式,直接用new InvocationHandler(){重寫invoke方法} 這種形式;
廢話不多說我們來看一個很簡單的例子就知道了:
接口:
package com.wyq.day527; public interface Animal { public void run(); public void eat(); }
目標類:
package com.wyq.day527; public class Dog implements Animal{ @Override public void run() { System.out.println("狗----run"); } @Override public void eat() { System.out.println("狗----eat"); } }
代理類的使用以及測試結果;
package com.wyq.day527; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class MyProxy { public static void main(String[] args) { //生成$Proxy0的class文件,也就是代理類的字節碼文件 System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); Animal target = new Dog();
//注意類加載器可以是任意一個類加載器,當然我們就隨便用用目標類的類加載器了;獲取目標類接口的方法就不多說了;
//最主要的就是InvocationHandler中的invoke方法中的邏輯,想擴展什么就擴展什么 Animal proxyDog = (Animal)Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("現在執行的所有方法都經過動態代理...."); method.invoke(target, args);//這里有沒有很熟悉,不就是反射么。。。 return null; } }); proxyDog.run(); proxyDog.eat(); } }
4.JDK動態代理源碼
在上面的代碼中,有兩個地方需要再仔細看看,第一個是生成的代理類的字節碼文件,由於動態代理的代理類是動態生成的,我們有沒有辦法拿到其中的源碼看看到底生成了一些什么東西呢?第二個就是invoke方法中用反射去調用目標類的方法,有沒有覺得很奇怪那個參數method,args為什么這么神奇,剛好就是對應於目標類的方法名和方法形參呢?
4.1.反編譯代理類字節碼文件
要想知道這兩個問題我們首先要拿到代理類的字節碼,由於添加了獲取代理類字節碼文件的那行代碼,我們可以在我們的電腦中找到代理類的字節碼文件;
基於eclispe:選中項目,右鍵,選擇最后一個properties,就能看到項目路徑了:
然后進入到該路徑下面,有個com\sun\proxy目錄下:
拿到了字節碼文件,怎么變成源碼文件呢?也就是變成xxx.java這樣的,這里就用到一個小技巧,叫做反編譯,我們可以下載一個小軟件,下圖所示:
反編譯軟件百度雲鏈接:https://pan.baidu.com/s/1czLYYC1Zij2LwQ3ES5fidg 提取碼:d9a0
4.2.代理類源碼
為了代碼簡潔,這里我將一些不重要的代碼進行刪減,然后調整一下順序:
package com.sun.proxy; import com.wyq.day527.Animal; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.lang.reflect.UndeclaredThrowableException; //注意這個代理類的名字$Proxy0,很奇怪的一個名字,也實現了Animal接口,而且還是繼承Proxy這個類,前面我們對這個Proxy這個類簡單的說了一下的,這里就會用到 public final class $Proxy0 extends Proxy implements Animal{ //此處這個靜態代碼塊中就是我們熟悉的反射了,獲取方法的Method對象,可以簡單看作是獲取方法的全名吧 static{ m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") }); m3 = Class.forName("com.wyq.day527.Animal").getMethod("run", new Class[0]); m4 = Class.forName("com.wyq.day527.Animal").getMethod("eat", new Class[0]); m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]); m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]); return; } //保存一下目標類中的方法,其中除了run()和eat()兩個方法之外,還有equals、toString、hashCode方法,這三個默認都是要實現的 private static Method m1; private static Method m3; private static Method m4; private static Method m2; private static Method m0; //這個有參構造將InvocationHandler參數傳給父類保存起來,也就是那個父類樹屬性h,方便后面使用這個h public $Proxy0(InvocationHandler paramInvocationHandler){ super(paramInvocationHandler); } public final boolean equals(Object paramObject){ return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue(); } public final void run(){ //注意,此處的invoke方法可不是反射哦!是調用父類中保存的屬性h,其實就是中間類,調用這個類的invoke方法,好好看一下形參 //this代表當前代理類,m3表示run方法的全類名,null其實就是run方法的參數,這里沒有參數就是null //現在知道為什么在中間類的invoke方法中可以直接用反射了吧,因為目標類的方法的Method對象,目標類和方法參數都准備好了,不就可以用反射了么.... //后面幾個方法都差不多,就不說廢話了 this.h.invoke(this, m3, null); return; } public final void eat(){ this.h.invoke(this, m4, null); return; } public final String toString(){ return (String)this.h.invoke(this, m2, null); } public final int hashCode(){ return ((Integer)this.h.invoke(this, m0, null)).intValue(); }
5.總結
其實反射和動態代理還是很容易的,都是一些很基礎的東西,再說一下用代理的好處,可以避免我們直接和目標類接觸,實現解耦,而且有利於目標類的擴展,而且代理類用起來方式和目標類一樣,所以我們在很多框架中即使用了代理,但是我們通常是感覺不出來的!打個比喻,就好像我們去餐館吃飯,你覺得你是直接去廚房跟廚師說你要吃什么什么,而且別放辣......還是直接和服務員說這些要求比較好呢?差不多的道理吧!
話說有個問題,上面的JDK動態代理必須要目標類要實現某一個或幾個接口,假如我們的類沒有實現接口怎么啊?這就日了狗了,於是就有了CGLib動態代理,這種代理方式剛好彌補了JDK動態代理的缺陷,其實就是生成一個目標類的子類,這個子類就是我們需要的代理類,重寫一下父類的所有方法,那么代理類所有方法的名字就和目標類一樣了,再然后就是反射調用父類的方法,前面說反射的最后那里好像說過了....后面有時間再簡單說說CGLib動態代理吧!
話說向進一步理解JDK動態代理的,可以去Proxy類中的newInstance方法中看看源碼,應該就差不多了。。。。