devtools 通过自动重启可以加快开发和测试验证的整个过程,开发人员不需要在频繁手动重启,每次都需要等待很长的时间才能启动起来看到效果。
devtools 表面上看来也是重启,devtools 是如何实现快速的重启呢?
当我们启动应用时,Java 需要加载大量的 jar 包,执行代码过程中,需要用到类时才去类加载器中查找类。在我们的开发过程中,频繁修改改变的是我们 IDE 中的某些项目,这些项目包含的类和整个 Java 需要加载的类相比几乎可以忽略。
devtools 就是从这点入手,将不变的 jar 和需要变化的 classes 拆分到两个不同的类加载器中,不变的这部分使用 Java 启动时的 AppClassLoader 进行加载,IDE 开发中可能会变化的这部分类使用 RestartClassLoader 进行加载,并且 RestartClassLoader 的 parent 就是 AppClassLoader。
当类发生变化时,创建新的 RestartClassLoader 替换原有的类加载器,这样就能保证大多数的 jar 都不需要重新加载,而且这部分已经创建的(不是 Spring 管理的)实例还可以继续使用。
devtools 重启过程中耗费时间的地方在 Spring 的初始化过程中,所以如果初始化过程需要很长时间的时候,devtools 的效果会大打折扣。
下图是 devtools 中类加载器相关类的关系图:
虽然这里看着有很多个类,但实际上本地开发中只会用到 RestartClassLoader 一个类,其他几个类是针对远程自动重启设计的,所以本章先不介绍,后续涉及远程重启时再说。
下面是 RestartClassLoader 和常见 ClassLoader 的关系图:
RestarterClassLoader 父子关系图:
Java 平台使用委托模型加载类。基本思想是每个类加载器都有一个“parent(父)”类加载器。载入类时,类加载器会先将类的搜寻「委派」给其父类加载器,如果找不到,再通过自身进行查找。当父加载器 parent==null 时,会使用引导类加载器进行加载,并且引导类加载器无法显式指定,只能通过 parent=null 设置。
devtools 中的 RestartClassLoader 默认情况下从 AppClassLoader 中复制(AppClassLoader 也有)了除 jar 之外的目录形式的路径,如果采用默认委托方式,RestartClassLoader 就无法起到替换类的作用,因此 RestartClassLoader 中对该机制做了调整。
RestartClassLoader 中所有和 updatedFiles 有关的代码都是远程方式中使用的,本地开发使用时,updatedFiles.size()==0,并且永远不会有内容,因此下面代码中都会忽略这部分逻辑。
下面是关键方法的代码(说明看注释):
@Override
public Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
//忽略下面5行代码,本地情况下 file == null
String path = name.replace('.', '/').concat(".class");
ClassLoaderFile file = this.updatedFiles.getFile(path);
if (file != null && file.getKind() == Kind.DELETED) {
throw new ClassNotFoundException(name);
}
//同步方法,避免重复加载
synchronized (getClassLoadingLock(name)) {
//先看是否加载过,如果存在就直接返回
Class> loadedClass = findLoadedClass(name);
if (loadedClass == null) {
try {
//这里没有先从父类获取,而且从当前类加载器进行加载
loadedClass = findClass(name);
}
catch (ClassNotFoundException ex) {
//只有当前类加载器找不到时,才通过父加载器加载
loadedClass = getParent().loadClass(name);
}
}
if (resolve) {
resolveClass(loadedClass);
}
return loadedClass;
}
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
//忽略下面两行代码,默认 file == null
String path = name.replace('.', '/').concat(".class");
final ClassLoaderFile file = this.updatedFiles.getFile(path);
if (file == null) {
//本地开发时,会调用这里的方法查找类
//super父类中会从RestartClassLoader 构造方法传入的 URL[] urls 中查找
return super.findClass(name);
}
//远程情况下才会执行下面的代码
if (file.getKind() == Kind.DELETED) {
throw new ClassNotFoundException(name);
}
return AccessController.doPrivileged((PrivilegedAction>) () -> {
byte[] bytes = file.getContents();
return defineClass(name, bytes, 0, bytes.length);
});
}
RestartClassLoader 通过这种简单的方式就实现了用当前类替换 AppClassLoader 加载类的功能。当后续监控路径中的文件发生变化时,就会使用新的 RestartClassLoader 替换原有的类加载器,所有 RestartClassLoader 都使用了同一个 AppClassLoader 作为 parent 加载器。
RestartClassLoader 类加载器的设计是比较简单的,整个 devtools 中,除了类路径的监控外,剩下最重要的就是如何控制程序自动重启并且使用新的类加载器加载。下一篇博客继续介绍。