热加载是指在不重启服务的情况下使更改的代码生效。注意和热部署的区别,热加载主要是在开发环境下使用。

首先要知道Java程序是怎么运行起来的,Java类加载分为其7个阶段。

phase

其中加载阶段是用户可以自定义,而验证阶段、准备阶段、解析阶段、初始化阶段都是用 JVM 来处理的。
整个类加载是在Java 中一个叫做类加载器上进行的,如果我们能程序更改后,让程序所在的进程能够实时的获取到编译后的Class类字节码信息,然后重新加载的话,那么就可以实现热加载功能。

Java 类加载器

类加载器,顾名思义就是加载Java类的工具,Java默认设置了三个类加载器。

  • BootstrapClassloader
  • ExtClassloader
  • AppClassloader

BootstrapClassloader 叫做启用类加载器,用于加载JRE核心类库,使用C++实现。加载路径%JAVA_HOME%/lib下的所有类库。

ExtClassloader 扩展类加载器,加载%JAVA_HOME%/lib/ext中的所有类库。

AppClassloader 应用类加载器也叫系统类加载器System Classloader,加载%CLASSPATH%路径下的所有类库。

Java 也提供了扩展,可以让我们自己实现类加载的功能。类加载器在Java中是java.lang.ClassLoader这个类,如果要自定义类加载器,只要实现这个类,重写加载方法就好了。

在Java中,由不同的类加载器加载的两个相同的类在Java虚拟机里是两个不同的类,那么Java是怎么确保一个类不会被多个类加载器重复加载,并且保证核心API不会被篡改的呢?

这就需要Java的双亲委派机制。

双亲委派机制

classloader

当一个类加载器接到加载类的请求后,首先会交给父类去加载,如果所有父类都无法加载,自己加载,并将被加载的类缓存起来。。

每加载一个类,所有的类加载器都会判断是否可以加载,最终会委托到启动类加载器来首先加载。所有的类的加载都尽可能由顶层的类加载器加载,这样就保证了加载的类的唯一性。

启动类加载器、扩展类加载器、应用程序类加载器,都有自己加载的类的范围,因此并不是所有的类父类都可以加载。

Java 类加载器中还有一个全盘委托机制,当指定一个ClassLoader加载一个类时,该类所依赖或者引用的类也会由这个类加载器来加载,除非显示的用别的类加载器加载。

比如:程序入口默认用的是AppClassloader,那么以后创建出来的类也是用AppClassloader来加载,除非自己显示的用别的类加载器去加载。

热加载

OK,有了以上铺垫,现在可以来实现热加载的功能了,怎么实现呢?

1、首先要实现自己的类加载器,破坏双亲委派机制。
2、通过自定义的类加载器加载所需的类。
3、不断的轮询判断类是否有变化,如果有变化重新加载。

自定义类加载器

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
public class MyClassLoader extends ClassLoader {


private static final String SUFFIX = ".class";

private String rootPath;

public MyClassLoader(String rootPath) {
this.rootPath = rootPath;
}

/**
* 破坏双亲委派机制,自定义类加载方式
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> loadedClass = findLoadedClass(name);
if (null == loadedClass) {
try {
return findClass(name);
} catch (ClassNotFoundException e) {
return super.loadClass(name);
}
}

return loadedClass;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = rootPath + name.replace(".", "/") + SUFFIX;
File file = new File(path);
byte[] classBytes = null;
try {
classBytes = getClassBytes(file);
} catch (Exception e) {
}
if (null != classBytes) {
if (null != super.findLoadedClass(name)) {
return super.findLoadedClass(name);
}
Class<?> aClass = defineClass(name, classBytes, 0, classBytes.length);
if (null != aClass) {
return aClass;
}
}
return super.findClass(name);
}

/**
* 加载类
* @param file
* @return
* @throws Exception
*/
private byte[] getClassBytes(File file) throws Exception {
FileInputStream fileInputStream = new FileInputStream(file);
FileChannel fc = fileInputStream.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel writableByteChannel = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024);
while (true) {
int read = fc.read(by);
if (read == 0 || read == -1) {
break;
}
by.flip();
writableByteChannel.write(by);
by.clear();
}
fileInputStream.close();
return baos.toByteArray();
}
}

自定义类加载器重写了loadClass()方法和findClass方法,破坏了Java的双亲委派机制,先通过自定义的类加载所需的类,如果加载不到,再交给父类加载。

接下来,写启动器

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
public class Run {

public static String rootPath;

public static void run(Class cl) {
rootPath = cl.getClass().getResource("/").getPath();
MyClassLoader myClassLoader = new MyClassLoader(rootPath);
startFileListener(rootPath);
start0(myClassLoader);
}

public static void startFileListener(String rootPath) {
FileAlterationObserver fileAlterationObserver = new FileAlterationObserver(rootPath);
fileAlterationObserver.addListener(new FileListener());
FileAlterationMonitor fileAlterationMonitor = new FileAlterationMonitor(5);
fileAlterationMonitor.addObserver(fileAlterationObserver);
try {
fileAlterationMonitor.start();
} catch (Exception e) {
e.printStackTrace();
}
}


public static void start0(MyClassLoader classLoader) {
Class<?> clazz = null;
try {
clazz = classLoader.findClass("com.example.Run");
clazz.getMethod("start").invoke(clazz.newInstance());
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 模拟启动应用程序
*/
public static void start() {
Application application = new Application();
application.printApplicationName();
}
}

run() 方法是入口,首先自定义了加载器,然后设置了文件监听,这个文件监听用的是commons-io

1
2
3
4
5
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>

最后调用start0来启动项目,在start0里通过自定义的类加载器又重新加载了Run本身,然后通过反射调用start()方法,start()方法里启动真正的项目。这样的目的是因为类加载器中的全盘委托机制。Java 默认用的是AppClassloader,所以只能显示的通过自定义的类加载器来加载启动类,再启动真正的项目。

文件的监听,使用commons-io

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
public class FileListener extends FileAlterationListenerAdaptor {

@Override
public void onFileCreate(File file) {
System.out.println(file.getName());
if (file.getName().indexOf(".class") != -1) {

try {
MyClassLoader myClassLoader = new MyClassLoader(Run.rootPath);
Run.start0(myClassLoader);
} catch (Exception e) {
e.printStackTrace();
}
}
super.onFileCreate(file);
}


@Override
public void onFileChange(File file) {
System.out.println(file.getName());
if (file.getName().indexOf(".class") != -1) {

try {
MyClassLoader myClassLoader = new MyClassLoader(Run.rootPath);
Run.start0(myClassLoader);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

通过监听文件的创建和修改,如果文件有变化,定义一个新的类加载器,重新运行项目。

模拟的真正项目

1
2
3
4
5
6
7
public class Application {

public void printApplicationName() {
System.out.println("应用程序777");
}
}

好了,现在来测试一下项目

1
2
3
4
5
6
public class Main {

public static void main(String[] args) {
Run.run(Main.class);
}
}

设置一下idea,让编译器可以自动编译
idea

现在修改ApplicationprintApplicationName输出的内容,等编译器编译完后,可以看到修改的内容了。

本文只是一个供学习使用的简单小小的例子,项目github地址:https://github.com/yaocl0/hot-loading