徒手?jǐn)]一個(gè)熱部署插件!
你知道的越多,不知道的就越多,業(yè)余的像一棵小草!
你來(lái),我們一起精進(jìn)!你不來(lái),我和你的競(jìng)爭(zhēng)對(duì)手一起精進(jìn)!
編輯:業(yè)余草
juejin.cn/post/7031051782939738125
推薦:https://www.xttblog.com/?p=5290
引言
在項(xiàng)目開(kāi)發(fā)中,每次修改文件就需要重啟一次代碼,這樣太浪費(fèi)時(shí)間了,所以在IDEA中使用JRebel插件實(shí)現(xiàn)項(xiàng)目??熱部署,可自動(dòng)熱部署,無(wú)需重啟項(xiàng)目。雖然一直清楚熱部署是打破雙親委派來(lái)實(shí)現(xiàn)的,但是一直沒(méi)有手寫(xiě)過(guò)熱部署代碼,今天寫(xiě)一次。??
雙親委派機(jī)制
了解熱部署之前,首先需要知道什么是雙親委派,在 IDE 中寫(xiě)的代碼最終經(jīng)過(guò)編譯器會(huì)形成 .class 文件,由 classLoader 加載到 JVM 中執(zhí)行。
JVM 中提供了三層的 ClassLoader:
Bootstrap classLoader:主要負(fù)責(zé)加載核心的類(lèi)庫(kù)(java.lang.*等),構(gòu)造ExtClassLoader和APPClassLoader。 ExtClassLoader:主要負(fù)責(zé)加載jre/lib/ext目錄下的一些擴(kuò)展的jar。 AppClassLoader:主要負(fù)責(zé)加載應(yīng)用程序的主函數(shù)類(lèi)
加載過(guò)程圖如下:

實(shí)現(xiàn)熱部署思路
一個(gè)類(lèi)一旦被JVM加載過(guò),就不會(huì)再次被加載。想實(shí)現(xiàn)熱部署,就需要在.class文件修改后,由classLoader重新加載修改的.class文件。對(duì).class文件做監(jiān)聽(tīng),一旦文件修改,則重新加載類(lèi)。
在此實(shí)現(xiàn)中用一個(gè)Map模擬JVM已經(jīng)加載過(guò)的.class文件,當(dāng)監(jiān)聽(tīng)到文件內(nèi)容修改之后,移除Map中舊的.class文件,將新的.class文件加載并存放至Map中,調(diào)用init方法,執(zhí)行初始化動(dòng)作,模擬.class文件已經(jīng)加載到JVM虛擬機(jī)中。
下面講代碼實(shí)現(xiàn)!
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接口,所有監(jiān)聽(tīng)的類(lèi)都實(shí)現(xiàn)自這個(gè)接口。
public?interface?IApplication?{
????/**
?????*?初始化
?????*/
????void?init();
????/**
?????*?執(zhí)行
?????*/
????void?execute();
????/**
?????*?銷(xiāo)毀
?????*/
????void?destroy();
}
TestApplication1
監(jiān)聽(tīng)加載的類(lèi)。
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
類(lèi)加載器,實(shí)現(xiàn)通過(guò)包掃描類(lèi)的功能。
public?interface?IClassLoader?{
????/**
?????*?創(chuàng)建classLoader
?????*?@param?parentClassLoader?父classLoader
?????*?@param?paths?路徑
?????*?@return?類(lèi)加載器
?????*/
????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配置類(lèi)
@Data
public?class?AppConfigList?{
????private?List?configs;
????@Data
????public?static?class?AppConfig{
????????private?String?name;
????????private?String?file;
????}
}
GlobalSetting 全局配置類(lèi)
public?class?GlobalSetting?{
????public?static?final?String?APP_CONFIG_NAME?=?"application.xml";
????public?static?final?String?JAR_FOLDER?=?"com/hanhang/app/";
}
application.xml配置
通過(guò)xml配置加后面的解析,確定監(jiān)聽(tīng)那個(gè)class文件。
<apps>
????<app>
????????<name>TestApplication1name>
????????<file>com.hanhang.app.TestApplication1file>
????app>
apps>
JarFileChangeListener 監(jiān)聽(tīng)器
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
此類(lèi)為config的管理類(lèi),用于加載配置。
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
此類(lèi)管理已經(jīng)在Map中加載的類(lèi),并且添加監(jiān)聽(tīng)器,實(shí)現(xiàn)class文件修改后的監(jiān)聽(tīng)重新加載工作。
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();
????????}
????}
????/**
?????*?加載類(lèi)
?????*?@param?basePath?根目錄
?????*/
????public?void?loadAllApplications(String?basePath){
????????for(AppConfigList.AppConfig?config?:?this.configManager.getConfigs()){
????????????this.createApplication(basePath,?config);
????????}
????}
????/**
?????*?初始化監(jiān)聽(tīng)器
?????*?@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();
????????}
????}
????/**
?????*?根據(jù)配置加載類(lèi)
?????*?@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?類(lèi)名
?????*/
????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;
????}
????/**
?????*?獲取類(lèi)
?????*?@param?name?類(lèi)名
?????*?@return?緩存中的類(lèi)
?????*/
????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
測(cè)試類(lèi),創(chuàng)建一個(gè)線程,讓程序一直監(jiān)聽(tīng)文件修改。
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();
????????}
????}
}
代碼演示
程序啟動(dòng)后,控制臺(tái)輸出。

將TestApplication1的 init 方法修改為:
@Override
public?void?init()?{
????System.out.println("TestApplication1--》300");
}
重新build項(xiàng)目,控制臺(tái)輸出如下:

此時(shí),TestApplication1已經(jīng)重新加載。以上就是我實(shí)現(xiàn)??熱部署的關(guān)鍵代碼,如需完整代碼,加微信:xttblog2,免費(fèi)獲??!
