Java類載入器( CLassLoader ) 死磕 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. 自定義網路類載入器的類設計
服務端的類圖如下:
客戶端的類圖:
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群
無程式設計不創客,無案例不學習。 一定記得去跑一跑案例哦
類載入器系列全目錄