再談類載入的父親委託(Parent
Delegation)機制
在父親委託機制中,各個載入器按照父子關係形成了樹狀結構,除了根類載入器以外,其餘的類載入器有且只有一個父載入器。
假設loader2的父親為loader1,loader1的父親為系統類別載入器。假設Java程式要求loader2載入Sample類,代碼如下:
Class sampleClass = loader2.loadClass("Sample");
loader2首先從自己的命名空間中尋找Sample類是否已經載入,如果已經載入,就直接返回代表Sample類的Class對象的引用。
如果Sample類還沒有被載入,loader2首先請求loader1代為載入,loader1再請求系統類別載入器代為載入,系統類別載入器再請求擴充類載入器代為載入,擴充類載入器再請求根類載入器代為載入。若根類載入器和擴充類載入器都不能載入,則系統類別載入器嘗試載入,若能載入成功,則將Sample類所對應的Class對象的引用返回給loader1,loader1再將引用返回給loader2,從而成功將Sample類載入進虛擬機器。
若系統類別載入器不能載入Smaple類,則loader1嘗試載入Sample類,若loader1也不能成功載入,則loader2嘗試載入。若所有的父載入器和loader2本身都不能載入,則拋出ClassNotFoundException異常。
父親委託機制的優點?
父親委託機制的優點是能夠提高軟體系統的安全性。因為在此機制下,使用者自訂的類載入器不可能載入應該由父載入器載入的可靠類,從而防止不可靠甚至惡意的代碼代替由父載入器載入的可靠代碼。
例如,java.lang.Object類總是由根類載入器載入(BootStrap),其他任何使用者自訂的類載入器都不可能載入含有惡意代碼的。
java.lang.Object類。
有幾個重要概念需要理解一下。(與安全有關)
1.命名空間
每個類載入器都有自己的命名空間,命名空間由載入器及所有父載入器所載入的類組成。
在同一個命名空間中,不會出現類的完整名字(包括類的包名)相同的兩個類;在不同的命名空間中,
有可能會出現類的完整名字(包括類的包名)相同的兩個類。
2.運行時包
由同一個類載入器載入的屬於相同包的類組成運行時包。決定兩個類是不是屬於同一個運行時包,不僅
要看它們的包名是否相同,還要看定義類載入器是否相同。只有屬於同一運行時包的類才能互相訪問可見(即預設存取層級)
的類和類成員。這樣的限制能避免使用者自訂的類冒充核心類庫的類,去訪問核心類庫的包可見成員。假設使用者自己定義了一個類
java.lang.Spy,並由使用者自訂的類載入器載入,由於jang.lang.Spy和核心類庫java.lang.*由不同的載入器載入,它們屬於不同的運行
時包,所以java.lang.Spy不能訪問核心類庫java.lang包中的可見成員。
建立使用者自訂的類載入器
要建立使用者自己的類載入器,只需擴充java.lang.ClassLoader類,然後覆蓋它的findClass(String
name)即可,該方法根據參數指定的類的名字,返回對應的Class對象的引用。
package com.jfans;
import java.io.ByteArrayOutputStream;
import java.io.File;
import
java.io.FileInputStream;
import java.io.IOException;
public class MyClassLoader extends ClassLoader
{
//給類載入器指定一個名字,在本例中只是為了便於區分不同的載入器對象
private String
name;
private String path = "d://";
private final String fileType =
".class";
public MyClassLoader(String name)
{
super();
this.name = name;
}
public MyClassLoader(ClassLoader parent,String name)
{
super(parent);
this.name = name;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path =
path;
}
@Override
public String toString(){
return
this.name;
}
//自訂私人方法
private byte[] loadClassData(String
name) throws ClassNotFoundException{
FileInputStream fis = null;
byte[] data = null;
ByteArrayOutputStream baos = null;
try{
//把name字串中的"."替換為"/",從而把類中的包名轉變為路徑名
//例如,如果name為com.jfans.Sample,那麼將轉變為"com/jfans/Sample"
name =
name.replaceAll("//.","////");
fis = new FileInputStream(new File(path + name + fileType));
baos
= new ByteArrayOutputStream();
int ch = 0;
while(
(ch=fis.read()) != -1){
baos.write(ch);
}
data = baos.toByteArray();
}catch(IOException
e){
//System.out.println("exception e: " + e);
//異常轉換
throw new ClassNotFoundException("class is not found:"+name,e);
}finally{
try{
if(fis != null){
fis.close();}
if(baos != null){
baos.close();}
}catch(IOException ioe){
ioe.printStackTrace();
}
}
return data;
}
//自訂載入器要重寫的方法
protected Class
findClass(String name) throws ClassNotFoundException{
byte[] data =
loadClassData(name);
return defineClass(name,data, 0,
data.length);
}
}
測試類別:
package com.jfans;
public class MyClassLoaderTest {
public static void main(String[] args) {
MyClassLoader loader1 = new
MyClassLoader("loader1");
loader1.setPath("D:/myapp/serverlib/");
MyClassLoader
loader2 = new
MyClassLoader(loader1,"loader2");
loader2.setPath("D:/myapp/clientlib/");
MyClassLoader
loader3 = new
MyClassLoader(null,"loader3");
loader3.setPath("D:/myapp/otherlib/");
test(loader1);
test(loader2);
test(loader3);
}
private
static void test(ClassLoader classLoader){
try {
Class personClass =
classLoader.loadClass("com.jfans.Person");
Object obj =
personClass.newInstance();
} catch (ClassNotFoundException e) {
//
TODO Auto-generated catch block
e.printStackTrace();
} catch
(InstantiationException e) {
// TODO Auto-generated catch
block
e.printStackTrace();
} catch (IllegalAccessException e)
{
// TODO Auto-generated catch
block
e.printStackTrace();
}
}
}
//Person類
package com.jfans;
public class Person {
private String name;
public Person(){
this.name = "cuser";//default
value
System.err.println("Person is load by: " +
this.getClass().getClassLoader());//由此觀察列印結果
}
public
Personx(String name){
this.name = name;
}
public String getName()
{
return name;
}
public void setName(String name) {
this.name = name;
}
}
測試方法:可以將Person類分別放到上面三個載入器(loader1,loader2,loader3所指定的目錄(path)下面。通過不同的組合,刪除等,進行測試。相信能觀察到Person到底由哪個載入器載入的,同時什麼情況下會出現ClassNotFoundException。
測試前,閱讀代碼,記住父親委託機制就可以了。
不同類載入器的命名空間存在以下關係:
●同一個命名空間內的類是相互可見的
●子載入器的命名空間包含所有父載入器的命名空間。因此子載入器載入的類能夠看見父載入器載入的類。例如系統類別載入器載入的類能夠看見根類載入器載入的類。
●由父載入器載入的類不能看見子載入器載入的類。
●如果兩個載入器之間沒有直接或間接的父子關係,那麼它們各自載入的類相互不可見。
所謂類A能看見類B,就是批類A的程式碼中可以引用類B的名字。例如:
class
A{
B b = new B();
}
URLClassLoader類
在JDK的java.net包中,提供了一個功能比較強大的URLClassLoader類,它擴充了ClassLoader類。它不僅能從本地檔案系統中載入類,還可以從網上下載類。Java程式可直接用URLClassLoader類作為使用者自訂的類載入器。URLClassLoader類提供了以下形式的構造方法:
URL(URL[]
urls) //父載入器為系統類別載入器
URL(URL[] urls,ClassLoader parent)
//parent參數指定父載入器
以上構造方法中的參數urls用來存放所有的URL路徑,URLClassLoader將從這些路徑中載入類。
類的卸載
當Sample類被載入、串連和初始化後,它的生命週期就開始了。當代表Sample類的Class對象不再被引用,即不可觸及時,Class對象就會結束生命週期,Sample類在方法區內的資料也會被卸載,從而結束Sample類的生命週期。由此可見,一個類何時結束生命週期,取決於代表它的Class對象何時結束生命週期。這是一個較大的話題。
由Java虛擬機器內建的類載入器所載入的類,在虛擬機器的生命週期內,始終不會被卸載。前面已經介紹過,Java虛擬機器內建的類載入器包括根類載入器,擴充類載入器,系統類別載入器,Java虛擬機器本身會始終引用這些類載入空對空,而這些類載入器則會始終引用它們所載入的類的Class對象,因此這些Class對象是始終可觸及的。