一、背景
某服务需要连接操作多种组件(每种组件可能有多个版本
),如kafka、mongodb、es、mysql等等,并且后续需要适配更多的组件。
主要难点:连接操作多组件多版本,且同种组件的不同版本所依赖的jar包可能不一样,操作源码也可能发生改变,项目无法直接依赖jar包,会产生类冲突
二、解决思路
由于每种组件的不同版本所依赖的jar包不同,我们可以借鉴tomcat的实现方式,通过自定义类加载器打破双亲委派机制来实现类隔离,从而达到操作多组件多版本的目的。
2.1 创建依赖所在目录
针对每一种组件我们创建一个目录,比如/data/kafka、/data/mongodb、/data/es等,且每种组件的不同版本创建对应的子目录,比如/data/kafka/0.10、/data/kafka/0.11,目录结构如下
| ----/data
| --------/kafka
| ------------/0.10
| ------------/0.11
| --------/mysql
| ------------/5.7
| ------------/8.0
| ...
把每种组件不同版本对应的依赖包放在各个子目录下面。
2.2 定义操作接口
在common公共模块中定义一个接口AbstractOperator,该接口定义一些通用方法,如下:
public interface Operator {
/**
* 测试连接
* @param connectionInfo
* @return
*/
boolean testConnection(String connectionInfo);
/**
* 获取组件版本
* @return
*/
String getVersion(String connectionInfo);
}
再定义各种组件的接口,如KafkaOperator、MysqlOperator等,使其继承该通用接口。组件接口内部包含一些组件自身的操作,如KafkaOperator中定义了getTopics、createTopic、deleteTopic等方法。代码如下:
public interface KafkaOperator extends Operator{
/**
* 获取topic列表
* @param connectionInfo
* @return
*/
List<String> getTopics(String connectionInfo);
/**
* 创建topic
* @param connectionInfo
* @param topic
* @return
*/
boolean createTopic(String connectionInfo, String topic);
/**
* 删除topic
* @param connectionInfo
* @param topic
* @return
*/
boolean deleteTopic(String connectionInfo, String topic);
}
2.3 编写并构建业务包
大致步骤如下:
- 针对每种组件的不同版本,可以在项目下新建一个模块,该模块依赖common公共模块
- 创建入口类com.kamier.Entry(所有组件的不同版本的入口类的全限定名统一为com.kamier.Entry),并实现对应的组件接口,比如Kafka的0.10版本,那么就实现KafkaOperator接口。
- 编写业务逻辑代码
public class Entry implements KafkaOperator {
@Override
public List<String> getTopics(String connectionInfo) {
return null;
}
@Override
public boolean createTopic(String connectionInfo, String topic) {
return false;
}
@Override
public boolean deleteTopic(String connectionInfo, String topic) {
return false;
}
@Override
public boolean testConnection(String connectionInfo) {
return false;
}
@Override
public String getVersion(String connectionInfo) {
return null;
}
}
- 打成jar包
- 将jar包放在对应的目录下,与依赖包同级,如/data/kafka/0.10
2.4 自定义类加载器
经过前面的准备工作,组件的每个版本的目录下都有了相应的依赖包和业务包。
开始编写一个自定义类加载器继承URLClassLoader,重写loadClass方法,优先加载当前类加载器路径下的class来打破双亲委派模式,代码如下
public static class MyClassLoader extends URLClassLoader {
public MyClassLoader(URL[] urls) {
super(urls);
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 先检查当前类加载器是否已经装载该类
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 在当前类加载器的路径下查找
c = findClass(name);
} catch (ClassNotFoundException e) {
// 说明在当前类加载器的路径下没有找到
}
if (c == null) {
// 走双亲委派机制
if (getParent() != null) {
c = getParent().loadClass(name);
}
}
}
return c;
}
}
}
针对每种组件的不同版本,我们创建与其对应的自定义类加载器,并将该版本对应目录下的所有jar包(包括依赖包和业务包)的URL传入。
2.5 主流程步骤
步骤如下:
- 当我们从页面上接收到一个获取Kafka(版本为0.10)topic列表的请求时,先判断是否已经初始化过Kafka(0.10版本)的类加载器,如果还未初始化,则进行类加载器的初始化。
URL[] urls = null;
File dir = new File("/data/kafka/0.10");
if (dir.isDirectory()) {
File[] files = dir.listFiles();
urls = new URL[files.length];
for (int i = 0; i < files.length; i++) {
urls[i] = files[i].toURL();
}
}
MyClassLoader contextClassLoader = new MyClassLoader(urls);
- 通过类加载器加载入口类com.kamier.Entry并实例化,通过反射调用对应的方法(组件与其对应的方法列表可以统一维护在数据库中)。
Class loadClass = contextClassLoader.loadClass("com.kamier.Entry");
Object entry = loadClass.newInstance();
Method method = loadClass.getDeclaredMethod("getTopics");
List<String> a = (List) method.invoke(entry, 参数);
- 获取到结果并返回
三、总结
至此整个实现步骤就结束了,我们通过自定义类加载器的方式来实现类隔离,从而达到操作多组件多版本的目的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。