一篇文章带你吃透,Java界最神秘技术ClassLoader( 三 )

钻石依赖项目管理上有一个著名的概念叫着「钻石依赖」,是指软件依赖导致同一个软件包的两个版本需要共存而不能冲突 。

一篇文章带你吃透,Java界最神秘技术ClassLoader

文章插图
 
我们平时使用的 maven 是这样解决钻石依赖的,它会从多个冲突的版本中选择一个来使用,如果不同的版本之间兼容性很糟糕,那么程序将无法正常编译运行 。Maven 这种形式叫「扁平化」依赖管理 。使用 ClassLoader 可以解决钻石依赖问题 。不同版本的软件包使用不同的 ClassLoader 来加载,位于不同 ClassLoader 中名称一样的类实际上是不同的类 。下面让我们使用 URLClassLoader 来尝试一个简单的例子,它默认的父加载器是 AppClassLoader
$ cat ~/source/jcl/v1/Dep.javapublic class Dep {public void print() {System.out.println("v1");}}$ cat ~/source/jcl/v2/Dep.javapublic class Dep { public void print() { System.out.println("v1"); }}$ cat ~/source/jcl/Test.javapublic 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 Testv1v2false在这个例子中如果两个 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,它是蚂蚁金服开源的轻量级类隔离框架 。
分工与合作这里我们重新理解一下 ClassLoader 的意义,它相当于类的命名空间,起到了类隔离的作用 。位于同一个 ClassLoader 里面的类名是唯一的,不同的 ClassLoader 可以持有同名的类 。ClassLoader 是类名称的容器,是类的沙箱 。
一篇文章带你吃透,Java界最神秘技术ClassLoader

文章插图
【一篇文章带你吃透,Java界最神秘技术ClassLoader】 
不同的 ClassLoader 之间也会有合作,它们之间的合作是通过 parent 属性和双亲委派机制来完成的 。parent 具有更高的加载优先级 。除此之外,parent 还表达了一种共享关系,当多个子 ClassLoader 共享同一个 parent 时,那么这个 parent 里面包含的类可以认为是所有子 ClassLoader 共享的 。这也是为什么 BootstrapClassLoader 被所有的类加载器视为祖先加载器,JVM 核心类库自然应该被共享 。Thread.contextClassLoader
如果你稍微阅读过 Thread 的源代码,你会在它的实例字段中发现有一个字段非常特别
class Thread { ... private ClassLoader contextClassLoader;public ClassLoader getContextClassLoader() { return contextClassLoader; }public void setContextClassLoader(ClassLoader cl) { this.contextClassLoader = cl; } ...}contextClassLoader「线程上下文类加载器」,这究竟是什么东西?
首先 contextClassLoader 是那种需要显示使用的类加载器,如果你没有显示使用它,也就永远不会在任何地方用到它 。你可以使用下面这种方式来显示使用它
Thread.currentThread().getContextClassLoader().loadClass(name);这意味着如果你使用 forName(string name) 方法加载目标类,它不会自动使用 contextClassLoader 。那些因为代码上的依赖关系而懒惰加载的类也不会自动使用 contextClassLoader来加载 。


推荐阅读