java反射機制與類加載機制
Class (Java SE 9 & JDK 9 ) - https://docs.oracle.com/javase/9/docs/api/java/lang/Class.html
Field (Java Platform SE 7 ) - https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/Field.html
ByteArrayOutputStream (Java SE 9 & JDK 9 ) - https://docs.oracle.com/javase/9/docs/api/java/io/ByteArrayOutputStream.html
URL (Java SE 9 & JDK 9 ) - https://docs.oracle.com/javase/9/docs/api/java/net/URL.html
Class熱替換與卸載 - http://www.importnew.com/22462.html
深入探討 Java 類加載器 - https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html
反射機制(Reflection) – http://blog.qiji.tech/archives/4374
深入理解Java類型信息(Class對象)與反射機制 - https://blog.csdn.net/javazejian/article/details/70768369
深入理解Java類加載器(一):Java類加載原理解析 - https://blog.csdn.net/justloveyou_/article/details/72217806
認識Class對象之前,先來了解一個概念扶踊,RTTI(Run-Time Type Identification)運行時類型識別蜕劝,對于這個詞一直是 C++ 中的概念,至于Java中出現(xiàn)RRTI的說法則是源于《Thinking in Java》一書,其作用是在運行時識別一個對象的類型和類的信息,這里分兩種:傳統(tǒng)的 RTTI,它假定我們在編譯期已知道了所有類型遥金,在沒有反射機制創(chuàng)建和使用類對象時,一般都是編譯期已確定其類型蒜田,如new對象時該類必須已定義好稿械。另外一種是反射機制,它允許我們在運行時發(fā)現(xiàn)和使用類型的信息冲粤。在Java中用來表示運行時類型信息的對應類就是Class類美莫,Class類也是一個實實在在的類,存在于JDK的java.lang包中梯捕。
Class類也是類的一種厢呵,但是特別地 Class 類用來描述使用 class 關(guān)鍵字定義的類,這些描述信息用于在運行時獲取類定義內(nèi)容傀顾,Class類的對象作用是運行時提供或獲得某個對象的類型信息襟铭。編寫好的類在編譯后都會產(chǎn)生一個Class對象,表示的是創(chuàng)建的類的類型信息短曾,而且這個Class對象保存在同名.class的字節(jié)碼文件中蝌矛。比如創(chuàng)建一個Shapes類,編譯Shapes類后就會創(chuàng)建其包含Shapes類相關(guān)類型信息的Class對象错英,并保存在Shapes.class字節(jié)碼文件中。每個通過關(guān)鍵字class標識的類隆豹,在內(nèi)存中有且只有一個與之對應的Class對象來描述其類型信息椭岩,無論創(chuàng)建多少個實例對象,其依據(jù)的都是用一個Class對象璃赡。Class類只存私有構(gòu)造函數(shù)判哥,因此對應Class對象只能有JVM創(chuàng)建和加載。
在運行期間碉考,如果我們要產(chǎn)生某個類的對象塌计,Java虛擬機(JVM)會檢查該類型的Class對象是否已被加載。如果沒有被加載侯谁,JVM會根據(jù)類的名稱找到.class文件并加載它锌仅。一旦某個類型的Class對象已被加載到內(nèi)存章钾,就可以用它來產(chǎn)生該類型的所有對象。
通過反射方法實例化對象及修改私有成員為公有成員
import java.lang.reflect.*;
class Gum {
private int weight = 0;
static { System.out.println("Loading Gum"); }
public Gum(){ }
public Gum(int i){ weight = i; }
public String greeting(){ return "Hi!"; }
}
public class coding {
public static void print(Object obj) {
System.out.println(obj);
}
@SuppressWarnings("unchecked")
public static void main(String[] args) {
try {
Class clz = Class.forName("Gum");
// Class clazz = Gum.class;
// Constructor[] cts = clz.getConstructors();
// for ( Constructor c:cts ) {
// print("Contructor: "+c.getName?());
// }
// Constructor constructor = clz.getConstructor();
// Object object = clz.newInstance();
Constructor constructor = clz.getConstructor(int.class);
Object object = constructor.newInstance(0);
Method method = clz.getMethod("greeting");
String msg = (String)method.invoke(object);
print(msg);
Field field = clz.getDeclaredField("weight");
field.setAccessible(true);
print("get private member:"+field.get(object));
Class<?> class1 = Class.forName("Gum");
String name = class1.getClassLoader().getClass().getName();
print("class loader is " + name);
} catch(Exception e) {
e.printStackTrace();
}
}
}
使用類反射方法 Class.forName() 載入類热芹,返回值是對應類的Class對象贱傀,也可以通過對象實例的 getClass() 方法獲取,這是基礎(chǔ)對象類Object定義的方法伊脓,字面常量的方式獲取Class對象如 Object.class府寒。如果沒有定義相應的類構(gòu)造函數(shù),getConstructor() 會引發(fā) NoSuchMethod 異常报腔。
Field.setAccessible(true) 并不是將方法的訪問權(quán)限改成了public株搔,而是取消java的權(quán)限控制檢查。即使是public成員纯蛾,其accessible 屬性默認也是false纤房,即需要進行權(quán)限檢查。
與Java反射相關(guān)的類如下:
Class類 代表類的實體茅撞,在運行的Java應用程序中表示類和接口
Field類 代表類的成員變量(成員變量也稱為類的屬性)
Method類 代表類的方法
Constructor類 代表類的構(gòu)造方法
類加載器
JVM預定義幾種類加載器帆卓,當JVM啟動的時候,Java缺省開始使用如下三種類型的類加載器:
啟動(Bootstrap)類加載器:引導類加載器是用 本地代碼實現(xiàn)的類加載器米丘,它負責將 <JAVA_HOME>/lib下面的核心類庫 或 -Xbootclasspath選項指定的jar包等 虛擬機識別的類庫 加載到內(nèi)存中剑令。由于引導類加載器涉及到虛擬機本地實現(xiàn)細節(jié),開發(fā)者無法直接獲取到啟動類加載器的引用拄查,所以 不允許直接通過引用進行操作吁津。
擴展(Extension)類加載器:擴展類加載器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現(xiàn)的,它負責將 <JAVA_HOME >/lib/ext或者由系統(tǒng)變量-Djava.ext.dir指定位置中的類庫 加載到內(nèi)存中堕扶。開發(fā)者可以直接使用標準擴展類加載器碍脏。
系統(tǒng)(System)類加載器:系統(tǒng)類加載器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現(xiàn)的,它負責將 用戶類路徑(java -classpath或-Djava.class.path變量所指的目錄稍算,即當前類所在路徑及其引用的第三方類庫的路徑典尾,如第四節(jié)中的問題6所述)下的類庫 加載到內(nèi)存中。開發(fā)者可以直接使用系統(tǒng)類加載器糊探,是最常用的加載器钾埂,同時也是java中默認的加載器。通過Java反射機制 Class.getClassLoader() 可以得到類加載器信息科平。
除了以上列舉的三種類加載器褥紫,還有一種比較特殊的類型就是線程上下文類加載器(context class loader),從 JDK 1.2 開始引入的瞪慧。Java.lang.Thread 類中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用來獲取和設置線程的上下文類加載器髓考。如果沒有通過 setContextClassLoader(ClassLoader cl)方法進行設置的話,線程將繼承其父線程的上下文類加載器弃酌。Java 應用運行的初始線程的上下文類加載器是系統(tǒng)類加載器氨菇。在線程中運行的代碼可以通過此類加載器來加載類和資源儡炼。使用線程上下文類加載器,可以在執(zhí)行線程中拋棄雙親委派加載鏈模式门驾,使用線程上下文里的類加載器加載類射赛。典型的例子有:通過線程上下文來加載第三方庫jndi實現(xiàn),而不依賴于雙親委派奶是。大部分Java application服務器(jboss, tomcat..)也是采用contextClassLoader來處理web服務楣责。還有一些采用hot swap特性的框架,也使用了線程上下文類加載器聂沙,比如 seasar (full stack framework in japenese)秆麸。
JVM在加載類時默認采用的是雙親委派機制。通俗的講及汉,就是某個特定的類加載器在接到加載類的請求時沮趣,首先將加載任務委托給父類加載器,依次遞歸 (本質(zhì)上就是loadClass函數(shù)的遞歸調(diào)用)坷随。因此房铭,所有的加載請求最終都應該傳送到頂層的啟動類加載器中。如果父類加載器可以完成這個類加載請求温眉,就成功返回缸匪;只有當父類加載器無法完成此加載請求時,子加載器才會嘗試自己去加載类溢。事實上凌蔬,大多數(shù)情況下,越基礎(chǔ)的類由越上層的加載器進行加載闯冷,因為這些基礎(chǔ)類之所以稱為“基礎(chǔ)”砂心,是因為它們總是作為被用戶代碼調(diào)用的API(當然,也存在基礎(chǔ)類回調(diào)用戶用戶代碼的情形)蛇耀。 系統(tǒng)類加載器的父類加載器是標準擴展類加載器辩诞,標準擴展類加載器的父類加載器是啟動類加載器。
虛擬機出于安全等因素考慮纺涤,不會加載<JAVA_HOME>/lib目錄下存在的陌生類译暂,換句話說,虛擬機只加載<JAVA_HOME>/lib目錄下它可以識別的類洒琢。因此,開發(fā)者通過將要加載的非JDK自身的類放置到此目錄下期待啟動類加載器加載是不可能的褐桌。
在絕大多數(shù)情況下衰抑,系統(tǒng)默認提供的類加載器實現(xiàn)已經(jīng)可以滿足需求。但是在某些情況下荧嵌,您還是需要為應用開發(fā)出自己的類加載器呛踊。比如您的應用通過網(wǎng)絡來傳輸Java類的字節(jié)代碼砾淌,為了保證安全性,這些字節(jié)代碼經(jīng)過了加密處理谭网。這個時候您就需要自己的類加載器來從某個網(wǎng)絡地址上讀取加密后的字節(jié)代碼汪厨,接著進行解密和驗證,最后定義出要在Java虛擬機中運行的類來愉择。下面將通過兩個具體的實例來說明類加載器的開發(fā)劫乱。通過用戶自定義的類裝載器,你的程序可以加載在編譯時并不知道或者尚未存在的類或者接口锥涕,并動態(tài)連接它們并進行有選擇的解析衷戈。
除了和本地實現(xiàn)密切相關(guān)的啟動類加載器之外,包括標準擴展類加載器和系統(tǒng)類加載器在內(nèi)的所有其他類加載器我們都可以當做自定義類加載器來對待层坠,唯一區(qū)別是是否被虛擬機默認使用。前面的內(nèi)容中已經(jīng)對java.lang.ClassLoader抽象類中的幾個重要的方法做了介紹,這里就簡要敘述一下一般 用戶自定義類加載器的工作流程(可以結(jié)合后面問題解答一起看):
1榕堰、首先檢查請求的類型是否已經(jīng)被這個類裝載器裝載到命名空間中了玉控,如果已經(jīng)裝載,直接返回座每;否則轉(zhuǎn)入步驟2前鹅;
2、委派類加載請求給父類加載器(更準確的說應該是雙親類加載器尺栖,真實虛擬機中各種類加載器最終會呈現(xiàn)樹狀結(jié)構(gòu))嫡纠,如果父類加載器能夠完成,則返回父類加載器加載的Class實例延赌;否則轉(zhuǎn)入步驟3除盏;
3、調(diào)用本類加載器的 findClass() 方法挫以,試圖獲取對應的字節(jié)碼者蠕。如果獲取的到,則調(diào)用 defineClass() 導入類型到方法區(qū)掐松;如果獲取不到對應的字節(jié)碼或者其他原因失敗踱侣,向上拋出異常。
在Java中大磺,一個類用其完全匹配類名(fully qualified class name)作為標識抡句,這里指的完全匹配類名包括包名和類名。但在JVM中杠愧,一個類用其全名和一個ClassLoader的實例 作為唯一標識待榔,不同類加載器加載的類將被置于不同的命名空間。
在java.net包下有一個 URLClassLoader 類提供了運程加載類的功能,它讓我們可以通過以下幾種方式進行加載:
* 文件: (從文件系統(tǒng)目錄加載)
* jar包: (從Jar包進行加載)
* Http: (從遠程的Http服務進行加載)
一個class被一個ClassLoader實例加載過的話锐锣,就不能再被這個ClassLoader實例再次加載腌闯,即加載類時調(diào)用了defileClass()方法,重新加載字節(jié)碼雕憔、解析姿骏、驗證后就不能重復執(zhí)行。系統(tǒng)默認的AppClassLoader加載器內(nèi)部會緩存加載過的class斤彼,重新加載的話分瘦,就直接取緩存。所以需要熱加載只能重新創(chuàng)建一個ClassLoader畅卓,然后再去加載已經(jīng)被加載過的class文件擅腰。實現(xiàn)熱加載需要重載 ClassLoader 的 loadClass() 方法,跳過內(nèi)部的雙親委托機制翁潘。即使不采用雙親委托機制趁冈,比如java.lang包中的相關(guān)類還是不能自定義一個同名的類來代替,主要因為JVM解析拜马、驗證class的 時候渗勘,會進行相關(guān)判斷。強制重復加載引發(fā) java.lang.LinkageError 異常:
Exception in thread "main" java.lang.LinkageError: loader (instance of NetworkClassLoader): attempted duplicate class definition
JVM中class和Meta信息存放在PermGen space區(qū)域俩莽。如果加載的class文件很多旺坠,那么可能導致PermGen space區(qū)域空間溢出。引起:java.lang.OutOfMemoryErrorPermGen space. 對于有些Class我們可能只需要使用一次扮超,就不再需要了取刃,JVM中的Class只有滿足以下三個條件,才能被GC回收出刷,也就是該Class被卸載(unload):
該類所有的實例都已經(jīng)被GC璧疗。
加載該類的ClassLoader實例已經(jīng)被GC。
該類的java.lang.Class對象沒有在任何地方被引用馁龟。
System.gc()執(zhí)行后的Class的卸載是不可控的崩侠。
自定義類加載器--遠程類加載器
反射機制的應用必須要求該類是public訪問權(quán)限的類,這樣才能由加載器加載坷檩。要實現(xiàn)HotSwap熱加載却音,只需要創(chuàng)建新實例去加載目標類就可以,熱加載邏輯可以根據(jù)文件更新時間來做判斷矢炼。
import java.lang.reflect.*;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.net.URL;
import java.net.HttpURLConnection;
/**
* NetworkClassLoader ncl = new NetworkClassLoader("http://xxx.com");
* String basicClassName = "Gum";
* Class<?> clazz = ncl.loadClass(basicClassName);
* Gum oo = (Gum)clazz.newInstance();
*/
class NetworkClassLoader extends ClassLoader {
private String rootUrl;
public NetworkClassLoader(String rootUrl) {
this.rootUrl = rootUrl;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = rootUrl + "/" + className.replace('.', '/') + ".class";
try {
URL url = new URL(path);
// HttpURLConnection con = (HttpURLConnection)url.openConnection();
// System.out.println(con.getResponseCode?() +" "+ con.getContentType());
// BufferedReader buffer = new BufferedReader(new InputStreamReader(ins));
// StringBuffer bs = new StringBuffer();
// String line = null;
// while((line=buffer.readLine())!=null){
// bs.append(line);
// }
// System.out.println("getClassData:"+bs.length()+":"+bs.substring(0));
// return bs.toString().getBytes();
InputStream ins = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int len = 0;
while ((len = ins.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
System.out.println("getClassData:"+len+":");
return baos.toByteArray();
} catch (Exception e) {
System.out.println("getClassData exception:");
e.printStackTrace();
}
System.out.println("getClassData null:");
return null;
}
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class c = findLoadedClass(name);
if( name.startsWith("java.") ){
ClassLoader system = ClassLoader.getSystemClassLoader();
c = system.loadClass(name);
}
if (null==c) c = findClass(name);
if( resolve ) resolveClass(c);
return c;
}
}