项目管理上有一个著名的概念叫着「钻石依赖」,是指软件依赖导致同一个软件包的两个版本需要共存而不能冲突。
我们平时使用的 maven 是这样解决钻石依赖的,它会从多个冲突的版本中选择一个来使用,如果不同的版本之间兼容性很糟糕,那么程序将无法正常编译运行。Maven 这种形式叫「扁平化」依赖管理。使用 ClassLoader 可以解决钻石依赖问题。不同版本的软件包使用不同的 ClassLoader 来加载,位于不同 ClassLoader 中名称一样的类实际上是不同的类。下面让我们使用 URLClassLoader 来尝试一个简单的例子,它默认的父加载器是 AppClassLoader
- $ cat ~/source/jcl/v1/Dep.java
- public class Dep {
- public void print() {
- System.out.println("v1");
- }
- }
- $ cat ~/source/jcl/v2/Dep.java
- public class Dep {
- public void print() {
- System.out.println("v1");
- }
- }
- $ cat ~/source/jcl/Test.java
- public class Test {
- public static void main(String[] args) throws Exception {
- String v1dir = "file:///Users/qianwp/source/jcl/v1/";
- String v2dir = "file:///Users/qianwp/source/jcl/v2/";
- URLClassLoader v1 = new URLClassLoader(new URL[]{new URL(v1dir)});
- URLClassLoader v2 = new URLClassLoader(new URL[]{new URL(v2dir)});
-
- Class<?> depv1Class = v1.loadClass("Dep");
- Object depv1 = depv1Class.getConstructor().newInstance();
- depv1Class.getMethod("print").invoke(depv1);
- Class<?> depv2Class = v2.loadClass("Dep");
- Object depv2 = depv2Class.getConstructor().newInstance();
- depv2Class.getMethod("print").invoke(depv2);
-
- System.out.println(depv1Class.equals(depv2Class));
- }
- }
在运行之前,我们需要对依赖的类库进行编译
- $ cd ~/source/jcl/v1
- $ javac Dep.java
- $ cd ~/source/jcl/v2
- $ javac Dep.java
- $ cd ~/source/jcl
- $ javac Test.java
- $ java Test
- v1
- v2
- false
在这个例子中如果两个 URLClassLoader 指向的路径是一样的,下面这个表达式还是 false,因为即使是同样的字节码用不同的 ClassLoader 加载出来的类都不能算同一个类
- depv1Class.equals(depv2Class)
我们还可以让两个不同版本的 Dep 类实现同一个接口,这样可以避免使用反射的方式来调用 Dep 类里面的方法。
- Class<?> depv1Class = v1.loadClass("Dep");
- IPrint depv1 = (IPrint)depv1Class.getConstructor().newInstance();
- depv1.print()
ClassLoader 固然可以解决依赖冲突问题,不过它也限制了不同软件包的操作界面必须使用反射或接口的方式进行动态调用。Maven 没有这种限制,它依赖于虚拟机的默认懒惰加载策略,运行过程中如果没有显示使用定制的 ClassLoader,那么从头到尾都是在使用 AppClassLoader,而不同版本的同名类必须使用不同的 ClassLoader 加载,所以 Maven 不能完美解决钻石依赖。 如果你想知道有没有开源的包管理工具可以解决钻石依赖的,我推荐你了解一下 sofa-ark,它是蚂蚁金服开源的轻量级类隔离框架。
分工与合作
(编辑:ASP站长网)
|