“This is the 8th day of my participation in the Gwen Challenge in November. Check out the details: The Last Gwen Challenge in 2021”

The introduction

During project development, you need to restart the code every time you modify the file, which is a waste of time, so you use the JRebel plug-in in IDEA to implement the project 🔥 hot deployment, which can be automatically hot deployed without restarting the project. Although it has always been clear that hot deployment is achieved by breaking parental delegation, I have not written the code for hot deployment. I will write it again today. 😁

Parent delegation mechanism

Before you understand hot deployment, you need to know what parental delegation is. Code written in the IDE is eventually produced by the compiler as a.class file that is loaded by the classLoader into the JVM for execution. The JVM provides three layers of classloaders:

  • Bootstrap classLoader: Mainly responsible for loading the core class libraries (java.lang.*, etc.) and constructing ExtClassLoader and APPClassLoader.
  • ExtClassLoader: is responsible for loading some extension jar in the jre/lib/ext directory.
  • AppClassLoader: The main function class that loads the application

The loading process is shown as follows:

Implement hot deployment

Once a class has been loaded by the JVM, it will never be loaded again. To implement hot deployment, you need to reload the modified. Class file by classLoader after the modification. Listen for.class files and reload classes if the files are modified. In this implementation, a Map is used to simulate the.class file that has been loaded by the JVM. After listening for changes in the file content, the old.class file is removed from the Map, the new.class file is loaded and stored in the Map, and the init method is called to initialize the file. The mock. Class file has been loaded into the JVM virtual machine.

Code implementation

Pom file


      
<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 the 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>
Copy the code

IApplication interface

Define the IApplication interface from which all listening classes are implemented.

public interface IApplication {
    /** * initializes */
    void init(a);

    /** * Execute */
    void execute(a);

    /** * destroy */
    void destroy(a);
}
Copy the code

TestApplication1

Listen for loaded classes

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

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

    @Override
    public void destroy(a) {
        System.out.println("TestApplication1 -" destroy"); }}Copy the code

IClassLoader

Class loader, realize the function of scanning classes through packages

Public interface IClassLoader {/** * create classLoader * @param parentClassLoader parentClassLoader * @param paths * @return ClassLoader createClassLoader(ClassLoader parentClassLoader, String... paths); }Copy the code

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); Private List<String> scanJarFiles(String folderPath) {private List<String> scanJarFiles(String folderPath) { List<String> jars = new ArrayList<>(); File folder = new File(folderPath); if (! Folder.isdirectory ()) {throw new RuntimeException(" Scanned path does not exist, 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; }}Copy the code

AppConfigList configuration class

@Data
public class AppConfigList {
    private List<AppConfig> configs;

    @Data
    public static class AppConfig{
        private String name;

        privateString file; }}Copy the code

GlobalSetting Global configuration class

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

Application. The XML configuration

Determine which class file to listen on through XML configuration and subsequent parsing.

<apps>
    <app>
        <name>TestApplication1</name>
        <file>com.hanhang.app.TestApplication1</file>
    </app>
</apps>
Copy the code

JarFileChangeListener listener

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); }}Copy the code

AppConfigManager

This class is the management class of Config and is used to load the configuration.

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(a){
        configs = new ArrayList<>();
    }

    /** * Load configuration *@paramPath the 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(newArrayList<>(configList.getConfigs())); }}catch(FileNotFoundException e) { e.printStackTrace(); }}/** * Get the XML configuration definition *@return XStream
     */
    private XStream getXmlDefine(a){
        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)){
                returnconfig; }}return null; }}Copy the code

ApplicationManager

This class manages classes that have been loaded in the Map and adds listeners to listen for reloading after class file changes.

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(a){}public void init(a){
        jarLoader = new SimpleJarLoader();
        configManager = new AppConfigManager();
        apps = new HashMap<>();

        initAppConfigs();

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

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

        initMonitorForChange(basePath.getPath());
    }

    /** * Initialize the configuration */
    public void initAppConfigs(a){

        try {
            URL path = this.getClass().getClassLoader().getResource(GlobalSetting.APP_CONFIG_NAME);
            configManager.loadAllApplicationConfigs(Objects.requireNonNull(path).toURI());
        } catch(URISyntaxException e) { e.printStackTrace(); }}/** * Load class *@paramBasePath Root directory */
    public void loadAllApplications(String basePath){

        for(AppConfigList.AppConfig config : this.configManager.getConfigs()){
            this.createApplication(basePath, config); }}/** * Initializes the listener *@paramBasePath path * /
    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(); }}/** * Load classes according to configuration *@paramBasePath path *@param* / config configuration
    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(); }}/** * reload *@paramName the name of the class * /
    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(a){
        if(instance == null){
            instance = new ApplicationManager();
        }
        return instance;
    }

    /** * get class *@paramName the name of the class@returnClass */ in the cache
    public IApplication getApplication(String name){
        if(this.apps.containsKey(name)){
            return this.apps.get(name);
        }
        return null;
    }

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

MainTest

Test class, create a thread, let the program always listen for file changes.

public static void main(String[] args){

    Thread t = new Thread(new Runnable() {

        @Override
        public void run(a) { ApplicationManager manager = ApplicationManager.getInstance(); manager.init(); }}); t.start();while(true) {try {
            Thread.sleep(300);
        } catch(InterruptedException e) { e.printStackTrace(); }}}Copy the code

Code demo

After the program starts, console outputTestApplication1Change the init method to:

@Override
public void init(a) {
    System.out.println("TestApplication1 -" 300");
}
Copy the code

Rebuild the project and the console output looks like this:

At this point,TestApplication1It has been reloaded.

conclusion

The above is my implementation of 🔥 hot deployment code, github source address: github.com/hanhang6/ho… If you have a problem with what I’ve written, leave a comment in the comments section.