“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 output 将TestApplication1
Change 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,TestApplication1
It 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.