Java程式碼實現熱部署

 

一.思路

0. 監聽java文件最後修改時間,如果發生變化,則表示文件已經修改,進行重新編譯

 

1. 編譯java文件為 class文件

 

2. 通過手寫類載入器,載入 class文件 ,創建對象

 

3. 反射創建對象 / 進行調用,(如果是web項目可以將創建的對象添加到spring容器中)

 

4. 調用測試

 

二.知識點

       1. 自定義類載入器 繼承 URLClassLoader 或 ClassLoader 都可以,繼承 URLClassLoader 重寫findClass(String name)方法即可實現載入class文件;

 

         2. findClass方法核心語句 :return super.defineClass(String name, byte[] b, int off, int len)方法,b是class文件讀取後得到的byte[]形式;

 

         3. cmd窗口 使用javac即可將java文件編譯成 class文件,在程式碼里使用 JavaCompiler 類,調用run方法即可編譯指定java文件為class文件;

 

         4. JavaCompiler不支援直接new,通過類ToolProvider.getSystemJavaCompiler()方法獲取;

 

         5. 通過類載入器獲取的 class文件有時不方便調用,所以可以採用反射調用;

 

         6. 對於一個java文件,可以通過File類的 lastModified獲取最後修改時間,循環比較lastModified即可判斷文件是否被修改;

 

         7. class文件可以生成在任意目錄,通過路徑讀取即可;

 

         8. 選擇合適的類載入器或自定義類載入器,對於電腦上任意位置的class文件完全都可以通過反射調用;

 

    9. @SneakyThrows可以理解成 try-catch,使用需要導入lombok

 

 

         10. 本demo是通過查閱資料和不斷測試實現,如果有不足請指出;

 

三.實現

1. Demo概述

目標: 實現對HotTestService類的熱部署,通過測試(main)監控java文件,如果java文件變動調用自定義類載入器MyClassLoader得到HotTestService的class對象,反射調用

 

2. 核心測試方法

package com.ahd.springtest.utils;

import lombok.SneakyThrows;

import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.TimeUnit;

public class AhdgTest {

    //sourcePath 是java文件存放,編輯的路徑
    private static String sourcePath = "D:\\workspace_ms\\20210319\\springtest\\src\\main\\java";
    //targetPath 是class文件存放路徑
//    private static String targetPath = "D:\\workspace_ms\\20210319\\springtest\\target\\classes";
    private static String targetPath = "D:\\workspace_ms\\20210319\\springtest\\src\\main\\java";
    private static String errPath = "D:\\文件清單\\hotlog.txt";//編譯日誌列印目錄
    private static String basePath = "\\com\\ahd\\springtest\\service\\HotTestService"; //包名 + 類名,路徑形式


    public static void main(String[] args) throws InterruptedException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, MalformedURLException, ClassNotFoundException {
        //測試熱部署
        testHot();
    }
    /***
     * 目標 : main方法循環調用,實時監測 com.ahd.springtest.service.HotTestService 是否發生改變,如果發生改變,重新載入並調用
     *
     * 0. 監聽java文件最後修改時間,如果發生變化,則表示文件已經修改,進行重新編譯
     *
     * 1. 編譯java文件為 class文件
     *
     * 2. 通過手寫類載入器載入 class文件 ,創建對象
     *
     * 3. 將新創建的對象 放入spring容器中
     *
     * 4. 調用測試
     *
     */
    public static void testHot() throws MalformedURLException, IllegalAccessException, InstantiationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InterruptedException {
        //        0. 監聽java文件最後修改時間,(如果發生變化,則表示文件已經修改,需要進行重新編譯)
        File file = new File(sourcePath + basePath + ".java");


        Thread thread = new Thread(new Runnable() {
            @SneakyThrows  //簡化 try catch寫法
            @Override
            public void run() {
                Long lastModifiedTime = file.lastModified();

                while (true) {
                    long timeEnd = file.lastModified();
                    if (timeEnd != lastModifiedTime) {
                        lastModifiedTime = timeEnd;

                        //        1. 編譯java文件為 class文件
                        try (InputStream is = new FileInputStream(file.getAbsolutePath());
                             OutputStream os = new FileOutputStream(targetPath + basePath + ".class");
                             OutputStream err = new FileOutputStream(errPath)) {

                            JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
                            javaCompiler.run(is, os, err, sourcePath + basePath + ".java");//前三參數傳入null 默認是 System.in,System.out.System.err

                        } catch (FileNotFoundException e) {
                            e.printStackTrace();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                        //        2. 通過手寫類載入器載入 class文件 ,創建對象
                        MyClassLoader instance = MyClassLoader.getInstance(new File(targetPath).toURI().toURL());
                        Class<?> aClass = instance.findClass("com.ahd.springtest.service.HotTestService");

                        Object o = aClass.newInstance();
                        //        3. 將新創建的對象 反射調用
                        Method test = aClass.getMethod("test");
                        Object invoke = test.invoke(o);
                        System.out.println(invoke);
                    }
                    try {
                        Thread.sleep(20);//檢測頻率:100ms
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        thread.setDaemon(true);//設置成守護執行緒
        thread.start();

//讓主main一直運行,可以查看結果
        while(true){
            Thread.sleep(1000);
        }
    }
}

核心程式碼

 

 

3. 自定義類載入器MyClassLoader

package com.ahd.springtest.utils;

import com.sun.xml.internal.ws.util.ByteArrayBuffer;
import lombok.SneakyThrows;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class MyClassLoader extends URLClassLoader {
    private static MyClassLoader myClassLoader;//可以直接通過getInstance方法獲取
    private URL[] urls;
    private URL url;

    public MyClassLoader(URL url) {
        super(new URL[]{url});
        this.url = url;
    }

    public MyClassLoader(URL[] urls) {
        super(urls);
        this.urls = urls;
    }
    /***
     *         1. name 是 類的 全限命名,通過全限名命 + 路徑 獲取 絕對路徑
     *
     *         2. io獲取位元組碼
     *
     *         3. 調用父類方法創建並返回class對象
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @SneakyThrows //簡化的 try catch寫法
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //1. name 是 類的 全限命名,通過全限名命 + 路徑 獲取 絕對路徑
        String path = url.getPath();
//        System.out.println("yangdc log:  " + path);
        String classPath = path + name.replaceAll("\\.","/").concat(".class");

        //2. io獲取位元組碼
        InputStream is = null;
        URL url = null;
        int b = 0;
        ByteArrayBuffer bab = new ByteArrayBuffer();
        try {
            url = new URL("file:" + classPath);
//            url = new URL("jar:" + classPath);
            is = url.openStream();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        while((b = is.read())!=-1){
            bab.write(b);
        }
        is.close();

        //3. 調用父類方法創建並返回class對象
        return super.defineClass(name,bab.toByteArray(),0,bab.size());
    }

    public static MyClassLoader getInstance(URL url){
        if (myClassLoader == null){
            return new MyClassLoader(url);
        }
        return myClassLoader;
    }

    public static MyClassLoader getInstance(URL[] url){
        if (myClassLoader == null){
            return new MyClassLoader(url);
        }
        return myClassLoader;
    }


    public URL[] getUrls() {
        return urls;
    }

    public void setUrls(URL[] urls) {
        this.urls = urls;
    }

    public URL getUrl() {
        return url;
    }

    public void setUrl(URL url) {
        this.url = url;
    }
}

自定義類載入器

 

4. 被測試的類HotTestService

package com.ahd.springtest.service;
import org.springframework.stereotype.Service;

@Service
public class HotTestService {
    public HotTestService() {
    }

    public String test() {
        return "第39696633次測試hot";
    }
}

 

測試結果來張圖