1. 程式人生 > >Java類載入器( CLassLoader ) 死磕5: 自定義一個檔案系統的classLoader

Java類載入器( CLassLoader ) 死磕5: 自定義一個檔案系統的classLoader

【正文】Java類載入器(  CLassLoader ) 死磕5: 

自定義一個檔案系統classLoader

本小節目錄


5.1. 自定義類載入器的基本流程
5.2. 入門案例:自定義檔案系統類載入器
5.3. 案例的環境配置
5.4 FileClassLoader 案例實現步驟
5.5. FileClassLoader 的類設計
5.6. FileClassLoader 的原始碼
5.7. FileClassLoader 的使用
5.8. 不同類載入器的名稱空間關係
5.9. 自定義載入器的兩個要點


1.1. 自定義classLoader


不管是Bootstrap ClassLoader還是ExtClassLoader等,這些類載入器都只是載入指定的目錄下的jar包或者資源。

要實現其他的途徑的類載入,比如從D盤某個資料夾載入一個class檔案,或者從網路上下載class主內容然後再進行載入,就需要我們自定義一個classloader。 


1.1.1. 自定義類載入器的基本流程


實際上,除了和本地實現密切相關的Bootstrap啟動類載入器之外,包括Extention標準擴充套件類載入器和AppClassLoader應用類載入器在內的所有其他類載入器,都可以當做自定義類載入器來對待。

前面的內容中已經對java.lang.ClassLoader抽象類中的loadClass方法做了介紹,在此方法中,如果所有的上層類載入器都沒有載入成功,則呼叫本類載入器的findClass()方法,在自己的地盤,獲取對應的位元組碼,並完成位元組碼到類的轉變,並且將class載入到快取中。這個findClass()方法,就是自定義載入器的關鍵。

實現一個自定義載入器,總體來說,分成三步:

(1)在自己的地盤,獲取對應的位元組碼;

(2)並完成位元組碼到類的轉變;

(3)將class載入到快取中;

前面兩步,需要在findClass()方法中完成。

廢話少說,先看一個簡單例項。


1.1.2. 入門案例:自定義檔案系統類載入器


在寵物店的案例中,如果需要裝載第三方的寵物庫,並且第三方寵物庫的類路徑非常靈活。

現在需要設計自定義載入器,按照需要,從第三方庫的載入寵物到記憶體。這裡設計一個自定義載入類FileClassLoader。

在設計FileClassLoader之前,先交代一下第三方的寵物類LittleDog ,為了演示,和之前的Dog類程式碼99%相同的。

LittleDog 的程式碼如下:

package com.crazymakercircle.annoDemo;

.......

public class LittleDog implements IPet{

    //寵物編號

    private static int dogNo;

    protected String name;

    protected int age;

    //無參構造器

    public LittleDog() {

        dogNo++;

        name="LittleDog-"+ dogNo;

        age = RandomUtil.randInMod(20);

    }

    @Tanscation

    public LittleDog sayHello() {

        Logger.info("嗨,大家好!我是" + name);

        return this;

    }

    @Tanscation

    public LittleDog sayAge() {

        Logger.info("我是" + name + ",我的年齡是:" + age);

        return this;

    }

}

至此,一個簡單的第三方類——LittleDog寵物類,已經介紹完畢。

下面看看本案例所涉及到的路徑,和其他的環境配置。


1.1.3. 案例的環境配置


這個類的名字,在System.properties 配置檔案的配置項為:

pet.dog.class=com.crazymakercircle.otherPet.pet.LittleDog

編譯完成之後,存在在一個獨立的路徑中。

這個第三方類庫的路徑,為了可以靈活多變,並且與原始碼工程的輸出路徑相不能相同,也在System.properties 配置檔案增加配置項,具體為:

class.server.path=D:/瘋狂創客圈 死磕java/code/out2/

為這方便讀取,給這個兩個配置項,增加其在對應在SystemConfig 配置類中的常量,具體如下:

package com.crazymakercircle.config;

@ConfigFileAnno(file = "/system.properties")

public class SystemConfig extends ConfigProperties

{

............

    //第三方的類路徑

    @ConfigFieldAnno(proterty = "class.server.path")

    public static String CLASS_SERVER_PATH;

    //寵物狗的型別

    @ConfigFieldAnno(proterty = "pet.dog.class")

    public static String PET_DOG_CLASS;

............

}

編譯完成LittleDog類後,將.class檔案,複製到配置項%class.server.path% 所在的目錄下。

至此,環境配置已經交代完畢。

下面馬上進入正題。


1.1.4. FileClassLoader 案例實現步驟


自定義類載入器,首先要繼承ClassLoader抽象類,並且重寫其findClass()方法。

在重寫的findClass()方法中,完成以下三步:

(1)在自己的地盤(查詢路徑),獲取對應的位元組碼;

(2)並完成位元組碼到Class類物件的轉變;

(3)返回Class類物件。

接下來,findClass()方法返回Class類物件之後,ClassLoader抽象類的程式碼,會將新返回Class類物件載入到快取中,這個工作由抽象類ClassLoader載入器去完成。


1.1.5. FileClassLoader 的類設計


這裡的自定義載入的名稱為——FileClassLoader。

類圖如下:

wpsEAB4.tmp

FileClassLoader的成員屬性rootDir,用來存著自己的查詢路徑。當載入一個類時,如果所有的雙親載入器都沒有載入到,就去rootDir下查詢。

FileClassLoader重寫了的findClass()方法。在這個重寫方法中,首先會呼叫getClassData,在自己的地盤(查詢路徑),獲取對應的位元組碼,返回位元組碼的二進位制陣列。

FileClassLoader增加了的getClassData()方法,這是其自己的私有方法。主要是找到類的二進位制class檔案,然後通過讀取檔案流的方式,讀取檔案的位元組碼,供findClass()重寫方法使用。


1.1.6. FileClassLoader 的原始碼


簡單粗暴,直接上原始碼。

package com.crazymakercircle.classLoader;

import java.io.*;

public class FileClassLoader extends ClassLoader {

    private String rootDir;

    public FileClassLoader(String rootDir) {

        this.rootDir = rootDir;

    }

    public FileClassLoader(ClassLoader parent,String rootDir) {

        super(parent);

        this.rootDir = rootDir;

    }

    @Override

    protected Class<?> findClass(String name)

throws ClassNotFoundException

{

        byte[] classData = getClassData(name);

        if (classData == null) {

            throw new ClassNotFoundException();

        } else {

            return defineClass(name, classData, 0, classData.length);

        }

    }

    protected byte[] getClassData(String className)

 {

        String path = classNameToPath(className);

        try {

            InputStream ins = new FileInputStream(path);

            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            int bufferSize = 4096;

            byte[] buffer = new byte[bufferSize];

            int bytesNumRead = 0;

            while ((bytesNumRead = ins.read(buffer)) != -1) {

                baos.write(buffer, 0, bytesNumRead);

            }

            return baos.toByteArray();

        } catch (IOException e) {

            e.printStackTrace();

        }

        return null;

    }

    protected String classNameToPath(String className) {

        return rootDir + File.separatorChar

                + className.replace('.', File.separatorChar) + ".class";

    }

}


案例路徑:com.crazymakercircle.classLoader.ClassLoader

案例提示:無程式設計不創客、無案例不學習。一定記得看案例哦


上面的findClass()方法中,了呼叫defineClass()方法。這個方法是基類ClassLoader的方法,其作用是,將位元組碼匯入到JVM的方法區記憶體,完成Class類物件載入、驗證、準備、解析四步工作。 這個方法在編寫自定義class loader的時候非常重要,它能將class二進位制內容轉換成Class物件,如果不符合要求的會丟擲各種異常。


總結一下,自定義載入器的步驟為:

(1)編寫一個類繼承自ClassLoader抽象類。 

(2)重寫它的findClass()方法。

(3)在自己的地盤,獲取對應的位元組碼;

(4)呼叫defineClass()方法,將位元組碼載入成Class物件,並且返回。


1.1.7. FileClassLoader 的使用


簡單粗暴,先上程式碼:

public class FileLoaderDemo

{

    public static void useFileLoader() {

        try {

            String baseDir = SystemConfig.CLASS_SERVER_PATH;

            FileClassLoader fileClassLoader = new FileClassLoader(baseDir);

            String className =SystemConfig.PET_DOG_CLASS;

            Class dogClass = fileClassLoader.loadClass(className);

            Logger.info("顯示dogClass的ClassLoader =>");

            ClassLoaderUtil.showLoader4Class(dogClass);

        } catch (ClassNotFoundException e) {

            e.printStackTrace();

        }

    }

    public static void main(String[] args) {

        useFileLoader();

    }

}


上面的例子中,所載入的類為:

pet.dog.class=com.crazymakercircle.otherPet.pet.LittleDog

這個類是提前編譯完成之後,放置在在一個獨立的路徑中,這個路徑為:

class.server.path=D:/瘋狂創客圈 死磕java/code/out2/


需要注意的是,這個路徑不包括在當前工程的類路徑%java.class.path%中。否則,自定義的載入器,是鐵定載入不到的。

為什麼呢?


依據雙親委託機制,包括在當前工程的類路徑%java.class.path%中的類,會優先被AppClassLoader載入。因為自定義載入器的parent,預設就是AppClassLoader。


案例路徑:com.crazymakercircle.classLoaderDemo.base.FileLoaderDemo

案例提示:無程式設計不創客、無案例不學習。一定要跑案例哦


執行的結果是:

  showLoaderTree |>  [email protected]

      showLoaderTree |>  [email protected]

      showLoaderTree |>  [email protected]

1.1.8. 不同類載入器的名稱空間關係


同一個名稱空間內的類是相互可見的。子載入器的名稱空間包含所有父載入器的名稱空間。因此子載入器載入的類能看見父載入器載入的類。例如系統類載入器載入的類能看見根類載入器載入的類。

由父載入器載入的類不能看見子載入器載入的類。

如果兩個載入器之間沒有直接或間接的父子關係,那麼它們各自載入的類相互不可見。當兩個不同名稱空間內的類相互不可見時,可以採用Java的反射機制來訪問例項的屬性和方法。

這裡,需要說明一下 Java 虛擬機器是如何判定兩個 Java 類是相同的。Java 虛擬機器不僅要看類的全名是否相同,還要看載入此類的類載入器是否一樣。只有兩者都相同的情況,才認為兩個類是相同的。

即便是同樣的位元組程式碼,被不同的類載入器載入之後所得到的類,也是不同的。

下面看一段程式碼:

package com.crazymakercircle.classLoaderDemo.base;

public class LoaderedCompare

{

    public static void  showClassSame() {

        try {

            String baseDir = SystemConfig.CLASS_SERVER_PATH;

            FileClassLoader fileClassLoader = new FileClassLoader(baseDir);

            String className =SystemConfig.PET_DOG_CLASS;

            Class dogClass = fileClassLoader.loadClass(className);

            FileClassLoader classLoader2 = new FileClassLoader(baseDir);

            Class dogClass2 = classLoader2.loadClass(className);

            Logger.info("dogClass2.equals(dogClass) => ");

            Logger.info( dogClass2.equals(dogClass));

        } catch (ClassNotFoundException e) {

            e.printStackTrace();

        }

    }

    public static void main(String[] args) {

        showClassSame();

    }

}


案例路徑:com.crazymakercircle.classLoaderDemo.base.LoaderedCompare

案例提示:無程式設計不創客、無案例不學習。一定要跑案例哦


執行的結果是:

  showClassSame |>  dogClass2.equals(dogClass) =>

   showClassSame |>  false



上面的例子中,載入的是同樣的位元組碼檔案。甚至兩個載入都是同一個類,只是是兩個不同的類載入器物件。但是,載入完成之後,在記憶體中的Class物件,是不一樣的。


1.1.9. 自定義載入器的兩個要點


要點一:

如果一個自定義載入器建立時如果沒有指定parent,那麼它的parent預設就是AppClassLoader。 

為什麼呢?

原因是:如果預設的parent父載入器是AppClassLoader,這樣就能夠保證它能訪問系統內建載入器載入成功的class檔案。


要點二:

一般儘量不要重寫ClassLoader抽象類的loadClass()方法,破壞其中的雙親委託的程式邏輯。

在JVM規範和JDK文件中(1.2或者以後版本中),都沒有建議使用者重寫loadClass()方法,相比而言,明確提示開發者在開發自定義的類載入器時重寫findClass()邏輯。




原始碼:


程式碼工程:  classLoaderDemo.zip

下載地址:在瘋狂創客圈QQ群檔案共享。


瘋狂創客圈:如果說Java是一個武林,這裡的聚集一群武痴, 交流程式設計體驗心得
QQ群連結:
瘋狂創客圈QQ群


無程式設計不創客,無案例不學習。 一定記得去跑一跑案例哦


類載入器系列全目錄

1.匯入

2. JAVA類載入器分類

3. 揭祕ClassLoader抽象基類

4. 神祕的雙親委託機制

5. 入門案例:自定義一個檔案系統的classLoader

6. 基礎案例:自定義一個網路類載入器

7. 中級案例:設計一個加密的自定義網路載入器

8. 高階案例1:使用ASM技術,結合類載入器,解密AOP原理

9. 高階案例2:上下文載入器原理和案例