徒手撸一个热部署插件!
共 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,免费获取!