解决类加载冲突和pandora

项目开发中,我们会引入框架、工具类、SDK等依赖,这些依赖的包也会有依赖,层层嵌套。一个比较关键的问题是,如果不同依赖,都引用相同的一个底层依赖,但是是不同版本,就会出现引用冲突。

如下图,一个项目引入了Diamond 2.3.4HSF 1.2.3FastJson 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的方法呢?

import_confict.png

Maven的解决思路

maven的解决方案是:平板化依赖,只用1个版本的包。

它制定了一些规则,比如”谁pom文件的坐标写在前面,就先加载谁的包”,来保证最后只会有一个版本的包的类被加载。

这就隐含了使用者需要遵循2个条件:

(1)高版本必须完全兼容低版本的接口,不能删除低版本的接口
否则依赖低版本包的应用,就会找不到方法。比如使用的是FastJson 1.2.0,但该版本删去了1.0.0的method0、1.1.0的method1,Diamond和HSF调用时就会报错。
old_method_not_found.png

(2)多个包冲突的时候,必须使用最高版本的包
否则依赖高版本包的应用,就会找不到方法。比如使用的是FastJson 1.0.0,但依赖1.2.0的method2,也会产生找不到方法的问题。
new_method_not_found.png

所以一般的解决方案就是:

  • 用一个全家桶。比如spring,让他保证主要用到的依赖都是兼容的
  • 修改旧代码。如果某个关键依赖,就是做了不兼容的升级,那就得改代码,去掉旧依赖的旧接口,一旦遇到了就比较痛苦。

Pandora的解决思路

由于阿里的中间件实在是太多,各个中间件之间相互依赖,每天都在更新。对于业务团队,如果引入中间件还要去考虑排除包冲突,实在是很浪费时间;对于中间件团队,如果新增功能还要去考虑各种兼容,实在是很难推进版本。

pandora的解决方案是:同时加载多个类,各自用自己的类。

pandora加载类

在java中,如何唯一确定一个类?答案是“类加载器+类全限定名”。所以,对于com.alibaba.fastjson.JSON这个类,全限定名都是一样的,但是如果采用不同的类加载器加载,就能够同时存在多个类:
pandora_class_loader.png

  • 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
2
3
4
5
6
7
8
9
10
11
package com.bewindoweb.pandora;

public class TestClass {
public TestClass() {
}

public void method1() {
System.out.println("version 1.0.0 -> method 1");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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下面:
test_classes.png

这里需要是全包名,因为URLClassLoader的findClass方法是这样写的:

1
2
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);

它会把com.bewindoweb.pandora转换成com/bewindoweb/pandora/TestClass.class去找文件。

构造独立类加载器:ModuleClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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的意义了。

执行加载,观察数据

1
2
3
4
5
6
7
8
9
10
11
// 创建类加载器
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);

执行的结果是:

1
2
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进行加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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和集团的耦合度太高,对其他场景没有太多价值,因此暂时不考虑开源。如果以后开源了,再来在这里补充叙述。

解决类加载冲突和pandora

https://www.bananaoven.com/posts/32427/

作者

香蕉微波炉

发布于

2023-02-26

更新于

2023-02-26

许可协议