上節(jié),我們探討了動態(tài)代理,在前幾節(jié)中,我們多次提到了類加載器ClassLoader,本節(jié)就來詳細討論Java中的類加載機制與ClassLoader。

類加載器ClassLoader就是加載其他類的類,它負責將字節(jié)碼文件加載到內存,創(chuàng)建Class對象。與之前介紹的反射、注解、和動態(tài)代理一樣,在大部分的應用編程中,我們不太需要自己實現ClassLoader。

不過,理解類加載的機制和過程,有助于我們更好的理解之前介紹的內容,更好的理解Java。在反射一節(jié),我們介紹過Class的靜態(tài)方法Class.forName,理解類加載器有助于我們更好的理解該方法。

ClassLoader一般是系統提供的,不需要自己實現,不過,通過創(chuàng)建自定義的ClassLoader,可以實現一些強大靈活的功能,比如:

  • 熱部署,在不重啟Java程序的情況下,動態(tài)替換類的實現,比如Java Web開發(fā)中的JSP技術就利用自定義的ClassLoader實現修改JSP代碼即生效,OSGI (Open Service Gateway Initiative)框架使用自定義ClassLoader實現動態(tài)更新。

  • 應用的模塊化和相互隔離,不同的ClassLoader可以加載相同的類但互相隔離、互不影響。Web應用服務器如Tomcat利用這一點在一個程序中管理多個Web應用程序,每個Web應用使用自己的ClassLoader,這些Web應用互不干擾。OSGI利用這一點實現了一個動態(tài)模塊化架構,每個模塊有自己的ClassLoader,不同模塊可以互不干擾。

  • 從不同地方靈活加載,系統默認的ClassLoader一般從本地的.class文件或jar文件中加載字節(jié)碼文件,通過自定義的ClassLoader,我們可以從共享的Web服務器、數據庫、緩存服務器等其他地方加載字節(jié)碼文件。

理解自定義ClassLoader有助于我們理解這些系統程序和框架,如Tomat, JSP, OSGI,在業(yè)務需要的時候,也可以借助自定義ClassLoader實現動態(tài)靈活的功能。

下面,我們首先來進一步理解Java加載類的過程,理解類ClassLoader和Class.forName,介紹一個簡單的應用,然后我們探討如何實現自定義ClassLoader,演示如何利用它實現熱部署。

類加載的基本機制和過程

運行Java程序,就是執(zhí)行java這個命令,指定包含main方法的完整類名,以及一個classpath,即類路徑。類路徑可以有多個,對于直接的class文件,路徑是class文件的根目錄,對于jar包,路徑是jar包的完整名稱(包括路徑和jar包名)。

Java運行時,會根據類的完全限定名尋找并加載類,尋找的方式基本就是在系統類和指定的類路徑中尋找,如果是class文件的根目錄,則直接查看是否有對應的子目錄及文件,如果是jar文件,則首先在內存中解壓文件,然后再查看是否有對應的類。

負責加載類的類就是類加載器,它的輸入是完全限定的類名,輸出是Class對象。類加載器不是只有一個,一般程序運行時,都會有三個:

  1. 啟動類加載器(Bootstrap ClassLoader):這個加載器是Java虛擬機實現的一部分,不是Java語言實現的,一般是C++實現的,它負責加載Java的基礎類,主要是<JAVA_HOME>/lib/rt.jar,我們日常用的Java類庫比如String, ArrayList等都位于該包內。

  2. 擴展類加載器(Extension ClassLoader):這個加載器的實現類是sun.misc.Launcher$ExtClassLoader,它負責加載Java的一些擴展類,一般是<JAVA_HOME>/lib/ext目錄中的jar包。

  3. 應用程序類加載器(Application ClassLoader):這個加載器的實現類是sun.misc.Launcher$AppClassLoader,它負責加載應用程序的類,包括自己寫的和引入的第三方法類庫,即所有在類路徑中指定的類。

這三個類加載器有一定的關系,可以認為是父子關系,Application ClassLoader的父親是Extension ClassLoader,Extension的父親是Bootstrap ClassLoader,注意不是父子繼承關系,而是父子委派關系,子ClassLoader有一個變量parent指向父ClassLoader,在子ClassLoader加載類時,一般會首先通過父ClassLoader加載,具體來說,在加載一個類時,基本過程是:

  1. 判斷是否已經加載過了,加載過了,直接返回Class對象,一個類只會被一個ClassLoader加載一次。

  2. 如果沒有被加載,先讓父ClassLoader去加載,如果加載成功,返回得到的Class對象。

  3. 在父ClassLoader沒有加載成功的前提下,自己嘗試加載類。

這個過程一般被稱為"雙親委派"模型,即優(yōu)先讓父ClassLoader去加載。為什么要先讓父ClassLoader去加載呢?這樣,可以避免Java類庫被覆蓋的問題,比如用戶程序也定義了一個類java.lang.String,通過雙親委派,java.lang.String只會被Bootstrap ClassLoader加載,避免自定義的String覆蓋Java類庫的定義。

需要了解的是,"雙親委派"雖然是一般模型,但也有一些例外,比如:

  • 自定義的加載順序:盡管不被建議,自定義的ClassLoader可以不遵從"雙親委派"這個約定,不過,即使不遵從,以"java"開頭的類也不能被自定義類加載器加載,這是由Java的安全機制保證的,以避免混亂。

  • 網狀加載順序:在OSGI框架中,類加載器之間的關系是一個網,每個OSGI模塊有一個類加載器,不同模塊之間可能有依賴關系,在一個模塊加載一個類時,可能是從自己模塊加載,也可能是委派給其他模塊的類加載器加載。

  • 父加載器委派給子加載器加載:典型的例子有JNDI服務(Java Naming and Directory Interface),它是Java企業(yè)級應用中的一項服務,具體我們就不介紹了。

一個程序運行時,會創(chuàng)建一個Application ClassLoader,在程序中用到ClassLoader的地方,如果沒有指定,一般用的都是這個ClassLoader,所以,這個ClassLoader也被稱為系統類加載器(System ClassLoader)。

下面,我們來具體看下表示類加載器的類 - ClassLoader。

理解ClassLoader

基本用法

類ClassLoader是一個抽象類,Application ClassLoader和Extension ClassLoader的具體實現類分別是sun.misc.Launcher$AppClassLoader和sun.misc.Launcher$ExtClassLoader,Bootstrap ClassLoader不是由Java實現的,沒有對應的類。

每個Class對象都有一個方法,可以獲取實際加載它的ClassLoader,方法是: 

public ClassLoader getClassLoader()

ClassLoader有一個方法,可以獲取它的父ClassLoader:

public final ClassLoader getParent()

如果ClassLoader是Bootstrap ClassLoader,返回值為null。

比如:

平面設計培訓,網頁設計培訓,美工培訓,游戲開發(fā),動畫培訓

public class ClassLoaderDemo {    public static void main(String[] args) {
        ClassLoader cl = ClassLoaderDemo.class.getClassLoader();        while (cl != null) {
            System.out.println(cl.getClass().getName());
            cl = cl.getParent();
        }
        
        System.out.println(String.class.getClassLoader());
    }
}

平面設計培訓,網頁設計培訓,美工培訓,游戲開發(fā),動畫培訓

輸出為:

sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoadernull

ClassLoader有一個靜態(tài)方法,可以獲取默認的系統類加載器:

public static ClassLoader getSystemClassLoader()

ClassLoader中有一個主要方法,用于加載類:

public Class<?> loadClass(String name) throws ClassNotFoundException

比如:

平面設計培訓,網頁設計培訓,美工培訓,游戲開發(fā),動畫培訓

ClassLoader cl = ClassLoader.getSystemClassLoader();try {
    Class<?> cls = cl.loadClass("java.util.ArrayList");
    ClassLoader actualLoader = cls.getClassLoader();
    System.out.println(actualLoader);
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

平面設計培訓,網頁設計培訓,美工培訓,游戲開發(fā),動畫培訓

需要說明的是,由于委派機制,Class的getClassLoader()方法返回的不一定是調用loadClass的ClassLoader,比如,上面代碼中,java.util.ArrayList實際由BootStrap ClassLoader加載,所以返回值就是null。

ClassLoader vs Class.forName

反射一節(jié),我們介紹過Class的兩個靜態(tài)方法forName:

public static Class<?> forName(String className)public static Class<?> forName(String name, boolean initialize, ClassLoader loader)

第一個方法使用系統類加載器加載,第二個指定ClassLoader,參數initialize表示,加載后,是否執(zhí)行類的初始化代碼(如static語句塊),沒有指定默認為true。

ClassLoader的loadClass方法與forName方法都可以加載類,它們有什么不同呢?基本是一樣的,不過,有一個不同,ClassLoader的loadClass不會執(zhí)行類的初始化代碼,看個例子:

平面設計培訓,網頁設計培訓,美工培訓,游戲開發(fā),動畫培訓

public class CLInitDemo {    public static class Hello {        static {
            System.out.println("hello");
        }
    };    public static void main(String[] args) {
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        String className = CLInitDemo.class.getName() + "$Hello";        try {
            Class<?> cls = cl.loadClass(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

平面設計培訓,網頁設計培訓,美工培訓,游戲開發(fā),動畫培訓

使用ClassLoader加載靜態(tài)內部類Hello,Hello有一個static語句塊,輸出"hello",運行該程序,類被加載了,但沒有任何輸出,即static語句塊沒有被執(zhí)行。如果將loadClass的語句換為:

Class<?> cls = Class.forName(className);

則static語句塊會被執(zhí)行,屏幕將輸出"hello"。

實現代碼

我們來看下ClassLoader的loadClass代碼,以進一步理解其行為:

public Class<?> loadClass(String name) throws ClassNotFoundException {    return loadClass(name, false);
}

它調用了另一個loadClass方法,其主要代碼為(省略了一些代碼,加了注釋,以便于理解):

平面設計培訓,網頁設計培訓,美工培訓,游戲開發(fā),動畫培訓

protected Class<?> loadClass(String name, boolean resolve)        throws ClassNotFoundException {    synchronized (getClassLoadingLock(name)) {        // 首先,檢查類是否已經被加載了
        Class c = findLoadedClass(name);        if (c == null) {            //沒被加載,先委派父ClassLoader或BootStrap ClassLoader去加載
            try {                if (parent != null) {                    //委派父ClassLoader,resolve參數固定為false
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {                //沒找到,捕獲異常,以便嘗試自己加載                            }            if (c == null) {                // 自己去加載,findClass才是當前ClassLoader的真正加載方法
                c = findClass(name);
            }
        }        if (resolve) {            // 鏈接,執(zhí)行static語句塊            resolveClass(c);
        }        return c;
    }
}

平面設計培訓,網頁設計培訓,美工培訓,游戲開發(fā),動畫培訓

參數resolve類似Class.forName中的參數initialize,可以看出,其默認值為false,即使通過自定義ClassLoader重寫loadClass,設置resolve為true,它調用父ClassLoader的時候,傳遞的也是固定的false。

findClass是一個protected方法,類ClassLoader的默認實現就是拋出ClassNotFoundException,子類應該重寫該方法,實現自己的加載邏輯,后文我們會看個具體例子。

類加載應用 - 可配置的策略

可以通過ClassLoader的loadClass或Class.forName自己加載類,但什么情況需要自己加載類呢?

很多應用使用面向接口的編程,接口具體的實現類可能有很多,適用于不同的場合,具體使用哪個實現類在配置文件中配置,通過更改配置,不用改變代碼,就可以改變程序的行為,在設計模式中,這是一種策略模式,我們看個簡單的示例。

定義一個服務接口IService:

public interface IService {    public void action();
}

客戶端通過該接口訪問其方法,怎么獲得IService實例呢?查看配置文件,根據配置的實現類,自己加載,使用反射創(chuàng)建實例對象,示例代碼為:

平面設計培訓,網頁設計培訓,美工培訓,游戲開發(fā),動畫培訓

public class ConfigurableStrategyDemo {    public static IService createService() {        try {
            Properties prop = new Properties();
            String fileName = "data/c87/config.properties";
            prop.load(new FileInputStream(fileName));
            String className = prop.getProperty("service");
            Class<?> cls = Class.forName(className);            return (IService) cls.newInstance();
        } catch (Exception e) {            throw new RuntimeException(e);
        }
    }    public static void main(String[] args) {
        IService service = createService();
        service.action();
    }
}

平面設計培訓,網頁設計培訓,美工培訓,游戲開發(fā),動畫培訓

config.properties的內容示例為:

service=shuo.laoma.dynamic.c87.ServiceB

代碼比較簡單,就不贅述了。

自定義ClassLoader

基本用法

Java類加載機制的強大之處在于,我們可以創(chuàng)建自定義的ClassLoader,自定義ClassLoader是Tomcat實現應用隔離、支持JSP,OSGI實現動態(tài)模塊化的基礎。 

怎么自定義呢?一般而言,繼承類ClassLoader,重寫findClass就可以了。怎么實現findClass呢?使用自己的邏輯尋找class文件字節(jié)碼的字節(jié)形式,找到后,使用如下方法轉換為Class對象:

protected final Class<?> defineClass(String name, byte[] b, int off, int len)

name表示類名,b是存放字節(jié)碼數據的字節(jié)數組,有效數據從off開始,長度為len。

看個例子:

平面設計培訓,網頁設計培訓,美工培訓,游戲開發(fā),動畫培訓

public class MyClassLoader extends ClassLoader {    private static final String BASE_DIR = "data/c87/";

    @Override    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = name.replaceAll("\\.", "/");
        fileName = BASE_DIR + fileName + ".class";        try {            byte[] bytes = BinaryFileUtils.readFileToByteArray(fileName);            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException ex) {            throw new ClassNotFoundException("failed to load class " + name, ex);
        }
    }
}

平面設計培訓,網頁設計培訓,美工培訓,游戲開發(fā),動畫培訓

MyClassLoader從BASE_DIR下的路徑中加載類,它使用了我們在57節(jié)介紹的BinaryFileUtils讀取文件,轉換為byte數組。MyClassLoader沒有指定父ClassLoader,默認是系統類加載器,即ClassLoader.getSystemClassLoader()的返回值,不過,ClassLoader有一個可重寫的構造方法,可以指定父ClassLoader:

protected ClassLoader(ClassLoader parent)

用途

MyClassLoader有什么用呢?將BASE_DIR加到classpath中不就行了,確實可以,這里主要是演示基本用法,實際中,可以從Web服務器、數據庫或緩存服務器獲取bytes數組,這就不是系統類加載器能做到的了。

不過,不把BASE_DIR放到classpath中,而是使用MyClassLoader加載,確實有一個很大的好處,可以創(chuàng)建多個MyClassLoader,對同一個類,每個MyClassLoader都可以加載一次,得到同一個類的不同Class對象,比如:

平面設計培訓,網頁設計培訓,美工培訓,游戲開發(fā),動畫培訓

MyClassLoader cl1 = new MyClassLoader();
String className = "shuo.laoma.dynamic.c87.HelloService";
Class<?> class1 = cl1.loadClass(className);

MyClassLoader cl2 = new MyClassLoader();
Class<?> class2 = cl2.loadClass(className);if (class1 != class2) {
    System.out.println("different classes");
}

http://www.cnblogs.com/swiftma/p/6901301.html