徒手撸一个热部署插件!

共 5621字,需浏览 12分钟

 ·

2021-11-18 02:00

你知道的越多,不知道的就越多,业余的像一棵小草!

你来,我们一起精进!你不来,我和你的竞争对手一起精进!

编辑:业余草

juejin.cn/post/7031051782939738125

推荐:https://www.xttblog.com/?p=5290

引言

在项目开发中,每次修改文件就需要重启一次代码,这样太浪费时间了,所以在IDEA中使用JRebel插件实现项目🔥热部署,可自动热部署,无需重启项目。虽然一直清楚热部署是打破双亲委派来实现的,但是一直没有手写过热部署代码,今天写一次。😁

双亲委派机制

了解热部署之前,首先需要知道什么是双亲委派,在 IDE 中写的代码最终经过编译器会形成 .class 文件,由 classLoader 加载到 JVM 中执行。

JVM 中提供了三层的 ClassLoader:

  • Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
  • ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。
  • AppClassLoader:主要负责加载应用程序的主函数类

加载过程图如下:

双亲委派机制

实现热部署思路

一个类一旦被JVM加载过,就不会再次被加载。想实现热部署,就需要在.class文件修改后,由classLoader重新加载修改的.class文件。对.class文件做监听,一旦文件修改,则重新加载类。

在此实现中用一个Map模拟JVM已经加载过的.class文件,当监听到文件内容修改之后,移除Map中旧的.class文件,将新的.class文件加载并存放至Map中,调用init方法,执行初始化动作,模拟.class文件已经加载到JVM虚拟机中。

下面讲代码实现!

pom文件:


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0modelVersion>

    <groupId>com.hanhanggroupId>
    <artifactId>hotCodeartifactId>
    <version>1.0-SNAPSHOTversion>
    <dependencies>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.22version>
            <scope>compilescope>
        dependency>
        <dependency>
            <groupId>log4jgroupId>
            <artifactId>log4jartifactId>
            <version>1.2.17version>
        dependency>
        <dependency>
            <groupId>org.slf4jgroupId>
            <artifactId>slf4j-log4j12artifactId>
            <version>1.7.26version>
        dependency>
        <dependency>
            <groupId>org.slf4jgroupId>
            <artifactId>slf4j-apiartifactId>
            <version>1.7.26version>
        dependency>
        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-vfs2artifactId>
            <version>2.9.0version>
        dependency>
        <dependency>
            <groupId>com.thoughtworks.xstreamgroupId>
            <artifactId>xstreamartifactId>
            <version>1.4.18version>
        dependency>
    dependencies>

    <properties>
        <maven.compiler.source>8maven.compiler.source>
        <maven.compiler.target>8maven.compiler.target>
    properties>

project>

IApplication接口

定义IApplication接口,所有监听的类都实现自这个接口。

public interface IApplication {
    /**
     * 初始化
     */

    void init();

    /**
     * 执行
     */

    void execute();

    /**
     * 销毁
     */

    void destroy();
}

TestApplication1

监听加载的类。

public class TestApplication1 implements IApplication {
    @Override
    public void init() {
        System.out.println("TestApplication1--》3");
    }

    @Override
    public void execute() {
        System.out.println("TestApplication1--》execute");
    }

    @Override
    public void destroy() {
        System.out.println("TestApplication1--》destroy");
    }
}

IClassLoader

类加载器,实现通过包扫描类的功能。

public interface IClassLoader {
    /**
     * 创建classLoader
     * @param parentClassLoader 父classLoader
     * @param paths 路径
     * @return 类加载器
     */

    ClassLoader createClassLoader(ClassLoader parentClassLoader, String...paths);
}

SimpleJarLoader

public class SimpleJarLoader implements IClassLoader {
    @Override
    public ClassLoader createClassLoader(ClassLoader parentClassLoader, String... paths) {
        List jarsToLoad = new ArrayList<>();
        for (String folder : paths) {
            List jarPaths = scanJarFiles(folder);

            for (String jar : jarPaths) {

                try {
                    File file = new File(jar);
                    jarsToLoad.add(file.toURI().toURL());

                } catch (MalformedURLException e) {
                    e.printStackTrace();
                }
            }
        }

        URL[] urls = new URL[jarsToLoad.size()];
        jarsToLoad.toArray(urls);

        return new URLClassLoader(urls, parentClassLoader);
    }

    /**
     * 扫描文件
     * @param folderPath 文件路径
     * @return 文件列表
     */

    private List scanJarFiles(String folderPath) {

        List jars = new ArrayList<>();
        File folder = new File(folderPath);
        if (!folder.isDirectory()) {
            throw new RuntimeException("扫描的路径不存在, path:" + folderPath);
        }

        for (File f : Objects.requireNonNull(folder.listFiles())) {
            if (!f.isFile()) {
                continue;
            }
            String name = f.getName();

            if (name.length() == 0) {
                continue;
            }

            int extIndex = name.lastIndexOf(".");
            if (extIndex < 0) {
                continue;
            }

            String ext = name.substring(extIndex);
            if (!".jar".equalsIgnoreCase(ext)) {
                continue;
            }

            jars.add(folderPath + "/" + name);
        }
        return jars;
    }
}

AppConfigList配置类

@Data
public class AppConfigList {
    private List configs;

    @Data
    public static class AppConfig{
        private String name;

        private String file;
    }
}

GlobalSetting 全局配置类

public class GlobalSetting {
    public static final String APP_CONFIG_NAME = "application.xml";
    public static final String JAR_FOLDER = "com/hanhang/app/";
}

application.xml配置

通过xml配置加后面的解析,确定监听那个class文件。

<apps>
    <app>
        <name>TestApplication1name>
        <file>com.hanhang.app.TestApplication1file>
    app>
apps>

JarFileChangeListener 监听器

public class JarFileChangeListener implements FileListener {
    @Override
    public void fileCreated(FileChangeEvent fileChangeEvent) throws Exception {
        String name = fileChangeEvent.getFileObject().getName().getBaseName().replace(".class","");

        ApplicationManager.getInstance().reloadApplication(name);
    }

    @Override
    public void fileDeleted(FileChangeEvent fileChangeEvent) throws Exception {
        String name = fileChangeEvent.getFileObject().getName().getBaseName().replace(".class","");

        ApplicationManager.getInstance().reloadApplication(name);
    }

    @Override
    public void fileChanged(FileChangeEvent fileChangeEvent) throws Exception {
        String name = fileChangeEvent.getFileObject().getName().getBaseName().replace(".class","");

        ApplicationManager.getInstance().reloadApplication(name);

    }
}

AppConfigManager

此类为config的管理类,用于加载配置。

public class AppConfigManager {
    private final List configs;

    public AppConfigManager(){
        configs = new ArrayList<>();
    }

    /**
     * 加载配置
     * @param path 路径
     */

    public void loadAllApplicationConfigs(URI path){

        File file = new File(path);
        XStream xstream = getXmlDefine();
        try {
            AppConfigList configList = (AppConfigList)xstream.fromXML(new FileInputStream(file));

            if(configList.getConfigs() != null){
                this.configs.addAll(new ArrayList<>(configList.getConfigs()));
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

    }

    /**
     * 获取xml配置定义
     * @return XStream
     */

    private XStream getXmlDefine(){
        XStream xstream = new XStream(new DomDriver());
        xstream.alias("apps", AppConfigList.class);
        xstream.alias("app", AppConfigList.AppConfig.class);
        xstream.aliasField("name", AppConfigList.AppConfig.class, "name");
        xstream.aliasField("file", AppConfigList.AppConfig.class, "file");
        xstream.addImplicitCollection(AppConfigList.class, "configs");
        Class[] classes = new Class[] {AppConfigList.class,AppConfigList.AppConfig.class};
        xstream.allowTypes(classes);
        return xstream;
    }

    public final List getConfigs() {
        return configs;
    }

    public AppConfigList.AppConfig getConfig(String name){
        for(AppConfigList.AppConfig config : this.configs){
            if(config.getName().equalsIgnoreCase(name)){
                return config;
            }
        }
        return null;
    }
}

ApplicationManager

此类管理已经在Map中加载的类,并且添加监听器,实现class文件修改后的监听重新加载工作。

public class ApplicationManager {
    private static ApplicationManager instance;

    private IClassLoader jarLoader;
    private AppConfigManager configManager;

    private Map apps;

    private ApplicationManager(){
    }

    public void init(){
        jarLoader = new SimpleJarLoader();
        configManager = new AppConfigManager();
        apps = new HashMap<>();

        initAppConfigs();

        URL basePath = this.getClass().getClassLoader().getResource("");

        loadAllApplications(Objects.requireNonNull(basePath).getPath());

        initMonitorForChange(basePath.getPath());
    }

    /**
     * 初始化配置
     */

    public void initAppConfigs(){

        try {
            URL path = this.getClass().getClassLoader().getResource(GlobalSetting.APP_CONFIG_NAME);
            configManager.loadAllApplicationConfigs(Objects.requireNonNull(path).toURI());
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }

    /**
     * 加载类
     * @param basePath 根目录
     */

    public void loadAllApplications(String basePath){

        for(AppConfigList.AppConfig config : this.configManager.getConfigs()){
            this.createApplication(basePath, config);
        }
    }

    /**
     * 初始化监听器
     * @param basePath 路径
     */

    public void initMonitorForChange(String basePath){
        try {
            FileSystemManager fileManager = VFS.getManager();

            File file = new File(basePath + GlobalSetting.JAR_FOLDER);
            FileObject monitoredDir = fileManager.resolveFile(file.getAbsolutePath());
            FileListener fileMonitorListener = new JarFileChangeListener();
            DefaultFileMonitor fileMonitor = new DefaultFileMonitor(fileMonitorListener);
            fileMonitor.setRecursive(true);
            fileMonitor.addFile(monitoredDir);
            fileMonitor.start();
            System.out.println("Now to listen " + monitoredDir.getName().getPath());

        } catch (FileSystemException e) {
            e.printStackTrace();
        }
    }

    /**
     * 根据配置加载类
     * @param basePath 路径
     * @param config 配置
     */

    public void createApplication(String basePath, AppConfigList.AppConfig config){
        String folderName = basePath + GlobalSetting.JAR_FOLDER;
        ClassLoader loader = this.jarLoader.createClassLoader(ApplicationManager.class.getClassLoader(), folderName);

        try {
            Class appClass = loader.loadClass(config.getFile());

            IApplication app = (IApplication)appClass.newInstance();

            app.init();

            this.apps.put(config.getName(), app);

        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * 重新加载
     * @param name 类名
     */

    public void reloadApplication(String name){
        IApplication oldApp = this.apps.remove(name);

        if(oldApp == null){
            return;
        }

        oldApp.destroy();

        AppConfigList.AppConfig config = this.configManager.getConfig(name);
        if(config == null){
            return;
        }

        createApplication(getBasePath(), config);
    }

    public static ApplicationManager getInstance(){
        if(instance == null){
            instance = new ApplicationManager();
        }
        return instance;
    }

    /**
     * 获取类
     * @param name 类名
     * @return 缓存中的类
     */

    public IApplication getApplication(String name){
        if(this.apps.containsKey(name)){
            return this.apps.get(name);
        }
        return null;
    }

    public String getBasePath(){
        return Objects.requireNonNull(this.getClass().getClassLoader().getResource("")).getPath();
    }
}

MainTest

测试类,创建一个线程,让程序一直监听文件修改。

public static void main(String[] args){

    Thread t = new Thread(new Runnable() {

        @Override
        public void run() {
            ApplicationManager manager = ApplicationManager.getInstance();
            manager.init();
        }
    });

    t.start();

    while(true){
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

代码演示

程序启动后,控制台输出。

代码演示

TestApplication1的 init 方法修改为:

@Override
public void init() {
    System.out.println("TestApplication1--》300");
}

重新build项目,控制台输出如下:

热发布、热部署

此时,TestApplication1已经重新加载。以上就是我实现🔥热部署的关键代码,如需完整代码,加微信:xttblog2,免费获取!

浏览 61
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报