背景
在小小的公司里面,挖呀挖呀挖。最近又挖到坑里去了。一个稳定运行多年的应用,需要在里面支持多个版本的中间件客户端;而多个版本的客户端在一个应用里运行时会有同名类冲突的矛盾。在经过询问chatGPT,百度,google,github,和各位大佬的文章后,进行了总结。大概有以下几种解决方案
业界方案1----更改类路径
低版本客户端,更改类路径;然后重新打包编译客户端;这样不同版本客户端,使用的类名就不同了
此解决方案
优点
1、适合简单的小项目:无需编写新代码
2、可能是最快的一种方式;只需要把代码下载下来;更改clients下的类路径,重新编译即可;属于不怎么费脑力,但有点费体力的方式。
缺点
1、遇到版本升级,需要把步骤2的过程重新人肉再来一遍。这个比较的那啥。。。。。。
2、不同版本客户端,可能也会依赖不同版本的三方jar包。这个也是蛮棘手的
此种方式适合小项目或者外包等一锤子买卖
解决方案2 -----自定义ClassLoader
使用ClassLoader进行类的隔离;不同版本客户端和依赖三方包,用不同classLoader进行加载和隔离;完全杜绝版本问题
此解决方案
1、属于自研;对ClassLoader的类加载机制需要有一定的了解
解决方案3------业界开源方案 sofa-ark
sofa-ark是动态热部署和类隔离框架,由蚂蚁集团开源贡献。主要提供应用模块的动态热部署和类隔离能力
提供功能:
1、包冲突的解决(能解决现有项目遇到的多版本问题)
2、合并部署:多个项目分工程开发,但可以合并部署;还支持静态合并部署和动态合并部署
资料传送门:
https://github.com/sofastack/sofa-ark
https://www.sofastack.tech/projects/sofa-boot/sofa-ark-readme/
此解决方案:
优点
1、功能强大不仅能执行类的隔离;还能静动态的热部署;还能进行插件的热插拔;能解决现有工程遇到的问题;文档也挺齐全的
2、性能,稳定性,易用性,应该是所有保障的,毕竟蚂蚁内部也在使用;
缺点
1、虽号称是轻量级,但那是和OSGI这种重型框架相比,在sofa-ark里,也还有蛮多概念比如:Ark Container,ark包,插件包,biz包;如何打ark包,如何打插件包等等;有一定的学习成本,好在文档齐全能降低一定的入门门槛
2、需要对现有工程进行改造,以符合sofa-ark的规范;打包和部署上还需要遵循其规范。对于小公司主打的就是一个“自由”这种状态来说;有一点点束缚和被迫学习了,因为我们都比较的“懒”
此种方式适合大型项目;大型项目开发人员和开发应用众多;而sofa-ark制定了相应的biz包和插件包的开发规范;在代码复杂性,模块化开发,扩展性,项目维护,应用运行期等都进行了综合考虑。
解决方案4------OSGI
属于比较重型解决方案
OSGI 作为业内最出名的类隔离框架,自然是可以被用于解决上述包冲突问题,但是 OSGI 框架太过臃肿,功能繁杂。为了解决包冲突问题,引入 OSGI 框架,有牛刀杀鸡之嫌,反而使工程变得更加复杂,不利于开发。
我们的选择
解决方案这么多,我们该选择哪个方案了?
我们选择方案2。为什么了?
先说说为什么不选择其它方案
方案1: 此种方式不一定是最快的方式;因为后续考虑到每增加一个版本,或者社区有更新,都要去做一次,更名,打包等。还是挺麻烦,不太自动化,属于不费脑子费体力。
方案3: 功能强大:在模块化开发,类隔离,热部署等功能性上无比优异;性能和稳定性也有大公司在背书;但我们目前是个小公司,遇到的业务场景和技术场景,没有那么复杂,强大的功能给我们,我们不一定能用的上,用的不好可能还会被反噬;由于公司技术人员对该框架缺少实际使用经验;并且对该框架的实现原理也没人懂属于需要现学;使用后万一后续出现什么问题,问题定位和维护也挺麻烦;对小公司来说还是“太重”了;并且还需要对现有工程进行改造
方案4: 就不用在详说了,比方案3还重的解决方案
为什么方案2适合我们?
实现难度上: 对我们小公司来说虽然要自己写代码实现;但经过评估大概几百行代码就能搞定,技术上不是那么的高不可攀;
技术熟悉度: 团队内大家对ClassLoader的机制还蛮熟悉;
使用经验上: 有多个同事曾经用classLoader进行过这种隔离机制的实现,但业务场景不同;
复用性上: 写一次代码;后续在遇到多个版本冲突;经过简单的配置即可,不需要修改代码;更不需要修改三方依赖源码,比方案1好
在性能和稳定性上: 性能上不影响运行期,只会影响到代码加载期,所以性能这块还好;而在稳定性上,可通过测试环境长期稳定运行和一定的业务压测进行验证;而恰好我们有这样的测试环境和线上引流进行压测验证工具
设计图
ClassLoader的原理: 从上图可看出,ClassLoader会影响JVM加载类的路径和类的加载顺序;
类的加载路径
bootStrap ClassLoader: 加载%JRE_HOME%\lib 下的jar,比如rt.jar等
Extendtion ClassLoader: 加载%JRE_HOME%\lib\ext 目录下的jar
AppClass ClassLoader: 加载应用classpath下的所有类,即工程里依赖三方jar和工程的class
加载顺序 :双亲委托机制;加载类时,先让父ClasserLoader进行加载,父Classloader找不到,才让子ClassLoader进行查找和加载。
主要想法: 自定义ClassLoader继承URLClassLoader(即图里的AppClassClassLoader);基础类的加载 还是用双亲委托机制,由父类去加载,自定义类实现findClass方法,在该方法里加载指定目录class和jar。
具体工具类-----应该可以拿来即用
该实现类主要参考了Cyber365大佬的文章;然后做了一些改动(简化类,抽取工具类,去除多重if嵌套)
主要由两个类进行实现
-
MiddlewareClassLoader 类 主要做了两件事
1:读取Class文件内容;根据传入类名,从指定url中查找并读取到对应class文件内容
2:生产Class对象: 传入class文件内容,调用底层defineClass方法生产 -
UrlUtils 类: 主要做了一件事
1:根据指定的url,计算出该url和对应子目录下jar的url。
MiddlewareClassLoader 类
public class MiddlewareClassLoader extends URLClassLoader {
private URL[] allUrl;
public MiddlewareClassLoader(String[] paths){
this(UrlUtils.getURLs(paths));
}
public MiddlewareClassLoader(URL[] urls) {
this(urls, MiddlewareClassLoader.class.getClassLoader());
}
public MiddlewareClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
this.allUrl = urls;
}
protected Class<?> findClass(final String name)
throws ClassNotFoundException{
return loadExtendClass(name);
}
// /**
// * 不建议重新定义loadClass 方法,打破双亲委派机制,采用逆向双亲委派
// *
// * @param className 加载的类名
// * @return java.lang.Class<?>
// * @author Cyber
// * <p> Created by 2022/11/22
// */
// @Override
// public Class<?> loadClass(String className) throws ClassNotFoundException {
// Class extClazz = loadExtendClass(className);
// if(null != extClazz){
// return extClazz;
// }
// return super.loadClass(className);
// }
public Class<?> loadExtendClass(String className) throws ClassNotFoundException {
if(null == allUrl){
return null;
}
String classPath = className.replace(".", "/");
classPath = classPath.concat(".class");
for (URL url : allUrl) {
byte[] data = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream is = null;
try {
File file = new File(url.toURI());
if (!file.exists()) {
continue;
}
JarFile jarFile = new JarFile(file);
if (jarFile == null) {
continue;
}
JarEntry jarEntry = jarFile.getJarEntry(classPath);
if (jarEntry == null) {
continue;
}
is = jarFile.getInputStream(jarEntry);
byte[] buffer = new byte[1024 * 10];
int length = -1;
while ((length = is.read(buffer)) > 0) {
baos.write(buffer, 0, length);
}
data = baos.toByteArray();
System.out.println("********找到classPath=" + classPath + "的jar=" + url.toURI().getPath() + "*******");
Class clazz = this.defineClass(className, data, 0, data.length);
return clazz;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
}
工具类UrlUtils
public class UrlUtils {
/**
* description 通过文件目录获取目录下所有的jar全路径信息
*
* @param paths 文件路径
* @return java.net.URL[]
* @author Cyber
* <p> Created by 2022/11/22
*/
public static URL[] getURLs(String[] paths) {
if (null == paths || 0 == paths.length) {
throw new RuntimeException("jar包路径不能为空.");
}
List<String> dirs = new ArrayList<String>();
for (String path : paths) {
dirs.add(path);
collectDirs(path, dirs);
}
List<URL> urls = new ArrayList<URL>();
for (String path : dirs) {
urls.addAll(doGetURLs(path));
}
URL[] threadLocalurls = urls.toArray(new URL[0]);
return threadLocalurls;
}
/**
* description 递归获取文件目录下的根目录
*
* @param path 文件路径
* @param collector 根目录
* @return void
* @author Cyber
* <p> Created by 2022/11/22
*/
private static void collectDirs(String path, List<String> collector) {
if (null == path || "".equalsIgnoreCase(path)) {
return;
}
File current = new File(path);
if (!current.exists() || !current.isDirectory()) {
return;
}
for (File child : current.listFiles()) {
if (!child.isDirectory()) {
continue;
}
collector.add(child.getAbsolutePath());
collectDirs(child.getAbsolutePath(), collector);
}
}
private static List<URL> doGetURLs(final String path) {
if (null == path || "".equalsIgnoreCase(path)) {
throw new RuntimeException("jar包路径不能为空.");
}
File jarPath = new File(path);
if (!jarPath.exists() || !jarPath.isDirectory()) {
throw new RuntimeException("jar包路径必须存在且为目录.");
}
FileFilter jarFilter = new FileFilter() {
/**
* description 判断是否是jar文件
* @param pathname jar 全路径文件
* @return boolean
* @author Cyber
* <p> Created by 2022/11/22
*/
@Override
public boolean accept(File pathname) {
return pathname.getName().endsWith(".jar");
}
};
File[] allJars = new File(path).listFiles(jarFilter);
List<URL> jarURLs = new ArrayList<URL>(allJars.length);
for (int i = 0; i < allJars.length; i++) {
try {
jarURLs.add(allJars[i].toURI().toURL());
} catch (Exception e) {
throw new RuntimeException("系统加载jar包出错", e);
}
}
return jarURLs;
}
}
kafka发送基础类
@Slf4j
public abstract class AbstractKafkaProducer {
private String kafkaClassName = "org.apache.kafka.clients.producer.KafkaProducer";
private String stringSerializerClassName = "org.apache.kafka.common.serialization.StringSerializer";
private String serializerClassName = "org.apache.kafka.common.serialization.Serializer";
private String producerRecordClassName = "org.apache.kafka.clients.producer.ProducerRecord";
private MiddlewareClassLoader middlewareClassLoader;
private Object producerObject = null;
private Method sendMethod = null;
private Constructor producerRecordConstructor = null;
//类的加载和初始化
public void init(String jarPath){
middlewareClassLoader = new MiddlewareClassLoader(new String[]{jarPath});
try {
ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(middlewareClassLoader);
Class kafkaProduceClazz = middlewareClassLoader.loadClass(kafkaClassName);
Class kafkaStringSerializerClazz = middlewareClassLoader.loadClass(stringSerializerClassName);
Class kafkaSerializerClazz = middlewareClassLoader.loadClass(serializerClassName);
//加载KafkaProducer类
Class ProducerRecordClazz = middlewareClassLoader.loadClass(producerRecordClassName);
Constructor producerConstructor = kafkaProduceClazz.getConstructor(Map.class, kafkaSerializerClazz, kafkaSerializerClazz);
Map<String,Object> produceConfigMap = new HashMap<String,Object>();
produceConfigMap.put("retries",3);
produceConfigMap.put("retry.backoff.ms",10000);
produceConfigMap.put("acks","all");
//回调方法,让自来可更改生产端配置
addExtendConfig(produceConfigMap);
producerObject = producerConstructor.newInstance(produceConfigMap,kafkaStringSerializerClazz.newInstance(),kafkaStringSerializerClazz.newInstance());
sendMethod = kafkaProduceClazz.getMethod("send",new Class[]{ProducerRecordClazz});
producerRecordConstructor = ProducerRecordClazz.getConstructor(String.class,Object.class);
Thread.currentThread().setContextClassLoader(threadClassLoader);
System.out.println("========end======");
} catch (Exception e) {
e.printStackTrace();
}
}
protected abstract void addExtendConfig(Map<String, Object> produceConfigMap);
//发送消息
public void send(String topic,String msg){
try {
sendMethod.invoke(producerObject,producerRecordConstructor.newInstance(topic,msg));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Kafka09Producer
@Data
public class Kafka09Producer extends AbstractKafkaProducer {
private String bootstrapServers;
@Override
protected void addExtendConfig(Map<String, Object> produceConfigMap) {
produceConfigMap.put("bootstrap.servers",bootstrapServers);
}
}
Kafka32Producer
@Data
public class Kafka32Producer extends AbstractKafkaProducer {
private String bootstrapServers;
@Override
protected void addExtendConfig(Map<String, Object> produceConfigMap) {
produceConfigMap.put("bootstrap.servers",bootstrapServers);
}
}
核心测试代码
@Slf4j
public class MiddlewareClassLoaderTest {
public static void main(String[] args) throws InterruptedException {
//kafka 0.9的测试
Kafka09Producer kafka09Producer = new Kafka09Producer();
kafka09Producer.setBootstrapServers(KafkaConfig.getInstance().getProperty("bootstrap.servers"));
kafka09Producer.init("/data/app/product-kafka/ext-lib/kafka090");
kafka09Producer.send("topic090","msg090:" + System.currentTimeMillis());
//kafka 3.2的测试
Kafka32Producer kafka32Producer = new Kafka32Producer();
kafka32Producer.setBootstrapServers(KafkaConfig.getInstance().getProperty("bootstrap.servers"));
kafka32Producer.init("/data/app/product-kafka/ext-lib/kafka32");
kafka32Producer.send("topic320","msg320:" + System.currentTimeMillis());
Thread.sleep(60 * 1000);
}
}
测试结果
kafka09类的加载
kafka32类的加载
总结
ClassLoader除了能加载指定版本jar包外;还可以做热部署和热更新;如果要再次加载同一个类达到热更新;可 new一个classLoader然后loadClass,再用该Class去实例化对象即可。
还有一个困扰新手较久的注意点:Class的加载和Object实例化需要分开去看待,ClassLoader只影响类的加载;类的实例化是另外一个问题。
原创不易,请点赞,留言,关注,收藏 4暴击 ^^
参考资料:
https://blog.csdn.net/briblue/article/details/54973413 ClassLoader类加载机制,类加载顺序
https://juejin.cn/post/7168678691839410213 Cyber365大佬的文章