Java热更新原理与代码实现
1.热更新
热更新指的是项目应用在运行的过程中,修改后的代码文件无需进行应用重启就能够被JVM加载并使用。
该方式能够提高开发效率,减少项目频繁的重启。使用IDEA的可以使用Jrebel
插件实现更加完善的项目热更新。
2.实现原理和步骤
热更新的实现主要是依赖java的类加载机制
加载需要进行热更新的类(加载的是.class文件),接着只需在使用该类的时候,用自定义的类加载器加载指定class文件,最后使用反射调用对应的方法。当我们修改文件后,只需要重新编译该文件生成新的class文件,接着就会被自定义类加载器加载使用,实现文件的热更新。
流程如下图所示:
3.实现过程
需要定义一个自定义的类加载器,该加载器可以加载项目根路径下所有的类
首先需要创建类去继承ClassLoader
并实现方法,其中重写的方法为loadClass()
- 这里要判断name是否是该项目前缀下的文件,因为该加载器只加载target目录下的类(调用
defineClass()
后Object类会使用CustomizedClassLoader
的loadClass方法加载,会导致找不到对应类报错(因为Object类在java.lang下),暂时还不知道为什么这里会加载Object类)
CustomizedClassLoader
public class CustomizedClassLoader extends ClassLoader{
private final String loadPath = Objects.requireNonNull(CustomClassLoader.class.getResource("/").getPath());//这里可以获取项目的target根目录
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
//这里要做判断,只有项目下的类可以使用该加载器加载
if(!name.startsWith("com.course")) {
return super.loadClass(name);
}
final String s = name.replaceAll("\\.", "/"); //com.xx.xxx -> com/xx/xxx
InputStream stream = null;
try {
stream = new FileInputStream(loadPath + s + ".class");
final int len = stream.available();
final byte[] bytes = new byte[len];
stream.read(bytes);
return defineClass(name, bytes,0 , len); //将byte[]转化为class对象
} catch (IOException e) {
e.printStackTrace();
throw new GlobalException("读取资源时出现异常");
} finally {
try {
if(stream != null){
stream.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
接下来是主逻辑,这里加载类之后通过反射调用对应的test1方法
public class Test {
public static void main(String[] args) {
try {
while(Boolean.TRUE) {
final CustomClassLoader loader1 = new CustomClassLoader();
final Class<?> clazz = loader1.loadClass("com.course.reflect.HotDeploy");
final Method method = clazz.getMethod("test1"); //反射执行对应的方法
final Object o = clazz.getConstructor().newInstance();
method.invoke(o);
Thread.sleep(20000); //暂停20s
Runtime.getRuntime().exec("cmd /c mvn compile");//编译项目中的文件
}
} catch (Exception e) {
e.printStackTrace();
throw new GlobalException(e.getMessage());
}
}
}
GlobalException
是自定义的全局异常
定义一个用于测试的HotDeploy
类如下
public class HotDeploy {
public void test1 () {
System.out.println("还没修改后的方法");
}
}
接着启动程序,每20s控制台打印如下
还没修改后的方法
还没修改后的方法
还没修改后的方法
接着修改HotDeploy
文件并保存(IDEA会自动保存)
public class HotDeploy {
public void test1 () {
//System.out.println("还没修改后的方法");
System.out.println("修改后的方法");
}
}
控制台打印结果如下
还没修改后的方法
还没修改后的方法
还没修改后的方法
修改后的方法
从结果可以看出,无需重新启动项目就能够实现类文件方法执行逻辑的更新
4.结论思考
-
上面使用
mvn complie
直接编译整个项目的方法能否更加简化,只对修改后的文件进行重编译即可。可以使用项目路径 + 类的全限定名
得到类所在的目录,再将其复制到target对应的目录中,调用javac进行编译 -
通过反射调用方法时是硬编码,要解决这个问题,可以使用Aop对特定的类/方法进行增强。无法用原本的类型对反射得到的class对象进行类型转换(如下)
HotDeploy hot = (HotDeploy) loader.loadClass("com.course.reflect.HotDeploy");
因为HotDeploy引用是
AppliacionClassLoader
加载器加载的,在程序启动的时候就已经扫描加载了,因此类加载器不同,这两个就不是同一个类,进行强制转换会报强转异常使用AOP这种方式感觉还是有点臃肿,每个方法都去增强使用反射再调用对应的方法。有许多的文件并没有进行修改但也使用了使用了反射,浪费性能。感觉又可以启动一个定时任务监听文件的是否被修改了(根据上次修改时间和监听间隔)如果修改了就放入一个集合中,就可以实现修改过的文件才使用反射调用方法
以上都是自己学习这部分内容后的一些想法和问题,如果有更好的实现思路或者解决方法可以给我一些建议哈!
谢谢阅读!