Java安全第一篇 | 反射看這一篇就夠了

雪痕*發表於2022-03-21

什麼是反射?

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);

執行一下,發現成功讀取到內容。

image-20220318225128457

然後需要建立物件,怎麼建立物件呢?有同學就說了,我們們可以直接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())輸出結果如下:

image-20220319131058490

第三步、通過cls得到你載入的類 com.lxflxf.Cat 的 methodName 的方法物件,我們可以在反射中,把方法視為物件。

Method method1 = cls.getMethod(methodName);

最後、通過method1呼叫方法、也就是通過方法物件來實現呼叫方法

method1.invoke(o);

在這裡我們也能發現反射和傳統方法的區別了,傳統方法是物件.方法(),反射中呢,是方法.invoke(物件)。那我們執行一下,看看能否輸出方法裡的內容呢,如下:

image-20220319132511331

說到這裡大家腦海裡應該也有了反射的概念。其實反射機制還有一個優點,那就是可以通過外部檔案配置,在不修改原始碼的情況下,來控制程式。比如這裡,我在Cat類下面再寫一個方法,cry()方法,程式碼如下:

public void cry(){
    System.out.println(name + "......喵喵喵");
}

如果我們使用傳統方法,要呼叫這個方法,是不是就要修改程式碼了,比如cat.cry();這樣的,那通過反射,我們只需要修改配置檔案就可以了,在配置檔案re.properties中,將method=hi改為method=cry,就可以了。

image-20220319133834353

執行,發現成功呼叫並輸出了內容,實現了改配置檔案,不改程式碼,完成了解藕。

image-20220319134051229

反射機制

上文中,通過一個小案例來簡單的瞭解了一下反射,現在來系統的說一下。反射機制允許程式在執行期藉助於ReflectionAPI取得任何類的內部資訊(比如成員變數、構造器、成員方法等等),並能操作物件的屬性及方法。載入完類後,在堆中就產生了一個Class型別的物件(一個類只有一個Class物件),這個物件包含了類的完整結構資訊。通過這個物件得到類的結構。為了便於理解,在這裡為大家畫一下Java反射機制原理示意圖。如下:

image-20220319212124846

然後現在做一個小小的總結,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));

發現成功拿到了成員變數的值

image-20220319145844821

Class類分析

接下來對Class類特點進行一下梳理。先看看Class類圖

image-20220319210337054

我們發現它的父類仍然是Object。

然後第二點是,Class類物件不是new出來的,而是系統建立的。這裡怎麼理解呢,還記得上面我們們畫的原理圖嗎?Class類是由loadClass()方法完成類載入,生成了某個類對應的Class類物件。現在為大家演示一下。寫如下案例程式碼:

Class<?> aClass = Class.forName("com.lxflxf.Cat");

然後這這句程式碼的前面下一個斷點,進行除錯。成功進入ClassLoader類中,到了loadClass()方法。如下:

image-20220319213127099

接下來說第三點,對於某個類的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());

執行結果如下圖,值相同

image-20220319214519605

最後關於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執行型別,如下圖:

image-20220319221830203

如果我想要得到包名,可以通過getPackageName()方法,可以通過System.out.println(cls.getPackageName()),輸出內容為com.lxflxf。如果想得到類名,可以通過getName()方法。還有一個很重要的方法,那就是建立物件例項:newInstance(),案例如:Object o = cls.newInstance();,這裡也需要注意一點,在JDK1.9往上,不再使用newInstance()。還可以通過getField()獲取到屬性。還有一寫其他方法,這裡就不一一舉例了。列了一個表格,如下:

image-20220320130749936

前面說了這麼多,那哪些型別有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);

輸出結果如下:

image-20220320164739459

動態載入

在文章最開始,就說了一下,通過“反射”,我們可以將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報錯。

image-20220320195802058

image-20220320195940551

現在是不是理解了動態載入了呢。

類載入

可能還有一些同學想要了解,比如,類載入過程到底是怎麼樣的呢?其實類載入大體分為三個階段(載入階段(Loading)、連結階段(驗證、準備、解析)、初始化階段(initalization)),這裡畫一張圖來便於理解。

image-20220320225149602

具體的這個內容我們們後續再說,現在這裡就不做探討了。

參考

Java安全漫談 - 01.反射篇

相關文章