向高手請教ant構建工具的類裝載器問題
問題概述:每個ClassLoader本身只能分別加載特定位置和目錄中的類,但是,ClassLoader被設計成了一種委托模式,使得某一個ClassLoader可以委托它的父級類裝載器去加載類,從而讓應用程序可以借助某一個子級的ClassLoader去多個位置和目錄中進行類的加載。這就好比“兒子”除了可以花自己的錢,他還可以花“父親”的錢,“父親”又可以花“父親的父親”的錢,所以,最終能通過“兒子”花出去的錢包括他歷代前輩的錢。類裝載器一級級委托到BootStrap類加載器,當BootStrap無法加載當前所要加載的類時,然后才一級級回退到子孫類裝載器去進行真正的加載。當回退到最初的類裝載器時,如果它自己也不能完成類的裝載,那就應報告ClassNotFoundException異常。
現在的問題是,我編寫了一個類裝載器去加載特定目錄中的類,使用java.exe測試這個類加載器時,測試結果完全正常,可以看到委托效果。而我使用ant工具去調用測試程序時,結果就有點問題了,我編寫的類裝載器似乎并沒有委托其父級類加載器去加載類,而總是自己加載。由于本人才學疏淺,且實在沒有精力去研究ant工具的源碼,無法了解其類加載內部細節,現在特針對這個問題,向真正的java高手們請教。為了便于高手們快速了解我的問題所在,也便于一些中手們學習,我寫出了詳細的實驗步驟,對于java新手,建議不要參與討論了,免得我耽誤了您寶貴時間。
1.源程序:MainClass.java
源程序:AuxiliaryClass.java
2.源文件及build結果文件的目錄結構
3.build.xml文件內容
4.進入project目錄中運行ant,執行結果正常,如下:
5.修改build.xml文件,將最后名稱為“run”的target(執行目標)修改成如下形式,即不設置其中的<classpath>子元素。
再次執行ant,將報告如下錯誤信息:
在執行ant的命令行窗口中設置classpath環境變量:
再次執行ant,執行結果正常,如下:
這個實驗說明CLASSPATH環境變量對ant起了作用,并且在這種情況下,類的加載入器不再是 org.apache.tools.ant.loader.AntClassLoader2,而是java.net.URLClassLoader。
6.修改build.xml文件,讓ant生成的AuxiliaryClass.class文件與MainClass文件位于不同的目錄中,即結果目錄如下:
修改后的build.xml文件內容如下:
因為第一個javac任務編譯MainClass.java時,也會編譯它引用的AuxiliaryClass.java文件,所以,增加了delete任務刪除掉生成的AuxiliaryClass.class文件,然后再使用一個javac任務將AuxiliaryClass.java編譯到另外一個目錄中。java任務中也增加了一個<arg>子元素,用于為java虛擬機傳遞參數,在這一步暫時不需要這個元素,在下一步的實驗中將使用這個元素。
再次執行ant,將報告如下錯誤信息:
在執行ant的命令行窗口中設置classpath環境變量,將編譯后生成的AuxiliaryClass.class類所在的目錄也加入進CLASSPATH環境變量中:
再次執行ant,執行結果正常,如下:
這個實驗再次說明CLASSPATH環境變量對ant起了作用,將AuxiliaryClass.class放在了classpath環境變量指定的另外一個目錄中,也可以被ant工具的java任務裝載。
7.修改MainClass.java文件,讓其擴展成一個類裝載器,專門負責從一個特定的目錄中去加載類。MainClass同時也作為一個啟動運行類,在其main方法中通過MainClass這個類裝載器加載AuxiliaryClass類。
源程序:MainClass.java
按如下方式執行ant命令,其中第一個參數為要加載的類,第二個參數為到哪個目錄中去加載如類。
命令執行的結果為:
從第一行打印的內容上可以看到:AuxiliaryClass類的類裝載器為MainClass。這個結果與我的預期不同,因為按照類加載器的委托機制,MailClass類加載器將先委托其父級類裝載器AppClassLoader加載AuxiliaryClass,而AuxiliaryClass所在的目錄f:project已經在第6步中加入到了Classpath環境變量當中,AppClassLoader可以成功加載AuxiliaryClass,所以,第一行打印出來的類裝載器應該是AppClassLoader。為了印證我的想法,我改用java.exe來執行上面的程序:
執行結果如下:
可見,使用java.exe執行上面的程序時,AuxiliaryClass類的類裝載器確實是MailClass類加載器的父級類加載器AppClassLoader。這就是我這次問題的內容:為什么在ant環境下運行,MailClass類加載器沒有委托其父級類裝載器AppClassLoader加載AuxiliaryClass類,而是自己加載了呢?就這個問題,本人向真正的Java高手們請教?請您幫忙解釋一下原因。
8.為了印證類加載器的委托機制,我們重新設置CLASSPATH環境變量,該環境變量不再包含AuxiliaryClass所在的目錄f:project。
set CLASSPATH=f:projectclasses;
用cd命令進入f:目錄(避免當前目錄的干擾),接著重復執行如下的java命令:
java cn.itcast.MainClass cn.itcast.AuxiliaryClass projectcnitcast
執行結果如下:
可見,這次AuxiliaryClass類的類裝載器是MailClass類,這是因為MailClass類裝載器的父級類加載器AppClassLoader找不到AuxiliaryClass類,加載過程又退回到MailClass類裝載器,MailClass類裝載器從projectcnitcast目錄中成功找到AuxiliaryClass類,所以,這次打印出的類裝載器為MailClass。
現在的問題是,我編寫了一個類裝載器去加載特定目錄中的類,使用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:projectclasses; |
再次執行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:projectclasses;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=cnitcast |
命令執行的結果為:
| 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 cnitcast |
執行結果如下:
| 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:projectclasses;
用cd命令進入f:目錄(避免當前目錄的干擾),接著重復執行如下的java命令:
java cn.itcast.MainClass cn.itcast.AuxiliaryClass projectcnitcast
執行結果如下:
| cn.itcast.MainClass sun.misc.Launcher$AppClassLoader sun.misc.Launcher$ExtClassLoader |
可見,這次AuxiliaryClass類的類裝載器是MailClass類,這是因為MailClass類裝載器的父級類加載器AppClassLoader找不到AuxiliaryClass類,加載過程又退回到MailClass類裝載器,MailClass類裝載器從projectcnitcast目錄中成功找到AuxiliaryClass類,所以,這次打印出的類裝載器為MailClass。