Java使用自定義類加載器實現熱部署


// 2020-08-01:之前的代碼 findClass 寫成 loadClass 了,弄錯了。

熱部署:

     熱部署就是在不重啟應用的情況下,當類的定義即字節碼文件修改后,能夠替換該Class創建的對象。一般情況下,類的加載都是由系統自帶的類加載器完成,且對於同一個全限定名的java類,只能被加載一次,而且無法被卸載。可以使用自定義的 ClassLoader 替換系統的加載器,創建一個新的 ClassLoader,再用它加載 Class,得到的 Class 對象就是新的(因為不是同一個類加載器),再用該 Class 對象創建一個實例,從而實現動態更新。如:修改 JSP 文件即生效,就是利用自定義的 ClassLoader 實現的。

    還需要創建一個守護線程,不斷地檢查class文件是否被修改過,通過判斷文件的上次修改時間實現。

 

演示:

原來的程序:

 

修改后重新編譯:

 

 

 

 

代碼:

 dynamic.ClassLoader:

package dynamic;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;

public class ClassLoadStudy {
    public static void main(String[] args) throws Exception {
        HotDeploy hot = new HotDeploy("dynamic.Task");
        hot.monitor();
        while (true) {
            TimeUnit.SECONDS.sleep(2);
            hot.getTask().run();
        }
    }
}

/**
 * 熱部署類
 */
class HotDeploy {
    private static volatile Runnable instance;
    private final String FILE_NAME;
    private final String CLASS_NAME;

    public HotDeploy(String name) {
        CLASS_NAME = name; // 類的完全限定名
        name = name.replaceAll("\\.", "/") + ".class";
        FILE_NAME = (getClass().getResource("/") + name).substring(6); // 判斷class文件修改時間使用,substring(6)去掉開頭的file:/
    }

    /**
     * 獲取一個任務
     * @return
     */
    public Runnable getTask() {
        if (instance == null) { // 雙重檢查鎖,單例,線程安全
            synchronized (this) {
                if (instance == null) {
                    try {
                        instance = createTask();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return instance;
    }

    /**
     * 創建一個任務,重新加載 class 文件
     */
    private Runnable createTask() {
        try {
            Class<?> clazz = new MyClassLoader(null).loadClass(CLASS_NAME);
            if (clazz != null)
                return (Runnable)clazz.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }


    /**
     * 監視器,監視class文件是否被修改過,如果是的話,則重新加載
     */
    public void monitor() {
        Thread t = new Thread(()->{
            try {
                long lastModified = Files.getLastModifiedTime(Path.of(FILE_NAME)).toMillis();
                while(true) {
                    TimeUnit.SECONDS.sleep(1);
                    long now = Files.getLastModifiedTime(Path.of(FILE_NAME)).toMillis();
                    if(now != lastModified) { // 如果class文件被修改過了
                        System.out.println("yes");
                        lastModified = now;
                        instance = createTask(); // 重新加載
                    }
                }
            } catch (InterruptedException | IOException e) {
                e.printStackTrace();
            }
        });
        t.setDaemon(true); // 守護進程
        t.start();
    }


    /**
     * 自定義類加載器
     */
    private class MyClassLoader extends ClassLoader {
        private MyClassLoader(ClassLoader parent) {
            super(parent);
        }

        @Override
        public Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] b = Files.readAllBytes(Path.of(FILE_NAME));
                return defineClass(name, b, 0, b.length);
            } catch (IOException e) {
                throw new ClassNotFoundException(name);
            }
        }
    }
}

  

動態改變的 Task 類,dynamic.Task:

package dynamic;

public class Task implements Runnable {
    @Override
    public void run() {
        System.out.print("=> ");
    }
}

  

 

 

遇到的坑:

剛開始自定義類加載器時,重寫的是 loadClass(String name) 方法,但不斷地報錯,后來明白了,因為 Task 類實現了 Java.lang.Runnable 接口,且重寫 loadClass 方法破壞了雙親委派機制,導致了自定義的類加載器去加載 java.lang.Runnable,但被Java安全機制禁止了所以會報錯。defineClass調用preDefineClass,preDefineClass 會檢查包名,如果以java開頭,就會拋出異常,因為讓用戶自定義的類加載器來加載Java自帶的類庫會引起混亂。

於是又重寫findClass 方法,但還是不行,findClass方法總是得不到執行,因為編譯好的類是在 classpath 下的,而自定義的 ClassLoader 的父加載器是 AppClassLoader,由於雙親委派機制,類就會被 Application ClassLoader來加載了。因此自定義的 findClass 方法就不會被執行。解決方法是,向構造器 ClassLoader(ClassLoader parent) 傳入null,或傳入 getSystemClassLoader().getParent(),這樣就可以保證,目標類被自定義加載器加載,而java.lang.Runnable被BootStrap類加載器加載了。當然,如果被加載的類如果不在classpath下,就不會出現這些問題了。

還有就是路徑問題:

  • path不以 / 開頭時,默認是從此類所在的包下取資源;path 以 / 開頭時,則是從ClassPath根下獲取;

    • URL getClass.getResource(String path)

    • InputStream getClass().getResourceAsStream(String path)

    • getResource("") 返回當前類所在的包的路徑

    • getResource("/") 返回當前的 classpath 根據路徑

  • path 不能以 / 開始,path 是從 classpath 根開始算的, 因為classloader 不是用戶自定義的類,所以沒有相對路徑的配置文件可以獲取,所以默認都是從哪個classpath 路徑下讀取,自然就沒有必要以 / 開頭了 。

    • URL Class.getClassLoader().getResource(String path)

    • InputStream Class.getClassLoader().getResourceAsStream(String path)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM