解决类加载冲突和pandora
项目开发中,我们会引入框架、工具类、SDK等依赖,这些依赖的包也会有依赖,层层嵌套。一个比较关键的问题是,如果不同依赖,都引用相同的一个底层依赖,但是是不同版本,就会出现引用冲突。
如下图,一个项目引入了Diamond 2.3.4
、HSF 1.2.3
、FastJson 1.2.0
共3个组件。Diamond组件引入FastJson 1.1.0
,HSF组件引入FastJson 1.0.0
。当我们使用com.alibaba.fastjson.JSON.toJSONString(data)方法的时候,到底调用的是FastJson 1.0.0
的方法、FastJson 1.1.0
的方法,还是FastJson 1.2.0
的方法呢?
Maven的解决思路
maven的解决方案是:平板化依赖,只用1个版本的包。
它制定了一些规则,比如”谁pom文件的坐标写在前面,就先加载谁的包”,来保证最后只会有一个版本的包的类被加载。
这就隐含了使用者需要遵循2个条件:
(1)高版本必须完全兼容低版本的接口,不能删除低版本的接口
否则依赖低版本包的应用,就会找不到方法。比如使用的是FastJson 1.2.0
,但该版本删去了1.0.0的method0、1.1.0的method1,Diamond和HSF调用时就会报错。
(2)多个包冲突的时候,必须使用最高版本的包
否则依赖高版本包的应用,就会找不到方法。比如使用的是FastJson 1.0.0
,但依赖1.2.0的method2,也会产生找不到方法的问题。
所以一般的解决方案就是:
- 用一个全家桶。比如spring,让他保证主要用到的依赖都是兼容的
- 修改旧代码。如果某个关键依赖,就是做了不兼容的升级,那就得改代码,去掉旧依赖的旧接口,一旦遇到了就比较痛苦。
Pandora的解决思路
由于阿里的中间件实在是太多,各个中间件之间相互依赖,每天都在更新。对于业务团队,如果引入中间件还要去考虑排除包冲突,实在是很浪费时间;对于中间件团队,如果新增功能还要去考虑各种兼容,实在是很难推进版本。
pandora的解决方案是:同时加载多个类,各自用自己的类。
pandora加载类
在java中,如何唯一确定一个类?答案是“类加载器+类全限定名”。所以,对于com.alibaba.fastjson.JSON
这个类,全限定名都是一样的,但是如果采用不同的类加载器加载,就能够同时存在多个类:
- AppClassLoader -> FastJSON 1.2.0的com.alibaba.fastjson.JSON
- Diamond’s Module ClassLoader -> FastJSON 1.1.0的com.alibaba.fastjson.JSON
- Hsf’s Module ClassLoader -> FastJSON 1.0.0的com.alibaba.fastjson.JSON
pandora正是这样为每个中间件都构建了自己的类加载器,即使存在同名类,也能同时加载。
pandora加载类实验
我们可以设计一个实验来体会这个过程。
代码:pandora.zip
构造两个相同的TestClass,不同的method
1 | package com.bewindoweb.pandora; |
1 | package com.bewindoweb.pandora; |
我们把他们编译后放在resources下面:
这里需要是全包名,因为URLClassLoader的findClass方法是这样写的:
1 | String path = name.replace('.', '/').concat(".class"); |
它会把com.bewindoweb.pandora
转换成com/bewindoweb/pandora/TestClass.class
去找文件。
构造独立类加载器:ModuleClassLoader
1 | public class ModuleClassLoader extends URLClassLoader { |
这里有个重点是,super(urls, null),一定要填写null,含义是“不要使用父类加载器”。因为ClassLoader默认是双亲委派,可以看它的loadClass方法:
- 先看是否已经被加载到缓存里了,直接拿:findLoadedClass
- 然后如果父加载器不为null,优先使用父加载器:parent.loadClass
- 最后才使用当前加载器:findClass
所以,如果这里父类加载器不为null,默认会用主线程的加载器,如果是IDE,通常是AppClassLoader,就失去构造ModuleClassLoader的意义了。
执行加载,观察数据
1 | // 创建类加载器 |
执行的结果是:
1 | method1-Diamond's ModuleClassLoader |
也就是,Diamond's ModuleClassLoader
加载了test-1.0.0
的TestClass,HSF's ModuleClassLoader
加载了test-2.0.0
的TestClass。
pandora使用类
仅仅加载还是不够,每个中间件使用的时候,也需要准确使用到自己下面的类。为了避免双亲委派,需要自己覆盖实现loadClass方法,破坏掉双亲委派,优先使用自己的Loader进行加载。
1 | public class ModuleClassLoader extends URLClassLoader { |
总结
pandora核心是利用”类加载器”的不同,来重复加载不同版本相同类名的类。pandora将中间件抽象成”PluginModule”,并为每个模块都构造了一个自己的ModuleClassLoader。这个Loader实际并没有特殊的解析动作,而只是做了一个编排,底层解析还是调用的通用的类加载器的方法。同时为了屏蔽加载文件的一些细节,pandora构造了Archive的抽象模型来代表一个jar或者文件夹;利用PicoContainer作为IoC容器、Pipeline作为启动阶段的设计模式来简化代码,最后pandora在每个插件加载生命周期,都预留了回调和事件,方便插件灵活实现自己的逻辑。
由于pandora目前并未开源,对2.1.19版代码的详细分析不能公开详细叙述,只发布在了内网ATA。目前作者认为pandora和集团的耦合度太高,对其他场景没有太多价值,因此暂时不考虑开源。如果以后开源了,再来在这里补充叙述。
解决类加载冲突和pandora