「这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战」
引言
在项目开发中,每次修改文件就需要重启一次代码,这样太浪费时间了,所以在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文件
<?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();
}
}
}
代码演示
程序启动后,控制台输出
将
TestApplication1
的init方法修改为:
@Override
public void init() {
System.out.println("TestApplication1--》300");
}
重新build项目,控制台输出如下:
此时,
TestApplication1
已经重新加载。
总结
以上就是我实现🔥热部署的代码,github源码地址:github.com/hanhang6/ho…
如果觉得我写的有问题,评论区里可以留言。