JVM虚拟机笔记(3)-- 类加载器实践热部署
1 需求
spring boot项目通过上传jar包的方式,自动加载jar包中的类。应用场景:用户上传驱动实现,服务实现热部署。
2 实现过程
要实现热部署功能,我们首先需要实现两个基础功能:1 加载指定路径的class文件 2 指定路径下的文件有变动时,触发事件,重新加载
2.1 多jar包应用实现
2.1.1 类加载设计
1 需要加载其他路劲下的jar包,只能由代码自身来引入一个类加载器,指定加载某一路径下的jar包,并且在没有特殊情况下不破坏双亲委派机制。
2 ide上运行的程序,是使用的jvm默认的类加载机制,从main函数启动,但是spring boot的可运行jar包,是在原有的基础上再包装了一层启动配置。并引入了自己的类加载器来处理这个比较特殊的jar包。
(springboot类加载机制:https://segmentfault.com/a/1190000013532009,http://hengyunabc.github.io/spring-boot-classloader/)

图1 ide下类加载图和可执行jar包下类加载图
3 思考一下下图所示的类加载图能否实现需求?

图2 可执行jar包下破坏双亲委派的类加载图
说明:这类加载图是我们在实现热部署需求时,比较易出现的现象,需要先对spring boot可执行jar的类加载机制有个初步认识。
解释: 这个类加载模式只有在self classLoader 和主程序不存在共用业务类时,可以正常使用(self classLoader和主程序业务完全独立),但是目前的需求下不能做到独立,所以该模式会有问题
2.1.2 代码实践
获取加载指定路径的类加载器,特别注意:新建类加载器对象时,需要指定其父类加载器,如果不指定:classLoader = new URLClassLoader(urls),默认为appClassLoader,这样就会造成图2 所示的的情形
public class ClassLoaderContext { public static String DEFAULT_PATH = "lib"; private static ClassLoader classLoader; .... public static ClassLoader getClassLoader() { if (null == classLoader) { String libPath = FileUtil.getDefaultPath(DEFAULT_PATH); List<File> files = pathToJarFile(libPath); URL[] urls = fileToUrl(files); // classLoader = new URLClassLoader(urls); classLoader = new URLClassLoader(urls, ClassLoaderContext.class.getClassLoader()); } return classLoader; } }
由指定路径的类加载来加载class,调用业务逻辑
//获取类加载器 ClassLoader cloader = ClassLoaderContext.getClassLoader(); String result = null; try { Class clazz = cloader.loadClass("com.example.timyag.service.impl.TestServiceImpl"); result = ((ITestService)clazz.newInstance()).sayHello(p1); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } return result;
由于接口 ITestService类文件在两个jar包中对会存在(不同时存在的话,编译都不过了,不多说看代码)。所以在图2 这种类加载图下,双亲委派被破坏,ITestService类被加载了两次,分别被加载在LaunchedURLClassLoader和self ClassLoader里,导致代码 “((ITestService)clazz.newInstance())” 就会报错,因为实例对象(clazz.newInstance())不是LaunchedURLClassLoader加载的ITestService接口的实现类,强制转换失败。
2.2 路径监听
系统热部署需要感知路径下文件的变化,及时重新加载类文件。我们使用 JDK 1.7提供的WatchService,利用底层文件系统提供的功能
参考:
http://www.oudahe.com/p/39580/
https://docs.oracle.com/javase/tutorial/essential/io/notification.html#concerns
主要业务代码:
在监听到路径下文件有变化时,通知订阅者。
public class PathWatcher extends Observable { @Getter private String watchPath; public long lastMod; public PathWatcher(String watchPath) { this.watchPath = watchPath; } public void doWatch() { try { final Path path = FileSystems.getDefault().getPath(watchPath); final WatchService watchService = FileSystems.getDefault().newWatchService(); path.register(watchService, ENTRY_CREATE, ENTRY_MODIFY); boolean UPDATED = false; while (true) { final WatchKey wk = watchService.take(); for (WatchEvent<?> event : wk.pollEvents()) { WatchEvent.Kind<?> kind = event.kind(); if (kind == OVERFLOW) { continue; } Path changed = (Path) event.context(); Path absolute = path.resolve(changed); File configFile = absolute.toFile(); long lastModified = configFile.lastModified(); // 利用文件时间戳,防止触发两次 if (lastModified != lastMod && configFile.length() > 0) { // 保存上一次时间戳 lastMod = lastModified; UPDATED = true; } } if (UPDATED) { System.out.println("path file changed"); setChanged(); //通知订阅者 notifyObservers(); UPDATED = false; } // reset the key boolean valid = wk.reset(); if (!valid) { System.out.println("watch key invalid!"); break; } //10秒刷新一次 Thread.sleep(10 * 1000); } } catch (Exception e) { e.printStackTrace(); } } }
2.3 类重新加载
在更新了jar包之后,我们需要重新加载class类,实现起来很简单,只需要新建一个类加载器。
主逻辑:在接受到发布者发布的文件变更通知时,将静态的classLoader置为null。下载使用该类加载器时,就能新建一个对象。
public void reloadClassLoaderAndClass(String path) { System.out.println("reload class"); classLoader = null; // getNewClassLoader(path); } @Override public void update(Observable o, Object arg) { reloadClassLoaderAndClass(((PathWatcher) o).getWatchPath()); }
3 小结
git项目地址:https://gitee.com/guijiaoqqq/spring-boot-hot-deploy 分支:hot_deploy_simple
后续优化点:
1 外部jar包类的使用,需要在主程序中指明类名(Class clazz = cloader.loadClass("com.example.timyag.service.impl.TestServiceImpl");),这个耦合性太大,参考jdbc的驱动引入方式,可以使用SPI机制,由外部jar包来指定实现类类名。
2 外部jar包类在使用时,不能总是实例化一个对象来使用, 希望能将实例对象注入到spring中,后续使用都有spring容器来管理,在热部署重新加载类时,又要及时更新注入的实例对象
3 如果热部署频率高,那么加载进的类会越来越多,我们需要及时的去卸载。但是我们没有办法直接控制它的卸载,只能到jvm触发GC的时候才会去卸载多余的类

浙公网安备 33010602011771号