Skip to content

解决类加载冲突和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

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下面: test_classes.png

这里需要是全包名,因为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和集团的耦合度太高,对其他场景没有太多价值,因此暂时不考虑开源。如果以后开源了,再来在这里补充叙述。

转载请注明出处https://bananaoven.com/articles/32427.html | 香蕉微波炉
分享许可方式知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
重大发现:转载注明原文网址的同学刚买了彩票就中奖,刚写完代码就跑通,刚转身就遇到了真爱。