1. 程式人生 > >Java類載入器( 深磕 6)

Java類載入器( 深磕 6)

【正文】Java類載入器(  CLassLoader )深磕 6: 

自定義網路類載入器

本小節目錄

6.1. 自定義網路類載入器的類設計
6.2. 檔案傳輸Server端的原始碼
6.3. 檔案傳輸Client端的原始碼
6. 4 自定義載入器SocketClassLoader的原始碼
6.5. SocketClassLoader的使用


前面提到,除了通過Java內建的三大載入器,從JVM中系統屬性中設定的三大地盤載入Java類,還存多種的獲取Class檔案途徑。其中非常重要的一種途徑,就是網路。

通過網路的載入類,就得依賴網路的傳輸協議。

網路的傳輸協議有很多種,比方說TCP、HTTP、SMB等等,都可以用來實現網路的類位元組碼的傳輸。

TCP是最為基礎的,也是一種傳輸可靠的傳輸協議。本小節通過TCP協議,實現自定義的網路類載入器。


1.1.1. 自定義網路類載入器的類設計


服務端的類圖如下:

wpsC7EF.tmp

客戶端的類圖:

wpsC800.tmp


1.1.2. 檔案傳輸Server端的原始碼


檔案傳輸Server端的工作:

開啟一個ServerSocket服務,等待Client客戶端的TCP連線。

對於每一個客戶端TCP連線,開啟一個單獨的執行緒,處理檔案的請求和傳送檔案資料。

獨立執行緒首先會接受客戶端傳輸過來的檔名稱,根據檔名稱,在自定義的類路徑下查詢檔案。這裡的類路徑是這個第三方類庫的路徑,為了可以靈活多變,並且與原始碼工程的輸出路徑相不能相同,也在System.properties 配置檔案增加配置項,具體為:

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

此配置項在前面的案例中,已經用到了,後面也會多次用到。

服務端找到檔案後,開始向客戶端傳輸資料。首先傳輸檔案的大小,然後在傳輸檔案的內容。

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

public class SocketClassServer

{

    ServerSocket serverSocket = null;

    static String filePath = null;

    public SocketClassServer() throws Exception

    {

        serverSocket = new ServerSocket(SystemConfig.SOCKET_SERVER_PORT);

        this.filePath = SystemConfig.CLASS_SERVER_PATH;

        startServer();

    }

    /**

     * 啟動服務端

     * 使用執行緒處理每個客戶端傳輸的檔案

     *

     * @throws Exception

     */

    public void startServer()

    {

        while (true)

        {

            // server嘗試接收其他Socket的連線請求,server的accept方法是阻塞式的

            Logger.info("server listen at:" + SystemConfig.SOCKET_SERVER_PORT);

            Socket socket = null;

            try

            {

                socket = serverSocket.accept();

                // 每接收到一個Socket就建立一個新的執行緒來處理它

                new Thread(new SendTask(socket)).start();

            } catch (Exception e)

            {

                e.printStackTrace();

            }

        }

    }

    /**

     * 處理客戶端傳輸過來的檔案執行緒類

     */

    class SendTask implements Runnable

    {

        private Socket socket;

        private DataInputStream dis;

        private FileOutputStream fos;

        public SendTask(Socket socket)

        {

            this.socket = socket;

        }

        @Override

        public void run()

        {

            try

            {

                dis = new DataInputStream(socket.getInputStream());

                // 檔名

                String fileName = dis.readUTF();

                DataOutputStream dos = new DataOutputStream(socket.getOutputStream());

                sendFile(fileName, dos);

            } catch (Exception e)

            {

                e.printStackTrace();

            } finally

            {

                IOUtil.closeQuietly(fos);

                IOUtil.closeQuietly(dis);

                IOUtil.closeQuietly(socket);

            }

        }

        private void sendFile(String fileName, DataOutputStream dos) throws Exception

        {

            fileName = classNameToPath(fileName);

            fileName = SocketClassServer.filePath + File.separator + fileName;

            File file = new File(fileName);

            if (!file.exists())

            {

                throw new Exception("file not found! :" + fileName);

            }

            long fileLen = file.length();

//先傳輸檔案長度

            dos.writeLong(fileLen);

            dos.flush();

            FileInputStream fis = new FileInputStream(file);

            // 開始傳輸檔案

            Logger.info("======== 開始傳輸檔案 ========");

            byte[] bytes = new byte[1024];

            int length = 0;

            long progress = 0;

            while ((length = fis.read(bytes, 0, bytes.length)) != -1)

            {

                dos.write(bytes, 0, length);

                dos.flush();

                progress += length;

                Logger.info("| " + (100 * progress / fileLen) + "% |");

            }

            Logger.info("======== 檔案傳輸成功 ========");

        }

    }

    private String classNameToPath(String className)

    {

        return className.replace('.', '/') + ".class";

    }

    public static void main(String[] args)

    {

        try

        {

            SocketClassServer socketServer = new SocketClassServer();

            socketServer.startServer();

        } catch (Exception e)

        {

            e.printStackTrace();

        }

    }

}

原始碼比較長,建議執行main函式,先將服務端的原始碼跑起來,然後再閱讀程式碼,這樣閱讀起來更加容易懂。

另外,在使用基於網路的類載入器之前,一定要確保服務端的程式碼先執行。否則客戶端會報錯。

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

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

執行的結果是:

         <clinit> |>  開始載入配置檔案到SystemConfig

        loadFromFile |>  load properties: /system.properties

         startServer |>  server listen at:18899

看到以上結果,表示服務端開始啟動。監聽了18899埠,等待客戶端的連線。


1.1.3. 檔案傳輸Client端的原始碼


客戶端的工作:

建立和伺服器的TCP連線後,首先做的第一步工作,是傳送檔名稱給伺服器端。

然後阻塞,直到伺服器的資料過來。客戶端開始接受伺服器傳輸過來的資料。接受資料的工作由函式receivefile()完成。

整個的資料的讀取工作分為兩步,先讀取檔案的大小,然後讀取傳輸過來的檔案內容。

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

public class SafeSocketClient {

    private Socket client;

    private FileInputStream fis;

    private DataOutputStream dos;

    /**

     * 建構函式<br/>

     * 與伺服器建立連線

     *

     * @throws Exception

     */

    public SafeSocketClient() throws IOException {

            this.client = new Socket(

                    SystemConfig.SOCKET_SERVER_IP,

                    SystemConfig.SOCKET_SERVER_PORT

            );

            Logger.info("Cliect[port:" + client.getLocalPort() + "] 成功連線服務端");

    }

    /**

     * 向服務端去取得檔案

     *

     * @throws Exception

     */

    public byte[] getFile(String fileName) throws Exception {

        byte[] result = null;

        try {

            dos = new DataOutputStream(client.getOutputStream());

            // 檔名和長度

            dos.writeUTF(fileName);

            dos.flush();

            DataInputStream dis = new DataInputStream(client.getInputStream());

            result = receivefile(dis);

            Logger.info("檔案接收成功,File Name:" + fileName);

        } catch (Exception e) {

            e.printStackTrace();

        } finally {

            IOUtil.closeQuietly(fis);

            IOUtil.closeQuietly(dos);

            IOUtil.closeQuietly(client);

        }

        return result;

    }

    public byte[] receivefile(DataInputStream dis) throws Exception {

        int fileLength = (int) dis.readLong();

        ByteArrayOutputStream bos = new ByteArrayOutputStream(fileLength);

        long startTime = System.currentTimeMillis();

        Logger.info("block IO 傳輸開始:");

        // 開始接收檔案

        byte[] bytes = new byte[1024];

        int length = 0;

        while ((length = dis.read(bytes, 0, bytes.length)) != -1) {

            DeEnCode.decode(bytes,length);

            bos.write(bytes, 0, length);

            bos.flush();

        }

        Logger.info(" Size:" + IOUtil.getFormatFileSize(fileLength));

        long endTime = System.currentTimeMillis();

        Logger.info("block IO 傳輸毫秒數:" + (endTime - startTime));

        bos.flush();

        byte[] result = bos.toByteArray();

        IOUtil.closeQuietly(bos);

        return result;

    }

}

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

案例提示:無程式設計不創客、無案例不學習。

此案例類沒法獨立執行,因為沒有執行的入口。只能作為基礎類,供其他類呼叫。


1.1.4. 自定義載入器SocketClassLoader的原始碼


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

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

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

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

(3)返回Class類物件。

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

public class SocketClassLoader extends ClassLoader {

    public SocketClassLoader() {

//        super(null);

    }

    protected Class<?> findClass(String name)

throws ClassNotFoundException {

       Logger.info("findClass name = " + name);

        byte[] classData = null;

        try {

            SocketClassClient client = new SocketClassClient();

            classData = client.getFile(name);

        } catch (Exception e) {

            e.printStackTrace();

            throw new ClassNotFoundException();

        }

        if (classData == null) {

            throw new ClassNotFoundException();

        } else {

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

        }

    }

}

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

有了前面的基礎,此原始碼超級簡單,關鍵的兩行,就是下面這個兩行:

  SafeSocketClient client = new SafeSocketClient();

  classData = client.getFile(name);

上面一行,例項化一個SafeSocketClient 客戶端client物件。下面一行,通過取得 client物件的client.getFile(name) 方法,通過網路TCP協議,獲得類的位元組碼。

至於findClass()方法中的其他的處理工作,和前面的檔案系統內載入器FileClassLoader 中的findClass(),是一樣的。

這裡不做贅述。


1.1.5. SocketClassLoader的使用


簡單粗暴,先上程式碼:

public class SocketLoaderDemo

{

    public static void testLoader()

    {

        try

        {

            SocketClassLoader classLoader = new SocketClassLoader();

            String className = SystemConfig.PET_DOG_CLASS;

            Class dogClass = classLoader.loadClass(className);

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

            ClassLoaderUtil.showLoader4Class(dogClass);

            IPet pet = (IPet) dogClass.newInstance();

            pet.sayHello();

        } catch (ClassNotFoundException e)

        {

            e.printStackTrace();

        } catch (IllegalAccessException e)

        {

            e.printStackTrace();

        } catch (InstantiationException e)

        {

            e.printStackTrace();

        }

    }

    public static void main(String[] args)

    {

        testLoader();

    }

}

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

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

執行的結果是:

  <clinit> |>  開始載入配置檔案到SystemConfig

        loadFromFile |>  load properties: /system.properties

           findClass |>  findClass name = com.crazymakercircle.otherPet.pet.LittleDog

              <init> |>  Cliect[port:51525] 成功連線服務端

         receivefile |>  block IO 傳輸開始:

         receivefile |>  Size:2.0KB

         receivefile |>  block IO 傳輸毫秒數:8

             getFile |>  檔案接收成功,File Name:com.crazymakercircle.otherPet.pet.LittleDog

          testLoader |>  顯示dogClass的ClassLoader =>

      showLoaderTree |>  [email protected]

      showLoaderTree |>  [email protected]

      showLoaderTree |>  [email protected]

Disconnected from the target VM, address: '127.0.0.1:51523', transport: 'socket'

            sayHello |>  嗨,大家好!我是LittleDog-1

看到以上結果,表示客戶端成功接收了位元組碼檔案,並且成功載入了類。

擴充套件一下,例如你的第三方的位元組碼是放在資料庫中,也可類似的自己寫個類載入器,從指定的資料庫載入二進位制類。




原始碼:


程式碼工程:  classLoaderDemo.zip

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


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


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


類載入器系列全目錄

1.匯入

2. JAVA類載入器分類

3. 揭祕ClassLoader抽象基類

4. 神祕的雙親委託機制

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

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

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

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

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