我学Java(1)——ClassLoader与双亲委托模式以及「SPI」
1、ClassLoader分类
Java虚拟机会创建三类ClassLoader,分别如下
名称 | 加载 | 加载路径 | 父加载器 | 实现 |
---|---|---|---|---|
BootStrap | 虚拟机的核心类库 | sun.boot.class.path | 无 | 系统 |
Extension | 扩展类库 | java.ext.dirs、jre/lib/ext | BootStrap | Java |
System | 应用类库 | classpath、java.class.path | Extension | Java |
2、双亲委托模式
其实我觉得把「双亲委托模式」称为「父加载委托模式」更好理解,「双」字把我给弄混了。
「双亲委托模式」指的就是某个特定的类加载器在接到加载类的请求时,
首先将加载任务委托给父类加载器
,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,自己才去加载。

下面是一段ClassLoader的源码,很容易可以看出上述规则:
protected synchronized Class loadClass(String name, boolean resolve)
throws ClassNotFoundException{
// 首先检查该name指定的class是否有被加载
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//如果parent不为null,则调用parent的loadClass进行加载
c = parent.loadClass(name, false);
}else{
//parent为null,则调用BootstrapClassLoader进行加载
c = findBootstrapClass0(name);
}
}catch(ClassNotFoundException e) {
//如果仍然无法加载成功,则调用自身的findClass进行加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
(1)优点
- 避免类库重复加载
- 安全,将核心类库与用户类库隔离,用户不能通过加载器替换核心类库,如String类。
(2)弊端
委托永远是子加载器去请求父加载器,是单向的,即上层的类加载器无法访问下层的类加载器所加载的类
:

举个例子,假设「BootStrap」中提供了一个接口,及一个创建其实例的工厂方法,但是该接口的实现类在「System」中,那么就会出现工厂方法无法创建在「System」加载的类的实例的问题。拥有这样问题的组件有很多,比如JDBC、Xml parser等。
3、如何解决弊端——使用「SPI」
现在引入一个新的名词「SPI」。
「SPI」 全称为 (Service Provider Interface) ,是JDK内置的一种
服务提供发现机制
。 目前有不少框架用它来做服务的扩展发现, 简单来说,它就是一种动态替换发现的机制。
JDBC本身是Java连接数据库的一个标准,是进行数据库连接的抽象层,由Java编写的一组类和接口组成,接口的实现由各个数据库厂商来完成,不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的「SPI」机制可以为某个接口寻找服务实现。
3、JDBC举例
下面以JDBC为例,介绍「SPI」机制。
在JDBC4.0之前,我们开发有连接数据库的时候,通常会用Class.forName("com.mysql.jdbc.Driver")这句先加载数据库相关的驱动,然后再进行获取连接等的操作。而JDBC4.0之后不需要用Class.forName("com.mysql.jdbc.Driver")来加载驱动,直接获取连接就可以了,现在这种方式就是使用了Java的「SPI」扩展机制来实现。
(1)接口定义
JDBC在java.sql.Driver
只定义了接口。

(2)厂商实现
这里以MySQL为例,在mysql-connector-java-6.0.6.jar
包里的META-INF/services
目录下可以找到一个java.sql.Driver
文件,文件内容是一个类名,这个名叫com.mysql.cj.jdbc.Driver
的类就是MySQL针对JDBC中定义的接口的实现。


(3)如何使用
在我们的应用里面,我们就可以直接连接MySQL了。
Connection conn = DriverManager.getConnection(url,username,password);
显然语句并没有加载实现类,这里就涉及到使用「SPI」扩展机制来查找相关驱动了,接下来,我们结合源码探究一下这是如何实现的。
4、源码解析
关于驱动的查找其实都在DriverManager中,DriverManager位于java.sql
包里,用来获取数据库连接,在DriverManager中有一个静态代码块如下:

loadInitialDrivers
方法用于实例化驱动,由3部分构成:
(1)获取有关驱动的名称

(2)加载并实例化驱动

两个比较关键的地方是ServiceLoader.load
, 还有loadedDrivers.iterator
,下面结合源码介绍一下:
(A)ServiceLoader.load
ServiceLoader
封装了一个自定义加载器loader
,还应留意一下下面2个成员,之后会用到:
- 默认接口路径:
PREFIX
- 实现类的加载迭代器:
lookupIterator

ServiceLoader.load(Driver.class)
最后会调用构造函数,返回ServiceLoader
实例



每一个线程都有自己的ContextClassLoader,默认以
SystemClassLoader
为ContextClassLoader。通过Thread.currentThread().getContextClassLoader()
,可以把一个ClassLoader置于一个线程的实例之中,使该ClassLoader成为一个相对共享的实例,这样即使是启动类加载器中的代码也可以通过这种方式访问应用类加载器中的类了。

多个加载器通过上下文加载器共享
(B)loadedDrivers.iterator
loadedDrivers.iterator
方法返回一个迭代器,这个迭代器是「SPI」机制加载实现类的关键,迭代器在iterator()
方法内定义:

「SPI」加载代码的是这样的:

执行driversIterator.hasNext
时,会调用lookupIterator.hasNext
去找的实现类的名字。



接着会调用lookupIterator.next()
去加载这个类:



至此,已经将实现类成功加载。
(3)加载驱动
现在就可以根据第1步获取到的驱动列表来加载实现类了:

5、「SPI」的弊端
「SPI」通过循环加载实现类,显而易见,它会把所有的类一同加载,无论有没有用到,这造成了一定的资源浪费:

参考链接
android classloader双亲委托模式
dubbo源码解析-spi(一)
Java中SPI机制深入及源码解析
走出ClassLoader的迷宫