项目开发中,我们会引入框架、工具类、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
java
package com.bewindoweb.pandora;
public class TestClass {
public TestClass() {
}
public void method1() {
System.out.println("version 1.0.0 -> method 1");
}
}java
package com.bewindoweb.pandora;
public class TestClass {
public TestClass() {
}
public void method1() {
System.out.println("version 1.0.0 -> method 1");
}
public void method2() {
System.out.println("version 2.0.0 -> method 2");
}
}我们把他们编译后放在resources下面: 
这里需要是全包名,因为URLClassLoader的findClass方法是这样写的:
java
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);它会把com.bewindoweb.pandora转换成com/bewindoweb/pandora/TestClass.class去找文件。
构造独立类加载器:ModuleClassLoader
public class ModuleClassLoader extends URLClassLoader {
private final String moduleName;
public ModuleClassLoader(String moduleName, URL[] urls) {
super(urls, null);
this.moduleName = moduleName;
}
@Override
public String toString() {
return moduleName + "'s ModuleClassLoader";
}
}这里有个重点是,super(urls, null),一定要填写null,含义是“不要使用父类加载器”。因为ClassLoader默认是双亲委派,可以看它的loadClass方法:
- 先看是否已经被加载到缓存里了,直接拿:findLoadedClass
- 然后如果父加载器不为null,优先使用父加载器:parent.loadClass
- 最后才使用当前加载器:findClass
所以,如果这里父类加载器不为null,默认会用主线程的加载器,如果是IDE,通常是AppClassLoader,就失去构造ModuleClassLoader的意义了。
执行加载,观察数据
java
// 创建类加载器
ModuleClassLoader loader1 = buildLoader("Diamond", root + "test-1.0.0");
ModuleClassLoader loader2 = buildLoader("HSF", root + "test-2.0.0");
// 加载类
Class<?> testClass1 = loader1.loadClass("com.bewindoweb.pandora.TestClass");
Class<?> testClass2 = loader2.loadClass("com.bewindoweb.pandora.TestClass");
// 查看加载的类
print(testClass1);
print(testClass2);执行的结果是:
method1-Diamond's ModuleClassLoader
method2-HSF's ModuleClassLoader, method1-HSF's ModuleClassLoader也就是,Diamond's ModuleClassLoader加载了test-1.0.0的TestClass,HSF's ModuleClassLoader加载了test-2.0.0的TestClass。
pandora使用类
仅仅加载还是不够,每个中间件使用的时候,也需要准确使用到自己下面的类。为了避免双亲委派,需要自己覆盖实现loadClass方法,破坏掉双亲委派,优先使用自己的Loader进行加载。
java
public class ModuleClassLoader extends URLClassLoader {
@Override
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
loadClassInternal(name, resolve);
}
private Class<?> loadClassInternal(String name, boolean resolve) throws ClassNotFoundException {
// 1. 已经加载的类
Class<?> clazz = resolveLoaded(name);
if (clazz != null) {
return clazz;
}
// 2. 加载JDK相关的类
clazz = resolveBootrap(name);
if (clazz != null) {
return clazz;
}
// 3. com.taobao.pandora开头的选择从PandoraClassLoader中加载
// 避免被底层中间件替换其实现
clazz = resolvePandoraClass(name);
if (clazz != null) {
return clazz;
}
// 4. 从共享缓存加载
// pandora构造了一个共享缓存,共享基础插件导出类,如HSF
clazz = resolveShared(name);
if (clazz != null) {
return clazz;
}
// 5. 根据import语义从bizClassLoader中加载
// 比如共用用户的spring组件
clazz = resolveImport(name);
if (clazz != null) {
return clazz;
}
// 6. 从import配置的插件里面加载
// pandora构造了一个共享缓存,共享基础插件导出类,如HSF
clazz = resolveImportPlugin(name);
if (clazz != null) {
return clazz;
}
// 7. 从当前classpath下加载
// 也就是用URLClassLoader调用findClass直接加载类了
clazz = resolveClassPath(name);
if (clazz != null) {
return clazz;
}
// 8. 从bizClassLoader中加载,如果有bizClassloader,或者说usebizClassLoader设置成为了true
clazz = resolveExternal(name);
if (clazz != null) {
return clazz;
}
// 9. 从SystemClassLoader下加载,解决agent加载的问题
clazz = resolveSystemClassLoader(name);
// 是否需要解析
if (clazz != null) {
if (resolve) {
resolveClass(clazz);
}
return clazz;
} else {
// 报错
}
}
}总结
pandora核心是利用"类加载器"的不同,来重复加载不同版本相同类名的类。pandora将中间件抽象成"PluginModule",并为每个模块都构造了一个自己的ModuleClassLoader。这个Loader实际并没有特殊的解析动作,而只是做了一个编排,底层解析还是调用的通用的类加载器的方法。同时为了屏蔽加载文件的一些细节,pandora构造了Archive的抽象模型来代表一个jar或者文件夹;利用PicoContainer作为IoC容器、Pipeline作为启动阶段的设计模式来简化代码,最后pandora在每个插件加载生命周期,都预留了回调和事件,方便插件灵活实现自己的逻辑。
由于pandora目前并未开源,对2.1.19版代码的详细分析不能公开详细叙述,只发布在了内网ATA。目前作者认为pandora和集团的耦合度太高,对其他场景没有太多价值,因此暂时不考虑开源。如果以后开源了,再来在这里补充叙述。



粤公网安备44030602007943号