這一篇咱們說說反射和動態代理,爲何這兩個要一塊兒說呢?由於動態代理中會用到反射,並且java中反射的用處太多了,基本上無處不在,並且功能十分強大;java
1.反射簡介程序員
反射是什麼呢?通常都是很專業的說法:在運行狀態中,對於任意一個類,都可以知道這個類的全部屬性和方法;我最初看這句話我是沒看出來什麼厲害的地方,運行狀態?什麼是運行狀態啊?安全
簡單看看下面這個圖,類的加載機制之前說過了,這裏就隨意看看,一個類的字節碼文件經過類加載器加載到jvm的堆中最終會生成一個該類的Class對象,注意,一個類只有一個Class對象,並且經過該類的全部實例對象均可以獲得這個Class對象,反過來講經過Class對象咱們也能夠實例化對象;框架
那麼假如咱們經過程序能夠獲取到java堆中的Class對象,那麼咱們不就能夠自由的實例化對象,而不須要老是依靠new關鍵字了麼!這就是所謂的java反射,而運行狀態指的是該類的字節碼文件必須加載而且在java堆中生成對應的Class對象!jvm
那麼如今咱們就要想辦法從外部程序怎麼獲取這個Class對象,通常有三種方法,我的感受對應於三個階段比較好記一點,下圖所示,有本身獨特的記憶方法是最好的;ide
我感受最好把Class對象看做是student.java在內存中另一種表現形式,這樣你才能更好理解反射的各類用法。。。測試
2.反射的簡單使用this
咱們既然獲得了一個類的Class對象,這個Class對象中確定包含了該類的屬性和方法的全部信息,換句話說就是能夠調用裏面的各類方法(公共方法和私有方法)、獲取修飾符、獲取構造器、獲得類名和方法名等等,簡單列舉一下最基本的方法:spa
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方法中看看源碼,應該就差很少了。。。。