手写热部署

「这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

引言

在项目开发中,每次修改文件就需要重启一次代码,这样太浪费时间了,所以在IDEA中使用JRebel插件实现项目🔥热部署,可自动热部署,无需重启项目。虽然一直清楚热部署是打破双亲委派来实现的,但是一直没有手写过热部署代码,今天写一次。😁

双亲委派机制

了解热部署之前,首先需要知道什么是双亲委派,在IDE中写的代码最终经过编译器会形成.class文件,由classLoader加载到JVM中执行。

JVM中提供了三层的ClassLoader:

  • Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
  • ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。
  • AppClassLoader:主要负责加载应用程序的主函数类
    加载过程图如下:
    Untitled-2021-11-11-1524.png

实现热部署思路

一个类一旦被JVM加载过,就不会再次被加载。想实现热部署,就需要在.class文件修改后,由classLoader重新加载修改的.class文件。对.class文件做监听,一旦文件修改,则重新加载类。
在此实现中用一个Map模拟JVM已经加载过的.class文件,当监听到文件内容修改之后,移除Map中旧的.class文件,将新的.class文件加载并存放至Map中,调用init方法,执行初始化动作,模拟.class文件已经加载到JVM虚拟机中。

代码实现

pom文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
xml复制代码<?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接口,所有监听的类都实现自这个接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public interface IApplication {
/**
* 初始化
*/
void init();

/**
* 执行
*/
void execute();

/**
* 销毁
*/
void destroy();
}

TestApplication1

监听加载的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码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

类加载器,实现通过包扫描类的功能

1
2
3
4
5
6
7
8
9
java复制代码public interface IClassLoader {
/**
* 创建classLoader
* @param parentClassLoader 父classLoader
* @param paths 路径
* @return 类加载器
*/
ClassLoader createClassLoader(ClassLoader parentClassLoader, String...paths);
}

SimpleJarLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
java复制代码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配置类

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Data
public class AppConfigList {
private List<AppConfig> configs;

@Data
public static class AppConfig{
private String name;

private String file;
}
}

GlobalSetting 全局配置类

1
2
3
4
java复制代码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文件。

1
2
3
4
5
6
xml复制代码<apps>
<app>
<name>TestApplication1</name>
<file>com.hanhang.app.TestApplication1</file>
</app>
</apps>

JarFileChangeListener 监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码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的管理类,用于加载配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
java复制代码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文件修改后的监听重新加载工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
java复制代码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

测试类,创建一个线程,让程序一直监听文件修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码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();
}
}
}

代码演示

程序启动后,控制台输出
image.png
TestApplication1的init方法修改为:

1
2
3
4
java复制代码@Override
public void init() {
System.out.println("TestApplication1--》300");
}

重新build项目,控制台输出如下:

image.png
此时,TestApplication1已经重新加载。

总结

以上就是我实现🔥热部署的代码,github源码地址:github.com/hanhang6/ho…

如果觉得我写的有问题,评论区里可以留言。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%