什么是反射?
Java安全可以從反序列化漏洞說起,反序列化漏洞又可以從反射說起。反射是⼤多數語⾔⾥都必不可少的組成部分,對象可以通過反射獲取他的類,類可以通過反射拿到所有⽅法(包括私有),拿到的⽅法可以調⽤,總之通過“反射”,我們可以將Java這種靜態語⾔附加上動態特性。可能說完這一兩句話大家還是不知道反射是個啥玩意,現在為了讓大家容易理解,先為大家提出一個需求,通過這個需要來引出反射。需求如下:
根據配置文件re.properties指定信息,創建對象並調用方法。
classfullpath=com.lxflxf.Cat
method=hi
這樣的需求在學習框架時很多,即在通過外部文件配置,在不修改源碼的情況下,來控制程序。
我們使用現有技術可以做到嗎?咱們可以動手寫一下。
首先創建配置文件,寫入上述內容,然后創建一個類,寫入如下內容:
public class Cat {
private String name = "小貓";
public void hi(){
System.out.println("hi" + name);
}
}
傳統的方法是不是我們可以先new一個對象,然后再調用它的方法。寫法如下:
Cat cat = new Cat();
cat.hi();
通過傳統方法,確實可以調用hi()方法,但是這和我們的需求不一樣,這里我們是要根據配置文件re.properties指定信息來完成。到了這里,有同學就說了,咱們可以通過IO流的方式來讀取配置文件的信息。好,咱們用代碼來寫一下。
使用Properties來讀寫配置文件。案例代碼如下:
Properties properties = new Properties();
properties.load(new FileInputStream("src//re.properties"));
String classfullpath = properties.get("classfullpath").toString();
String methodName = properties.get("method").toString();
System.out.println("classfullpath" + classfullpath);
System.out.println("methodName=" + methodName);
運行一下,發現成功讀取到內容。
然后需要創建對象,怎么創建對象呢?有同學就說了,咱們可以直接new classfullpath
,這樣不就好了嘛?嗯,想法不錯,下回不要想了。不要忘記了,我們現在的classfullpath可是字符串類型,怎么能去new
呢。所以現有技術是做不到這個事情的。那么這里就要引入我們要將的重點——反射機制。
為了能更好的理解反射,這里先寫一個小案例,然后在去解釋。
第一步、加載類,返回Class類型的對象cls
Class cls = Class.forName(classfullpath);
第二步、通過cls得到你加載的類 com.lxflxf.Cat 的對象實例
Object o = cls.newInstance();
可能有同學會問,你怎么知道這里拿到的是com.lxflxf.Cat呢,我們可以打印一下來看看,System.out.println(o.getClass())
輸出結果如下:
第三步、通過cls得到你加載的類 com.lxflxf.Cat 的 methodName 的方法對象,我們可以在反射中,把方法視為對象。
Method method1 = cls.getMethod(methodName);
最后、通過method1調用方法、也就是通過方法對象來實現調用方法
method1.invoke(o);
在這里我們也能發現反射和傳統方法的區別了,傳統方法是對象.方法(),反射中呢,是方法.invoke(對象)。那我們運行一下,看看能否輸出方法里的內容呢,如下:
說到這里大家腦海里應該也有了反射的概念。其實反射機制還有一個優點,那就是可以通過外部文件配置,在不修改源碼的情況下,來控制程序。比如這里,我在Cat類下面再寫一個方法,cry()方法,代碼如下:
public void cry(){
System.out.println(name + "......喵喵喵");
}
如果我們使用傳統方法,要調用這個方法,是不是就要修改代碼了,比如cat.cry();
這樣的,那通過反射,我們只需要修改配置文件就可以了,在配置文件re.properties中,將method=hi改為method=cry,就可以了。
運行,發現成功調用並輸出了內容,實現了改配置文件,不改代碼,完成了解藕。
反射機制
上文中,通過一個小案例來簡單的了解了一下反射,現在來系統的說一下。反射機制允許程序在執行期借助於ReflectionAPI取得任何類的內部信息(比如成員變量、構造器、成員方法等等),並能操作對象的屬性及方法。加載完類后,在堆中就產生了一個Class類型的對象(一個類只有一個Class對象),這個對象包含了類的完整結構信息。通過這個對象得到類的結構。為了便於理解,在這里為大家畫一下Java反射機制原理示意圖。如下:
然后現在做一個小小的總結,Java反射機制可以完成:
- 在運行時判斷任意一個對象所屬的類
- 在運行時構造任意一個類的對象
- 在運行時得到任意一個類所具有的成員變量和方法
- 在運行時調用任意一個對象的成員變量和方法
- 生成動態代理
反射相關的主要類如下:
1、Java.long.Class:代表一個類,Class對象表示某個類加載后在堆中的對象
2、Java.lang.reflect.Method:代表類的方法
3、Java.lang.reflect.Field:代表類的成員變量
4、Java.lang.reflect.Constructor:代表類的構造方法
上文的案例代碼中,我們使用了Method和Class相關的方法,現在演示一下,通過Field來拿到成員變量,代碼如下:
Field name = cls.getField("name");
System.out.println(name.get(o));
發現成功拿到了成員變量的值
Class類分析
接下來對Class類特點進行一下梳理。先看看Class類圖
我們發現它的父類仍然是Object。
然后第二點是,Class類對象不是new出來的,而是系統創建的。這里怎么理解呢,還記得上面咱們畫的原理圖嗎?Class類是由loadClass()方法完成類加載,生成了某個類對應的Class類對象。現在為大家演示一下。寫如下案例代碼:
Class<?> aClass = Class.forName("com.lxflxf.Cat");
然后這這句代碼的前面下一個斷點,進行調試。成功進入ClassLoader類中,到了loadClass()方法。如下:
接下來說第三點,對於某個類的Class類對象,在內存中只有一份,因為類只加載一次。現在寫一個小案例來驗證一下這個事情,通過ha shCode來判斷,寫如下幾行代碼:
Class<?> cls1 = Class.forName("com.lxflxf.Cat");
Class<?> cls2 = Class.forName("com.lxflxf.Cat");
System.out.println(cls1.hashCode());
System.out.println(cls2.hashCode());
執行結果如下圖,值相同
最后關於Class類對象還有兩點說一下,一是每個類的實例都會記得自己是由哪個Class實例所生成,二是Class對象可以完整地得到一個類的完整結構,通過一系列的API。
Class類常用方法
這里通過寫小案例的方式,為大家說說Class類常用方法,首先新建一個Car類,代碼如下:
public class Car {
public String brand;
public int price;
public String color;
}
然后我要獲取到Car類對應的Class對象,這里用到的就是forName()方法:
String classAllPath = "com.lxflxf.Car";
//獲取到Car類對應的Class對象
Class cls = Class.forName(classAllPath);
我們可以輸出一下
System.out.println(cls);
System.out.println(cls.getClass());
第一個輸出的是cls對象,是哪個類的Class對象,第二個輸出的是cls運行類型,如下圖:
如果我想要得到包名,可以通過getPackageName()方法,可以通過System.out.println(cls.getPackageName())
,輸出內容為com.lxflxf
。如果想得到類名,可以通過getName()方法。還有一個很重要的方法,那就是創建對象實例:newInstance(),案例如:Object o = cls.newInstance();
,這里也需要注意一點,在JDK1.9往上,不再使用newInstance()。還可以通過getField()獲取到屬性。還有一寫其他方法,這里就不一一舉例了。列了一個表格,如下:
前面說了這么多,那哪些類型有Class對象呢?如下列表:
- 外部類,成員內部類,靜態內部類,局部內部類,匿名內部類
- interface:接口
- 數組
- enum: 枚舉
- annotation: 注解
- 基本數據類型
- void
案例代碼如下:
Class<String> cls1 = String.class; //外部類
Class<Serializable> cls2 = Serializable.class; //接口
Class<Integer[]> cls3 = Integer[].class; //數組
Class<Deprecated> cls4 = Deprecated.class; //注解
System.out.println(cls1);
System.out.println(cls2);
System.out.println(cls3);
System.out.println(cls4);
輸出結果如下:
動態加載
在文章最開始,就說了一下,通過“反射”,我們可以將Java這種靜態語⾔附加上動態特性,換句話說,就是反射機制是Java實現動態語言的關鍵,也就是通過反射實現類動態加載。怎么理解呢,就是在運行時加載需要的類,如果運行時不用該類,則不報錯,降低了依賴性。
舉個例子吧
新建一個Java文件,命名為ClassLoad,寫入如下代碼
Scanner scanner = new Scanner(System.in);
System.out.println("請輸入數字");
String key = scanner.next();
switch (key){
case "1":
System.out.println("我等於1");
case "2":
Class<?> cls = Class.forName("Person");
Object o = cls.newInstance();
Method m = cls.getMethod("hi");
m.invoke(o);
System.out.println("ok!");
break;
}
這里,我沒有寫Person類,但是程序編譯的時候是不會報錯的。也就是說,等到程序執行到case "2"
,里面時才會發生報錯,也就是上文中提到的在運行時加載需要的類,如果運行時不用該類,則不報錯,這就是動態加載。我們現在來運行看一眼。先輸入1程序正常,然后輸入2報錯。
現在是不是理解了動態加載了呢。
類加載
可能還有一些同學想要了解,比如,類加載過程到底是怎么樣的呢?其實類加載大體分為三個階段(加載階段(Loading)、鏈接階段(驗證、准備、解析)、初始化階段(initalization)),這里畫一張圖來便於理解。
具體的這個內容咱們后續再說,現在這里就不做探討了。
參考
Java安全漫談 - 01.反射篇