引子
有别于 java 提供的 IO 模块,java 中的classloader主要是用来加载类的,当然除了加载类,也可以加载资源文件。
那么首先我们会问一个问题,有了 IO 为什么要 classloader?这是我们开启 classloader 大门要弄明白的第一个问题。
java IO 提供了一些常见的功能,比如读文件、写文件,操作字符流、字节流,网络的读写,文件系统操作等等功能,不胜枚举。显而易见,java IO 提供了一些通用方法。
而 classloader 是 JVM 用来按需动态加载资源的工具。之所以有 classloader 有多方面的考虑,首先要解决程序运行时怎么加载类,需要一套机制,这套机制就是我们常说的双亲委派模型。其次是怎么读取资源,比如我们想要读取某个配置文件,或者一张图片(当然读取资源文件我们可以直接用 IO 也不是不可以,殊途同归)。
双亲委派模型
老生常谈的话题,不过也值得讨论一番。java 内建的classloader主要分为 3 类:
- Bootstrap ClassLoader
- Extension ClassLoader(又叫 Platform ClassLoader)
- Application ClassLoader(又叫 System ClassLoader)
Bootstrap ClassLoader: 是最顶层的ClassLoader,负责加载JRE核心库,它是用C++实现的,无法通过Java代码来创建。
Extension ClassLoader:负责加载Java的扩展库。(本质上还是 java 官方提供的,由 java 实现的类库)
Application ClassLoader:负责加载用户类路径下的类。比如我们自己编写的类,引入的第三方 jar 包等。
如下图:我们举一个例子,假设 JVM 要加载类 A,首先会通过 Application ClassLoader 进行加载,这时首先检查其缓存中是否已加载此类,如果加载,则返回。如果缓存中没有类 A,则委托给 Extension ClassLoader进行加载,同样是先检查是否有缓存,如果没有则委托给 Bootstrap ClassLoader 进行加载,同样是检查缓存,如果还是没有,则尝试扫描 JRE 核心库是否有该类,如果有,则加载类,否则返回到 Extension ClassLoader,Extension ClassLoader 扫描其负责的扩展库,如果有,则加载,否则返回到 Application ClassLoader进行加载,Application ClassLoader扫描用户的类路径,如果找到该类,则加载,否则则抛出ClassNotFound 异常。
你真的明白了吗?
回答下面这个问题:如果类 A 是 Extension ClassLoader 加载,而类 A 中又引入了类 B,那么类 B 会怎么被加载呢?还是从 Appcation ClassLoader 开始加载吗?
答案是否定的,类 B 会从 Extension ClassLoader 开始加载,先委托Bootstrap ClassLoader,如果没找到,则 Extension ClassLoader自己开始加载,如果找不到,则抛出 ClassNotFound,并不会再返回到 Application ClassLoader 进行加载。为什么要这样设计?很简单,留给大家自己思考吧。
双亲委派“不够用了”
有时候默认的双亲委派不够用,举个例子,java 定义了一个数据库标准接口 JDBC,各个数据库厂商会实现这个标准接口,即我们所说的数据库驱动包。大家在学JDBC 的时候应该都写过类似这种代码
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/dbName");
大家可以尝试将第一句删除掉,你会发现还是可以获取到Connection,这是为什么呢?DriverManager的包名是 java.sql
,显然是 jdk的核心包,所以定然不会在其中写入加载某个具体驱动类的代码。所以 java 的开发人员就发明了一种新的方法:SPI(Service Provider Interface)。
SPI机制
SPI 的机制很简单,我们还是以数据库驱动为例,首先各个驱动厂商开发对应的驱动包,不过动包会有些特殊,如下图:
在驱动包的 META-INF/services 下会包含与所要实现驱动名称相同的一个文本文件,文本文件的内容是实现这个驱动的具体类。
然后在执行 DriverManager.getConnection("jdbc:mysql://localhost:3306/dbName");
时有以下代码:
通过 ServiceLoader.load(Driver.class)去加载驱动。具体怎么加载这里就不说了,无非是扫描上面我们说的META-INF/services目录下的文件,将所有实现了Driver接口的驱动都注册进来。那为什么这里就可以加载到了呢?因为我们在执行ServiceLoader.load(Driver.class)方法时,方法内部是通过 Application ClassLoader 进行加载的,自然可以加载到外部的驱动包了。
那么,如果我引入了多个驱动包呢?系统怎么知道我们用的哪一个?如下图,在 Driver 接口中定义了一个方法:acceptsURL,通过对jdbc:mysql://localhost:3306/dbName这种格式的判断来决定此驱动是不是用户想要的驱动。
DriverManager 中调用上面实现的acceptsURL 方法:
其他琐碎
说了那么多,好像跟我们自己平常开发没有多少关系。其实我们也可以利用ClassLoader来加载起源,比如我们想读取一个配置文件。可以用类似ClassLoader.findResource("xxx")
或者this.class.getResource("xx")
。在一些代码里我们还会看到:ClassLoader cl = Thread.currentThread().getContextClassLoader();
这样的代码,这些是在干嘛?后续再跟大家唠唠吧。