1. 程式人生 > >Java中的反射機制和動態代理

Java中的反射機制和動態代理

一、反射概述

  反射機制指的是Java在執行時候有一種自觀的能力,能夠了解自身的情況為下一步做準備,其想表達的意思就是:在執行狀態中,對於任意一個類,都能夠獲取到這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意一個方法和屬性(包括私有的方法和屬性),這種動態獲取的資訊以及動態呼叫物件的方法的功能就稱為java語言的反射機制。通俗點講,通過反射,該類對我們來說是完全透明的,想要獲取任何東西都可以,這是一種動態獲取類的資訊以及動態呼叫物件方法的能力。

  想要使用反射機制,就必須要先獲取到該類的位元組碼檔案物件(.class),通過該類的位元組碼物件,就能夠通過該類中的方法獲取到我們想要的所有資訊(方法,屬性,類名,父類名,實現的所有介面等等),每一個類對應著一個位元組碼檔案也就對應著一個Class型別的物件,也就是位元組碼檔案物件。

  Java提供的反射機制,依賴於我們下面要講到的Class類和java.lang.reflect類庫。我們下面要學習使用的主要類有:①Class表示類或者介面;②java.lang.reflect.Field表示類中的成員變數;③java.lang.reflect.Method表示類中的方法;④java.lang.reflect.Constructor表示類的構造方法;⑤Array提供動態陣列的建立和訪問陣列的靜態方法。

二、反射之Class類

(1)初識Class類

  在類Object下面提供了一個方法:,此方法將會被所有的子類繼承,該方法的返回值為一個Class,這個Class類就是反射的源頭。那麼Class類是什麼呢?Class類是Java中用來表達執行時型別資訊的對應類,我們剛剛也說過所有類都會繼承Object類的getClass()方法,那麼也體現了著Java中的每個類都有一個Class物件,當我們編寫並編譯一個建立的類就會產生對應的class檔案並將類的資訊寫到該class檔案中,當我們使用正常方式new一個物件或者使用類的靜態欄位時候,JVM的累加器子系統就會將對應的Class物件載入到JVM中,然後JVM根據這個型別資訊相關的Class物件建立我們需要的例項物件或者根據提供靜態變數的引用值。將Class類稱為類的型別,一個Class物件稱為類的型別物件。

(2)Class有下面的幾個特點

  ①Class也是類的一種(不同於class,class是一個關鍵字);

  ②Class類只有一個私有的建構函式,只有JVM能夠建立Class類的例項;

  ③對於同一類的物件,在JVM中只存在唯一一個對應的Class類例項來描述其資訊;

  ④每個類的例項都會記得自己是由哪個Class例項所生成;

  ⑤通過Class可以完整的得到一個類中的完整結構;

(3)獲取Class類例項

  剛剛說到過Class只有一個私有的建構函式,所以我們不能通過new建立Class例項 ,有下面這幾種獲取Class例項的方法:

  ①Class.forName("類的全限定名"),該方法只能獲取引用型別的類型別物件。該方法會丟擲異常(a.l類載入器在類路徑中沒有找到該類  b.該類被某個類載入器載入到JVM記憶體中,另外一個類載入器有嘗試從同一個包中載入)

1 //Class<T> clazz = Class.forName("類的全限定名");這是通過Class類中的靜態方法forName直接獲取一個Class的物件
2 Class<?> clazz1 = null;
3 try {
4     clazz1 = Class.forName("reflect.Person");
5 } catch (ClassNotFoundException e) {
6     e.printStackTrace();
7 }
8 System.out.println(clazz1); //class reflect.Person

  ②如果我們有一個類的物件例項,那麼通過這個物件的getClass()方法可以獲得他的Class物件,如下所示

 1 //Class<T> clazz = xxx.getClass(); //通過類的例項獲取類的Class物件
 2 Class<?> clazz3 = new Person().getClass();
 3 System.out.println(clazz3); //class reflect.Person
 4 
 5 Class<?> stringClass = "string".getClass();
 6 System.out.println(stringClass); //class java.lang.String
 7 
 8 /**
 9  * [代表陣列,
10  * B代表byte;
11  * I代表int;
12  * Z代表boolean;
13  * L代表引用型別
14  * 組合起來就是指定型別的一維陣列,如果是[[就是二維陣列
15  */
16 Class<?> arrClass = new byte[20].getClass();
17 System.out.println(arrClass); //class [B
18 
19 Class<?> arrClass1 = new int[20].getClass();
20 System.out.println(arrClass1); //class [I
21 
22 Class<?> arrClass2 = new boolean[20].getClass();
23 System.out.println(arrClass2); //class [Z
24 
25 Class<?> arrClass3 = new Person[20].getClass();
26 System.out.println(arrClass3); //class [Lreflect.Person;
27 
28 Class<?> arrClass4 = new String[20].getClass();
29 System.out.println(arrClass4); //class [Ljava.lang.String;

  ③通過類的class位元組碼檔案獲取,通過類名.class獲取該類的Class物件

1 //Class<T> clazz = XXXClass.class; 當類已經被載入為.class檔案時候,
2 Class<Person> clazz2 = Person.class;
3 System.out.println(clazz2);
4 System.out.println(int [][].class);//class [[I
5 System.out.println(Integer.class);//class java.lang.Integer

(4)關於包裝類的靜態屬性

  我們知道,在Java中對於基本型別和void都有對應的包裝類。在包裝類中有一個靜態屬性TYPE儲存了該類的類型別。如下所示

1     /**
2      * The {@code Class} instance representing the primitive type
3      * {@code int}.
4      *
5      * @since   JDK1.1
6      */
7     @SuppressWarnings("unchecked")
8     public static final Class<Integer>  TYPE = (Class<Integer>) Class.getPrimitiveClass("int");

  我們使用這個靜態屬性來獲得Class例項,如下所示

1 Class c0 = Byte.TYPE; //byte
2 Class c1 = Integer.TYPE; //int
3 Class c2 = Character.TYPE; //char
4 Class c3 = Boolean.TYPE; //boolean
5 Class c4 = Short.TYPE; //short
6 Class c5 = Long.TYPE; //long
7 Class c6 = Float.TYPE; //float
8 Class c7 = Double.TYPE; //double
9 Class c8 = Void.TYPE; //void

(5)通過Class類的其他方法獲取

①public native Class<? super T> getSuperclass():獲取該類的父類

1 Class c1 = Integer.class;
2 Class par = c1.getSuperclass();
3 System.out.println(par); //class java.lang.Number

②public Class<?>[] getClasses():獲取該類的所有公共類、介面、列舉組成的Class陣列,包括繼承的;

③public Class<?>[] getDeclaredClasses():獲取該類的顯式宣告的所有類、介面、列舉組成的Class陣列;

④(Class/Field/Method/Constructor).getDeclaringClass():獲取該類/屬性/方法/構造器所在的類

三、Class類的API

  這是下面測試用例中使用的Person類和實現的介面

 1 package reflect;
 2 
 3 interface Test {
 4     String test = "interface";
 5 }
 6 
 7 public class Person  implements Test{
 8 
 9     private String id;
10     private String name;
11 
12     public void sing(String name) {
13         System.out.println(getName() + "會唱" + name +"歌");
14     }
15 
16     private void dance(String name) {
17         System.out.println(getName() + "會跳" + name + "舞");
18     }
19 
20     public void playBalls() {
21         System.out.println(getName() + "會打籃球");
22     }
23 
24     public String getId() {
25         return id;
26     }
27 
28     public void setId(String id) {
29         this.id = id;
30     }
31 
32     public String getName() {
33         return name;
34     }
35 
36     public void setName(String name) {
37         this.name = name;
38     }
39 
40 
41     public Person() {
42     }
43 
44     public Person(String id, String name) {
45         this.id = id;
46         this.name = name;
47     }
48 
49     public Person(String id) {
50         this.id = id;
51     }
52 
53     @Override
54     public String toString() {
55         return "Person{" +
56                 "id='" + id + '\'' +
57                 ", name='" + name + '\'' +
58                 '}';
59     }
60 }
Person

1、建立例項物件

1 public void test4() throws Exception{
2     Class clazz =Class.forName("reflect.Person");
3     Person person = (Person)clazz.newInstance();
4     System.out.println(person);
5 }

  建立執行時類的物件,使用newInstance(),實際上就是呼叫執行時指定類的無參構造方法。這裡也說明要想建立成功,需要對應的類有無參構造器,並且構造器的許可權要足夠,否則會丟擲下面的異常。

  ①我們顯示宣告Person類一個帶參構造,並沒有無參構造,這種情況會丟擲InstantiationException

  

  ②更改無參構造器訪問許可權為private

  

2、獲取構造器

(1)獲取指定可訪問的構造器建立物件例項

  上面我們所說的使用newInstance方法建立物件,如果不指定任何引數的話預設是呼叫指定類的無參構造器的。那麼如果沒有無參構造器,又想建立物件例項怎麼辦呢,就使用 Class類提供的獲取構造器的方法,顯示指定我們需要呼叫哪一個無參構造器。

1 @Test
2 public void test5() throws Exception {
3     Class clazz = Class.forName("reflect.Person");
4     //獲取帶參構造器
5     Constructor constructor = clazz.getConstructor(String.class, String .class);
6     //通過構造器來例項化物件
7     Person person = (Person) constructor.newInstance("p1", "person");
8     System.out.println(person);
9 }

  當我們指定的構造器全部不夠(比如設定為private),我們在呼叫的時候就會丟擲下面的異常

  

(2)獲得全部構造器

 1 @Test
 2 public void test6() throws Exception {
 3     Class clazz1 = Class.forName("reflect.Person");
 4     Constructor[] constructors = clazz1.getConstructors();
 5     for (Constructor constructor : constructors) {
 6         Class[] parameters = constructor.getParameterTypes();
 7         System.out.println("建構函式名:" + constructor + "\n" + "引數");
 8         for (Class c: parameters) {
 9             System.out.print(c.getName() + " ");
10         }
11         System.out.println();
12     }
13 }

  執行結果如下

  

3、獲取成員變數並使用Field物件的方法

  (1)Class.getField(String)方法可以獲取類中的指定欄位(可見的), 如果是私有的可以用getDeclaedField("name")方法獲取,通過set(物件引用,屬性值)方法可以設定指定物件上該欄位的值, 如果是私有的需要先呼叫setAccessible(true)設定訪問許可權,用獲取的指定的欄位呼叫get(物件引用)可以獲取指定物件中該欄位的值。

 1 @Test
 2 public void test7() throws Exception {
 3     Class clazz1 = Class.forName("reflect.Person");
 4     //獲得例項物件
 5     Person person = (Person) clazz1.newInstance();
 6     /**
 7      * 獲得類的屬性資訊
 8      * 使用getField(name),通過指定的屬性name獲得
 9      * 如果屬性不是public的,使用getDeclaredField(name)獲得
10      */
11     Field field = clazz1.getDeclaredField("id");
12     //如果是private的,需要設定許可權為可訪問
13     field.setAccessible(true);
14     //設定成員變數的屬性值
15     field.set(person, "person1");
16     //獲取成員變數的屬性值,使用get方法,其中第一個引數表示獲得欄位的所屬物件,第二個引數表示設定的值
17     System.out.println(field.get(person)); //這裡的field就是id屬性,列印person物件的id屬性的值
18 }

  (2)獲得全部成員變數

 1 @Test
 2 public void test8() throws Exception{
 3     Class clazz1 = Class.forName("reflect.Person");
 4     //獲得例項物件
 5     Person person = (Person) clazz1.newInstance();
 6     person.setId("person1");
 7     person.setName("person1_name");
 8     Field[] fields = clazz1.getDeclaredFields();
 9     for (Field f : fields) {
10         //開啟private成員變數的可訪問許可權
11         f.setAccessible(true);
12         System.out.println(f+ ":" + f.get(person));
13     }
14 }

  

4、獲取方法並使用method

  (1)使用Class.getMethod(String, Class...) 和 Class.getDeclaredMethod(String, Class...)方法可以獲取類中的指定方法,如果為私有方法,則需要開啟一個許可權。setAccessible(true);用invoke(Object, Object...)可以呼叫該方法。如果是私有方法而使用的是getMethod方法來獲得會丟擲NoSuchMethodException

 1 @Test
 2 public void test9() throws Exception{
 3     Class clazz1 = Class.forName("reflect.Person");
 4     //獲得例項物件
 5     Person person = (Person) clazz1.newInstance();
 6     person.setName("Person");
 7     //①不帶引數的public方法
 8     Method playBalls = clazz1.getMethod("playBalls");
 9     //呼叫獲得的方法,需要指定是哪一個物件的
10     playBalls.invoke(person);
11 
12     //②帶參的public方法:第一個引數是方法名,後面的可變引數列表是引數型別的Class型別
13     Method sing = clazz1.getMethod("sing",String.class);
14     //呼叫獲得的方法,呼叫時候傳遞引數
15     sing.invoke(person,"HaHaHa...");
16 
17     //③帶參的private方法:使用getDeclaredMethod方法
18     Method dance = clazz1.getDeclaredMethod("dance", String.class);
19     //呼叫獲得的方法,需要先設定許可權為可訪問
20     dance.setAccessible(true);
21     dance.invoke(person,"HaHaHa...");
22 }

  (2)獲得所有方法(不包括構造方法)

 1 @Test
 2 public void test10() throws Exception{
 3     Class clazz1 = Class.forName("reflect.Person");
 4     //獲得例項物件
 5     Person person = (Person) clazz1.newInstance();
 6     person.setName("Person");
 7     Method[] methods = clazz1.getDeclaredMethods();
 8     for (Method method: methods) {
 9         System.out.print("方法名" + method.getName() + "的引數是:");
10         //獲得方法引數
11         Class[] params = method.getParameterTypes();
12         for (Class c : params) {
13             System.out.print(c.getName() + " ");
14         }
15         System.out.println();
16     }
17 }

5、獲得該類的所有介面

  Class[] getInterfaces():確定此物件所表示的類或介面實現的介面,返回值:介面的位元組碼檔案物件的陣列

1 @Test
2 public void test11() throws Exception{
3     Class clazz1 = Class.forName("reflect.Person");
4     Class[] interfaces = clazz1.getInterfaces();
5     for (Class inter : interfaces) {
6         System.out.println(inter);
7     }
8 }

6、獲取指定資源的輸入流

  InputStream getResourceAsStream(String name),返回值:一個 InputStream 物件;如果找不到帶有該名稱的資源,則返回 null;引數:所需資源的名稱,如果以"/"開始,則絕對資源名為"/"後面的一部分。

 1 @Test
 2 public void test12() throws Exception {
 3     ClassLoader loader = this.getClass().getClassLoader();
 4     System.out.println(loader);//sun.misc.Launcher$AppClassLoader@18b4aac2 ,應用程式類載入器
 5     System.out.println(loader.getParent());//sun.misc.Launcher$ExtClassLoader@31befd9f ,擴充套件類載入器
 6     System.out.println(loader.getParent().getParent());//null ,不能獲得啟動類載入器
 7 
 8     Class clazz = Person.class;//自定義的類
 9     ClassLoader loader2 = clazz.getClassLoader();
10     System.out.println(loader2);//sun.misc.Launcher$AppClassLoader@18b4aac2
11 
12     //下面是獲得InputStream的例子
13     ClassLoader inputStreamLoader = this.getClass().getClassLoader();
14     InputStream inputStream = inputStreamLoader.getResourceAsStream("person.properties");
15     Properties properties = new Properties();
16     properties.load(inputStream);
17     System.out.println("id:" + properties.get("id"));
18     System.out.println("name:" + properties.get("name"));
19 }

  其中properties檔案內容

1 id = person001
2 name = person-name1
View Code

五、反射的應用之動態代理

  代理模式在Java中應用十分廣泛,它說的是使用一個代理將物件包裝起來然後用該代理物件取代原始物件,任何原始物件的呼叫都需要通過代理物件,代理物件決定是否以及何時將方法呼叫轉到原始物件上。這種模式可以這樣簡單理解:你自己想要做某件事情(被代理類),但是覺得自己做非常麻煩或者不方便,所以就叫一個另一個人(代理類)來幫你做這個事情,而你自己只需要告訴要做啥事就好了。上面我們講到了反射,在下面我們會說一說java中的代理

 1、靜態代理

  靜態代理其實就是程式執行之前,提前寫好被代理類的代理類,編譯之後直接執行即可起到代理的效果,下面會用簡單的例子來說明。在例子中,首先我們有一個頂級介面(ProductFactory),這個介面需要代理類(ProxyTeaProduct)和被代理類(TeaProduct)都去實現它,在被代理類中我們重寫需要實現的方法(action),該方法會交由代理類去選擇是否執行和在何處執行;被代理類中主要是提供頂級介面的的一個引用但是引用實際指向的物件則是實現了該介面的代理類(使用多型的特點,在代理類中提供構造器傳遞實際的物件引用)。分析之後,我們通過下面這個圖理解一下這個過程。

 1 package proxy;
 2 
 3 /**
 4  * 靜態代理
 5  */
 6 //產品介面
 7 interface ProductFactory {
 8     void action();
 9 }
10 
11 //一個具體產品的實現類,作為一個被代理類
12 class TeaProduct implements ProductFactory{
13     @Override
14     public void action() {
15         System.out.println("我是生產茶葉的......");
16     }
17 }
18 
19 //TeaProduct的代理類
20 class ProxyTeaProduct implements ProductFactory {
21     //我們需要ProductFactory的一個實現類,去代理這個實現類中的方法(多型)
22     ProductFactory productFactory;
23 
24     //通過構造器傳入實際被代理類的物件,這時候代理類呼叫action的時候就可以在其中執行被代理代理類的方法了
25     public ProxyTeaProduct(ProductFactory productFactory) {
26         this.productFactory = productFactory;
27     }
28 
29     @Override
30     public void action() {
31         System.out.println("我是代理類,我開始代理執行方法了......");
32         productFactory.action();
33     }
34 }
35 public class TestProduct {
36 
37     public static void main(String[] args) {
38         //建立代理類的物件
39         ProxyTeaProduct proxyTeaProduct = new ProxyTeaProduct(new TeaProduct());
40         //執行代理類代理的方法
41         proxyTeaProduct.action();
42     }
43 }

  那麼程式測試的輸出結果也很顯然了,代理類執行自己實現的方法,而在其中有呼叫了被代理類的方法

  

  那麼我們想一下,上面這種稱為靜態代理的方式有什麼缺點呢?因為每一個代理類只能為一個藉口服務(因為這個代理類需要實現這個介面,然後去代理介面實現類的方法),這樣一來程式中就會產生過多的代理類。比如說我們現在又來一個介面,那麼是不是也需要提供去被代理類去實現它然後交給代理類去代理執行呢,那這樣程式就不靈活了。那麼如果有一種方式,就可以處理新新增介面的以及實現那不就更加靈活了嗎,在java中反射機制的存在為動態代理創造了機會

2、JDK中的動態代理

  動態代理是指通過代理類來呼叫它物件的方法,並且是在程式執行使其根據需要建立目標型別的代理物件。它只提供一個代理類,我們只需要在執行時候動態傳遞給需要他代理的物件就可以完成對不同介面的服務了。看下面的例子。(JDK提供的代理正能針對介面做代理,也就是下面的newProxyInstance返回的必須要是一個介面)

 1 package proxy;
 2 
 3 import java.lang.reflect.InvocationHandler;
 4 import java.lang.reflect.Method;
 5 import java.lang.reflect.Proxy;
 6 
 7 /**
 8  * JDK中的動態代理
 9  */
10 //第一個介面
11 interface TargetOne {
12     void action();
13 }
14 //第一個介面的被代理類
15 class TargetOneImpl implements TargetOne{
16     @Override
17     public void action() {
18         System.out.println("我會實現父介面的方法...action");
19     }
20 }
21 
22 
23 //動態代理類
24 class DynamicProxyHandler implements InvocationHandler {
25     //介面的一個引用,多型的特性會使得在程式執行的時候,它實際指向的是實現它的子類物件
26     private TargetOne targetOne;
27     //我們使用Proxy類的靜態方法newProxyInstance方法,將代理物件偽裝成那個被代理的物件
28     /**
29      * ①這個方法會將targetOne指向實際實現介面的子類物件
30      * ②根據被代理類的資訊返回一個代理類物件
31      */
32     public Object setObj(TargetOne targetOne) {
33         this.targetOne = targetOne;
34         //    public static Object newProxyInstance(ClassLoader loader, //被代理類的類載入器
35         //                                          Class<?>[] interfaces, //被代理類實現的介面
36         //                                          InvocationHandler h) //實現InvocationHandler的代理類物件
37         return Proxy.newProxyInstance(targetOne.getClass().getClassLoader(),targetOne.getClass().getInterfaces(),this);
38     }
39     //當通過代理類的物件發起對介面被重寫的方法的呼叫的時候,都會轉換為對invoke方法的呼叫
40     @Override
41     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
42         System.out.println("這是我代理之前要準備的事情......");
43         /**
44          *      這裡回想一下在靜態代理的時候,我們顯示指定代理類需要執行的是被代理類的哪些方法;
45          *      而在這裡的動態代理實現中,我們並不知道代理類會實現什麼方法,他是根據執行時通過反射來
46          *  知道自己要去指定被代理類的什麼方法的
47          */
48         Object returnVal = method.invoke(this.targetOne,args);//這裡的返回值,就相當於真正呼叫的被代理類方法的返回值
49         System.out.println("這是我代理之後要處理的事情......");
50         return returnVal;
51     }
52 }
53 public class TestProxy {
54     public static void main(String[] args) {
55         //建立被代理類的物件
56         TargetOneImpl targetOneImpl = new TargetOneImpl();
57         //建立實現了InvocationHandler的代理類物件,然後呼叫其中的setObj方法完成兩項操作
58         //①將被代理類物件傳入,執行時候呼叫的是被代理類重寫的方法
59         //②返回一個類物件,通過代理類物件執行介面中的方法
60         DynamicProxyHandler dynamicProxyHandler = new DynamicProxyHandler();
61         TargetOne targetOne = (TargetOne) dynamicProxyHandler.setObj(targetOneImpl);
62         targetOne.action(); //呼叫該方法執行時都會轉為對DynamicProxyHandler中的invoke方法的呼叫
63     }
64 }

  執行結果如下。現在我們對比jdk提供的動態代理和我們剛剛實現的靜態代理,剛剛說到靜態代理對於新新增的介面需要定義對應的代理類去代理介面的實現類。而上面的測試程式所使用的動態代理規避了這個問題,即我們不需要顯示的指定每個介面對應的代理類,有新的介面新增沒有關係,只需要在使用的時候傳入介面對應的實現類然後返回代理類物件(介面實現型別),然後呼叫被代理類的方法即可。

  &n