热加载是指在不重启服务的情况下使更改的代码生效。注意和热部署的区别,热加载主要是在开发环境下使用。
首先要知道Java程序是怎么运行起来的,Java类加载分为其7个阶段。
其中加载阶段是用户可以自定义,而验证阶段、准备阶段、解析阶段、初始化阶段都是用 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的双亲委派机制。
双亲委派机制
当一个类加载器接到加载类的请求后,首先会交给父类去加载,如果所有父类都无法加载,自己加载,并将被加载的类缓存起来。。
每加载一个类,所有的类加载器都会判断是否可以加载,最终会委托到启动类加载器来首先加载。所有的类的加载都尽可能由顶层的类加载器加载,这样就保证了加载的类的唯一性。
启动类加载器、扩展类加载器、应用程序类加载器,都有自己加载的类的范围,因此并不是所有的类父类都可以加载。
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; }
@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); }
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,让编译器可以自动编译
现在修改Application
里printApplicationName
输出的内容,等编译器编译完后,可以看到修改的内容了。
本文只是一个供学习使用的简单小小的例子,项目github地址:https://github.com/yaocl0/hot-loading