問題概述:每個ClassLoader本身只能分別加載特定位置和目錄中的類,但是,ClassLoader被設計成了一種委托模式,使得某一個ClassLoader可以委托它的父級類裝載器去加載類,從而讓應用程序可以借助某一個子級的ClassLoader去多個位置和目錄中進行類的加載。這就好比“兒子”除了可以花自己的錢,他還可以花“父親”的錢,“父親”又可以花“父親的父親”的錢,所以,最終能通過“兒子”花出去的錢包括他歷代前輩的錢。類裝載器一級級委托到BootStrap類加載器,當BootStrap無法加載當前所要加載的類時,然後才一級級回退到子孫類裝載器去進行真正的加載。當回退到最初的類裝載器時,如果它自己也不能完成類的裝載,那就應報告ClassNotFoundException異常。
現在的問題是,我編寫了一個類裝載器去加載特定目錄中的類,使用java.exe測試這個類加載器時,測試結果完全正常,可以看到委托效果。而我使用ant工具去調用測試程序時,結果就有點問題了,我編寫的類裝載器似乎並沒有委托其父級類加載器去加載類,而總是自己加載。由於本人才學疏淺,且實在沒有精力去研究ant工具的源碼,無法了解其類加載內部細節,現在特針對這個問題,向真正的java高手們請教。為了便於高手們快速了解我的問題所在,也便於一些中手們學習,我寫出了詳細的實驗步驟,對於java新手,建議不要參與討論了,免得我耽誤了您寶貴時間。
1.源程序:MainClass.java
package cn.itcast;
public class MainClass
{
public static void main(String [] args)
{
ClassLoader loader = MainClass.class.getClassLoader();
//打印出當前的類裝載器,及該類裝載器的各級父類裝載器
while(loader != null)
{
System.out.println(loader.getClass().getName());
loader = loader.getParent();
}
//加載AuxiliaryClass類
System.out.println(AuxiliaryClass.class.getName());
}
}
源程序:AuxiliaryClass.java
package cn.itcast;
public class AuxiliaryClass
{}
2.源文件及build結果文件的目錄結構
f:\project
|__src
| |__cn
| |__itcast
| |__MainClass.java
| |__AuxiliaryClass.java
|__build.xml
|__classes
|__cn
|__itcast
|__MainClass.class
|__AuxiliaryClass.class
3.build.xml文件內容
<project name="antloader" default="run">
<property name="classes.dir" value="classes" />
<property name="src.dir" value="src" />
<target name="init">
<mkdir dir="${classes.dir}" />
</target>
<target name="compile" depends="init">
<javac destdir="${classes.dir}" >
<src path="${src.dir}" />
</javac>
</target>
<target name="run" depends="compile">
<java classname="cn.itcast.MyClassLoader">
<classpath>
<pathelement location="${classes.dir}"/>
</classpath>
</java>
</target>
</project>
4.進入project目錄中運行ant,執行結果正常,如下:
org.apache.tools.ant.loader.AntClassLoader2
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
cn.itcast.AuxiliaryClass
5.修改build.xml文件,將最後名稱為“run”的target(執行目標)修改成如下形式,即不設置其中的<classpath>子元素。
<target name="run" depends="compile">
<java classname="cn.itcast.MyClassLoader">
<!--classpath>
<pathelement location="${classes.dir}"/>
</classpath-->
</java>
</target>
再次執行ant,將報告如下錯誤信息:
Could not find cn.itcast.MainClass. Make sure you have it in your classpath
at org.apache.tools.ant.taskdefs.ExecuteJava.execute(ExecuteJava.java:170)
在執行ant的命令行窗口中設置classpath環境變量:
set CLASSPATH=f:\project\classes;
再次執行ant,執行結果正常,如下:
java.net.URLClassLoader
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
cn.itcast.AuxiliaryClass
這個實驗說明CLASSPATH環境變量對ant起了作用,並且在這種情況下,類的加載入器不再是 org.apache.tools.ant.loader.AntClassLoader2,而是java.net.URLClassLoader。
6.修改build.xml文件,讓ant生成的AuxiliaryClass.class文件與MainClass文件位於不同的目錄中,即結果目錄如下:
f:\project
|__src
| |__cn
| |__itcast
| |__MainClass.java
| |__AuxiliaryClass.java
|__build.xml
|__classes
| |__cn
| |__itcast
| |__MainClass.class
|__cn
|__itcast
|__AuxiliaryClass.class
修改後的build.xml文件內容如下:
<project name="antloader" default="run">
<property name="classes.dir" value="classes" />
<property name="src.dir" value="src" />
<property name="mainclass" value="cn.itcast.MainClass" />
<target name="init">
<mkdir dir="${classes.dir}" />
</target>
<target name="compile" depends="init">
<javac destdir="${classes.dir}" >
<src path="${src.dir}" />
<include name="cn/itcast/MainClass.java" />
</javac>
<delete file="${classes.dir}/cn/itcast/AuxiliaryClass.class" />
<javac destdir="." >
<src path="${src.dir}" />
<include name="cn/itcast/AuxiliaryClass.java" />
</javac>
</target>
<target name="run" depends="clean,compile">
<java classname="${mainclass}">
<!--classpath>
<pathelement location="${classes.dir}"/>
</classpath-->
<arg line="${arg0} ${arg1}" />
</java>
</target>
<target name="clean">
<delete dir="${classes.dir}" />
</target>
</project>
因為第一個javac任務編譯MainClass.java時,也會編譯它引用的AuxiliaryClass.java文件,所以,增加了delete任務刪除掉生成的AuxiliaryClass.class文件,然後再使用一個javac任務將AuxiliaryClass.java編譯到另外一個目錄中。java任務中也增加了一個<arg>子元素,用於為java虛擬機傳遞參數,在這一步暫時不需要這個元素,在下一步的實驗中將使用這個元素。
再次執行ant,將報告如下錯誤信息:
Could not find cn.itcast.Auxiliary. Make sure you have it in your classpath
at org.apache.tools.ant.taskdefs.ExecuteJava.execute(ExecuteJava.java:170)
在執行ant的命令行窗口中設置classpath環境變量,將編譯後生成的AuxiliaryClass.class類所在的目錄也加入進CLASSPATH環境變量中:
set CLASSPATH=f:\project\classes;f:\project;
再次執行ant,執行結果正常,如下:
java.net.URLClassLoader
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
cn.itcast.AuxiliaryClass
這個實驗再次說明CLASSPATH環境變量對ant起了作用,將AuxiliaryClass.class放在了classpath環境變量指定的另外一個目錄中,也可以被ant工具的java任務裝載。
7.修改MainClass.java文件,讓其擴展成一個類裝載器,專門負責從一個特定的目錄中去加載類。MainClass同時也作為一個啟動運行類,在其main方法中通過MainClass這個類裝載器加載AuxiliaryClass類。
源程序:MainClass.java
package cn.itcast;
import java.io.*;
public class MainClass extends ClassLoader
{
private String path = null;
public MainClass(String path)
{
//錯誤檢查省略
this.path = path;
}
protected Class findClass(String name) throws ClassNotFoundException
{
try
{
File f = new File(path,name.substring(name.lastIndexOf('.')+1) + ".class");
FileInputStream fis = new FileInputStream(f);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int b = 0;
while((b=fis.read()) != -1)
{
bos.write(b);
}
byte [] buf = bos.toByteArray();
fis.close();
bos.close();
return defineClass(name,buf,0,buf.length);
}catch(Exception e)
{
throw new ClassNotFoundException(name + "is not found!");
}
}
public static void main(String [] args) throws Exception
{
Class cls = new MainClass(args[1]).loadClass(args[0]);
ClassLoader loader = cls.getClassLoader();
//打印出的動態加載的AuxiliaryClass的類裝載器,及該類裝載器的各級父類裝載器
while(loader != null)
{
System.out.println(loader.getClass().getName());
loader = loader.getParent();
}
}
}
按如下方式執行ant命令,其中第一個參數為要加載的類,第二個參數為到哪個目錄中去加載如類。
ant -Darg0=cn.itcast.AuxiliaryClass -Darg1=cn\itcast
命令執行的結果為:
cn.itcast.MainClass
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
從第一行打印的內容上可以看到:AuxiliaryClass類的類裝載器為MainClass。這個結果與我的預期不同,因為按照類加載器的委托機制,MailClass類加載器將先委托其父級類裝載器AppClassLoader加載AuxiliaryClass,而AuxiliaryClass所在的目錄f:\project已經在第6步中加入到了Classpath環境變量當中,AppClassLoader可以成功加載AuxiliaryClass,所以,第一行打印出來的類裝載器應該是AppClassLoader。為了印證我的想法,我改用java.exe來執行上面的程序:
java cn.itcast.MainClass cn.itcast.AuxiliaryClass cn\itcast
執行結果如下:
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
可見,使用java.exe執行上面的程序時,AuxiliaryClass類的類裝載器確實是MailClass類加載器的父級類加載器AppClassLoader。這就是我這次問題的內容:為什麼在ant環境下運行,MailClass類加載器沒有委托其父級類裝載器AppClassLoader加載AuxiliaryClass類,而是自己加載了呢?就這個問題,本人向真正的Java高手們請教?請您幫忙解釋一下原因。
8.為了印證類加載器的委托機制,我們重新設置CLASSPATH環境變量,該環境變量不再包含AuxiliaryClass所在的目錄f:\project。
set CLASSPATH=f:\project\classes;
用cd命令進入f:\目錄(避免當前目錄的干擾),接著重復執行如下的java命令:
java cn.itcast.MainClass cn.itcast.AuxiliaryClass project\cn\itcast
執行結果如下:
cn.itcast.MainClass
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
可見,這次AuxiliaryClass類的類裝載器是MailClass類,這是因為MailClass類裝載器的父級類加載器AppClassLoader找不到AuxiliaryClass類,加載過程又退回到MailClass類裝載器,MailClass類裝載器從project\cn\itcast目錄中成功找到AuxiliaryClass類,所以,這次打印出的類裝載器為MailClass。