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类加载机制: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的时候才会去卸载多余的类