前言
从23年底开始维护自己的开源项目,异构数据源流转系统datalinkx,系统支持不同数据源之间的数据同步,包括mysql、oracle、es、redis等等。
有一天一个同学问我说datalinkx系统支不支持用户上传自己的自定义数据源插件,不用动datalinkx的源码即可让系统支持用户自定义数据源的读写同步呢。
我一听有点意思,一般我都是版本迭代后支持某个数据源的读写,能不能把这个能力开放出去呢,这样大家使用的时候就不需要等系统更新,上传符合规范的插件包就可以。
也就是说我们需要实现,用户只需要将其他数据源的同步jar包,放到某个文件夹下,系统就会自动扫描到这个jar包从而获得对应数据源的读写能力。
SPI
通常听到插件化就会下意识的想起SPI,SPI是JDK内置的一种类似服务提供发现的机制,比如java.sql.Driver接口,其他不同的数据源厂商可以针对这一接口做出不同的实现, MySQL和PostgreSQL分别有不同的实现提供给用户,利用SPI机制可以为某个接口寻找服务实现。
主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,核心思想就解耦,具体实现可以参考jdbc的实现。
思考下利用SPI可以实现插拔式插件开发吗,似乎不可以,使用SPI必须在当前classpath中,也就是说虽然解耦了,但必须存在依赖,比如你想要让系统支持PG,必须要在项目中引入PG驱动依赖才可以加载到PG驱动内的相关类。
这样还是要修改系统源代码才能得到支持,这明显是不符合我们的预期的。
自定义加载器
我们想要的效果是用户可以开发一个独立的插件jar包,放到datalinkx的driver-dist目录下,datalinkx即可获得独立插件jar包中的能力。
看到这里有准备面试的jym心里默背一遍java类加载过程和双亲委派机制,背不下来的看完赶紧补上。我们知道一个JVM进程正常情况下只能加载当前classpath下的所有java类。
有没有通过扫描固定路径下的jar包,将外部java类加载当当前classpath的方式呢。有的兄弟,有的。
URLClassLoader
// 从driver-dist文件夹下读取各个数据源插件
List<URL> dataSourcePlugins = JarLoaderUtil.loadJarsFromDirectory(DRIVER_DIST);
String targetJarName = String.format("datalinkx-driver-%s-1.0.0.jar", dsType.toLowerCase());
String targetClassName = getDriverClass(dsType);
URL targetJarUrl = dataSourcePlugins.stream()
.filter(plugin -> plugin.getPath().endsWith(targetJarName))
.findFirst()
.orElseThrow(() -> new DatalinkXServerException(StatusCode.DRIVER_LOAD_FAIL, "系统未支持该数据源类型"));
// 使用自定义的 URLClassLoader 加载目标 JAR 包中的类
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{targetJarUrl}, Thread.currentThread().getContextClassLoader());
Class<?> clazz = Class.forName(targetClassName, true, urlClassLoader);
我们总结一下具体做了啥:
- 各个数据源模块打包后都会自动复制到datalinkx/driver-dist中
- 通过使用自定义 URLClassLoader加载器加载driver-dist文件夹内的jar包
- 通过class.forName反射的方式生成对用对象
这样我们就可以在当前进程中加载到外部jar包中的类,也就实现了Java插拔式加载用户自定义插件功能。