手写热部署

1,255 阅读4分钟

「这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

引言

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

双亲委派机制

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

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

实现热部署思路

一个类一旦被JVM加载过,就不会再次被加载。想实现热部署,就需要在.class文件修改后,由classLoader重新加载修改的.class文件。对.class文件做监听,一旦文件修改,则重新加载类。 在此实现中用一个Map模拟JVM已经加载过的.class文件,当监听到文件内容修改之后,移除Map中旧的.class文件,将新的.class文件加载并存放至Map中,调用init方法,执行初始化动作,模拟.class文件已经加载到JVM虚拟机中。

代码实现

pom文件

<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>

    <groupId>com.hanhang</groupId>
    <artifactId>hotCode</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.26</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.26</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-vfs2</artifactId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>com.thoughtworks.xstream</groupId>
            <artifactId>xstream</artifactId>
            <version>1.4.18</version>
        </dependency>
    </dependencies>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.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

import com.hanhang.inter.IClassLoader;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * @author hanhang
 */
public class SimpleJarLoader implements IClassLoader {
    @Override
    public ClassLoader createClassLoader(ClassLoader parentClassLoader, String... paths) {
        List<URL> jarsToLoad = new ArrayList<>();
        for (String folder : paths) {
            List<String> 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<String> scanJarFiles(String folderPath) {

        List<String> 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<AppConfig> 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>TestApplication1</name>
        <file>com.hanhang.app.TestApplication1</file>
    </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的管理类,用于加载配置。

import com.hanhang.config.AppConfigList;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;

/**
 * @author hanhang
 */
public class AppConfigManager {
    private final List<AppConfigList.AppConfig> 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<AppConfigList.AppConfig> 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文件修改后的监听重新加载工作。

import com.hanhang.config.AppConfigList;
import com.hanhang.config.GlobalSetting;
import com.hanhang.inter.IApplication;
import com.hanhang.inter.IClassLoader;
import com.hanhang.inter.impl.SimpleJarLoader;
import com.hanhang.listener.JarFileChangeListener;
import org.apache.commons.vfs2.*;
import org.apache.commons.vfs2.impl.DefaultFileMonitor;

import java.io.File;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * @author hanhang
 */
public class ApplicationManager {
    private static ApplicationManager instance;

    private IClassLoader jarLoader;
    private AppConfigManager configManager;

    private Map<String, IApplication> 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();
        }
    }
}

代码演示

程序启动后,控制台输出 image.pngTestApplication1的init方法修改为:

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

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

image.png 此时,TestApplication1已经重新加载。

总结

以上就是我实现🔥热部署的代码,github源码地址:github.com/hanhang6/ho…
如果觉得我写的有问题,评论区里可以留言。