開發與維運

Java反射原理以及一些常見的應用

本文由內容志願者整理阿里雲社群直播而來。

講師:關鍵

目錄
image.png

一、類裝載

我們都知道,java在編譯類後並不是產生固有機器的機器碼,而是一段字節碼,這段字節碼可以存放於任何地方,如.class文件,jar包中,可以通過網絡傳輸。JVM虛擬機在拿取到這段二進制數據流字節碼後,就會處理這些數據,並最終轉換成一個java.lang.Class的實例(注意,這裡並不是類本身的實例)。java.lang.Class實例是訪問類型元數據的接口,也是實現反射的關鍵數據。通過Class類提供的接口,可以訪問一個類型的方法,字段等信息。
二進制數據流字節碼被加載到虛擬機之後,會進行一系列的驗證檢查,主要步驟如下:

1.格式檢查:包括魔數檢查(識別文件類型開頭的幾個十六進制數,每一種文件類型都不同),版本檢查,長度檢查
2.語義檢查:是否繼承final,是否有父類,抽象方法是否有實現
3.字節碼驗證:跳轉指令是否指向正確位置,操作數類型是否合理
4.符號引用驗證:符號引用的直接引用是否存在

當一個類驗證通過時,虛擬機就會進入準備階段。分配內存空間,分配初始值。如果類存在常量字段,如果被final修飾,就會被直接放入常量池中。如果沒有final修飾,就會在初始化中賦值,而不是直接放入常量池。

準備階段完成後,就是解析類,解析類就是把字節碼中的類,接口,字段,方法放入JVM虛擬機實際內存的地址中,方便程序可以真正執行,比如說類的方法會有一個方法表,當需要調用一個類的方法時,就要知道這個方法在方法表中的偏移量,直接調用該方法。

類的初始化是類裝載的最後一個階段。初始化的重要工作就是執行類的初始化方法。方法是由編譯器自動生成的,它是由類靜態成員的賦值語句以及static語句合併產生的。類似代碼如下

image.png

反射代碼示例如下:
image.png
image.png

運行結果:
public equals (Object)
public toString ()
public hashCode ()
public compareTo (String)
public volatile compareTo (Object)
public indexOf (String,int)
public indexOf (String)
public indexOf (int,int)
public indexOf (int)
static indexOf (char[],int,int,char[],int,int,int)
static indexOf (char[],int,int,String,int)
public static valueOf (int)
public static valueOf (long)
public static valueOf (float)
public static valueOf (boolean)
public static valueOf (char[])
public static valueOf (char[],int,int)
public static valueOf (Object)
public static valueOf (char)
public static valueOf (double)
public charAt (int)
private static checkBounds (byte[],int,int)
public codePointAt (int)
public codePointBefore (int)
public codePointCount (int,int)
public compareToIgnoreCase (String)
public concat (String)
public contains (CharSequence)
public contentEquals (CharSequence)
public contentEquals (StringBuffer)
public static copyValueOf (char[])
public static copyValueOf (char[],int,int)
public endsWith (String)
public equalsIgnoreCase (String)
public static transient format (Locale,String,Object[])
public static transient format (String,Object[])
public getBytes (int,int,byte[],int)
public getBytes (Charset)
public getBytes (String)
public getBytes ()
public getChars (int,int,char[],int)
 getChars (char[],int)
private indexOfSupplementary (int,int)
public native intern ()
public isEmpty ()
public static transient join (CharSequence,CharSequence[])
public static join (CharSequence,Iterable)
public lastIndexOf (int)
public lastIndexOf (String)
static lastIndexOf (char[],int,int,String,int)
public lastIndexOf (String,int)
public lastIndexOf (int,int)
static lastIndexOf (char[],int,int,char[],int,int,int)
private lastIndexOfSupplementary (int,int)
public length ()
public matches (String)
private nonSyncContentEquals (AbstractStringBuilder)
public offsetByCodePoints (int,int)
public regionMatches (int,String,int,int)
public regionMatches (boolean,int,String,int,int)
public replace (char,char)
public replace (CharSequence,CharSequence)
public replaceAll (String,String)
public replaceFirst (String,String)
public split (String)
public split (String,int)
public startsWith (String,int)
public startsWith (String)
public subSequence (int,int)
public substring (int)
public substring (int,int)
public toCharArray ()
public toLowerCase (Locale)
public toLowerCase ()
public toUpperCase ()
public toUpperCase (Locale)
public trim ()

Class.forName可以得到代表String類的Class實例,它是反射中最重要的方法。

二、反射原理

反射離不開Class.forName(),我們先從Class.forName說起。
反射可以跟new一個對象有相同的效果。例如:
image.png
image.png
image.png

運行結果:
Company{a='A', b='B'}

又可以寫成如下
image.png

運行結果:
Company{a='A', b='B'}

new VS newInstance
雖然效果一樣,但他們的過程並不一樣。 首先,newInstance( )是一個方法,而new是一個關鍵字;其次,Class下的newInstance()的使用有侷限,因為它生成對象只能調用無參的構造函數,而使用 new關鍵字生成對象沒有這個限制。

newInstance()的時候是使用的上篇說的類裝載機制的,它會走完全部過程。具體可以看 淺析類裝載 ,而new一個實例的時候,走的流程不太一樣,它會先在JVM內部先去尋找該類的Class實例,然後依照該Class實例的定義,依葫蘆畫瓢,把該類的實例給生成出來。但如果找不到該類的Class實例,則會走上篇說的裝載流程。 其中JDK的Class實例一般是在jvm啟動時用啟動類加載器完成加載,用戶的Class實例則是在用到的時候再加載。

Class.forName()
Class.forName()被重載為有一個參數和三個參數的,我們來看一下其源碼。
image.png

無論是三參還是單參的都調用了此方法:
image.png

它的第二個參數boolean initialize表示是否要初始化該類,單參Class.forName()默認true是要初始化的,三參的Class.forName()由你自己選擇。一旦初始化,就會觸發目標對象的 static塊代碼執行,static參數也也會被再次初始化。當然如果你使用了三個參數的Class.forName(),並調用了newInstance()以後,是肯定會初始化的。

ClassLoader,這個才是真正裝載類的核心組件。所有的Class都是由ClassLoader進行加載的,ClassLoader負責通過各種方式將Class信息的二進制字節碼數據流讀入系統,然後交給JVM虛擬機進行連接、初始化等操作。

ClassLoader的分類
在標準的Java程序中,Java虛擬機會創建3類ClassLoader為整個應用程序服務。它們分別是:BootStrap ClassLoader(啟動類加載器),Extension ClassLoader(擴展類加載器),App ClassLoader(應用類加載器,也稱為系統類加載器)。

此外,每一個應用程序還可以擁有自定義的ClassLoader,擴展Java虛擬機獲取Class數據的能力。其中,應用類加載器的雙親為擴展類加載器,擴展類加載器的雙親為啟動類加載器。

當系統需要使用一個類時,在判斷類是否已經被加載時,會先從當前底層類加載器進行判斷。當系統需要加載一個類時,會從頂層類開始加載,依次向下嘗試,直到成功。

三、反射使用場景

1、編碼階段不知道需要實例化的類名是哪個,需要在runtime從配置文件中加載:
Class clazz = class.forName("xxx.xxx.xxx")
clazz.newInstance();

2、在runtime階段,需要臨時訪問類的某個私有屬性
ClassA objA = new ClassA();
Field xxx = objA.getClass().getDeclaredField("xxx")
xxx.setAccessible(true);

3、當使用標籤的時候,我們要獲取標籤

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotionTest {

String value() default "哈士奇";

}

@AnnotionTest
public class Dog {

private String type;
private String name;
public Dog() {
    type = "金毛";
    name = "大黃";
}

public Dog(String type,String name){

    this.type = type;
    this.name = name;

}

// toString()省略
}

public class CheckDog {

public static void main(String[] args) throws Exception {
    Class clazz = Class.forName("com.guanjian.Dog");
    if (clazz.isAnnotationPresent(AnnotionTest.class)) {
        Field field = clazz.getDeclaredField("type");
        field.setAccessible(true);
        System.out.println(field.get(clazz.newInstance()));
        AnnotionTest test = (AnnotionTest)clazz.getAnnotation(AnnotionTest.class);
        System.out.println(test.value());
    }
}

}

4、獲取具體的構造器來構造類本身的實例
Class<?>[] constructorParams = {String.class,String.class};

    Constructor<?> cons = clazz.getConstructor(constructorParams);
    System.out.println(cons.newInstance("哈士奇","神哈"));

5、其他(可以用到的地方還有很多,比如獲取父類的方法,判斷是不是一個接口等等)

四、AOP

AOP主要是通過動態代理實現的,動態代理分兩種,一種是JDK的,一種是CGlib。JDK的動態代理必須有一個接口。
image.png

還有一個實現類,是實現了它的主要功能。
image.png

image.png

main方法
image.png

運行結果:
Before
Hello! Jack
After

動態代理是通過反射來實現的,這裡有一個Proxy.newProxyInstance,我們來看一下它的源代碼。
image.png
image.png
image.png
image.png
image.png
image.png
image.png

由以上代碼可知,該動態代理必須要有接口才能代理,而不能代理沒有接口的類。
沒有接口的類,我們可以使用CGLib來動態代理
要使用CGLib,先要在pom中加入它的引用
image.png
image.png
image.png
image.png
image.png
main方法
image.png

我們來簡單看一下Enhancer.create的源碼
image.png

瞭解了動態代理之後,我們來了解一下Spring+AspectJ的AOP。我們主要結合攔截指定註解(@annotation)的方式來大概說明一下用法。
比如說我們要攔截一個自定義的標籤,對標有該標籤的方法,記錄日誌,通過MQ(具體不限定哪種MQ)發送到日誌中心,進行存儲。
首先我們有一個日誌的實體類。
image.png
image.png

有一個自定義的標籤,這是一個方法級標籤。
image.png
image.png

增加日誌所屬的模塊(用戶操作)。
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png

然後我們定義一個AOP的實現類。
image.png
image.png
image.png
image.png
image.png
image.png

因為@Aspect標籤是未被Spring託管的,所以要對LogAop實行自動配置,在資源文件夾下的spring.factories中
image.png

有多條以, 分隔成多行。在一些自己開發的jar包導入新的Spring boot工程中,此種方法是最為有效的依賴注入方式。

比如在用戶模塊要修改個人信息的時候
image.png
image.png

這個updateMe方法就會被AOP環繞增強,將操作的信息,傳遞參數放入log對象,並通過MQ發送到日誌中心進行接收。其實這裡面接入點的具體實現也是通過反射來處理的。而AOP的整個實現也是通過動態代理來實現的。當然不要忘了在本模塊內的配置文件中增加。
image.png

它的默認值是false,默認只能代理接口(使用JDK動態代理),當為true時,才能代理目標類(使用CGLib動態代理)。

社區內容志願者招募中,戳我瞭解詳情,社區大獎等你來!

Leave a Reply

Your email address will not be published. Required fields are marked *