top
Loading...
向高手請教ant構建工具的類裝載器問題
問題概述:每個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: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。
作者:http://www.zhujiangroad.com
來源:http://www.zhujiangroad.com
北斗有巢氏 有巢氏北斗