【java 基础】闲话 ClassLoader 和 SPI

引子

有别于 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 异常。
Image: https://uploader.shimo.im/f/XticuP44pZvBeor5.png!thumbnail?accessToken=eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE3MDkyNzg0NzUsImZpbGVHVUlEIjoiNXhrR29HeEVqT0N6MXprWCIsImlhdCI6MTcwOTI3ODE3NSwiaXNzIjoidXBsb2FkZXJfYWNjZXNzX3Jlc291cmNlIiwidXNlcklkIjo5NDQ4NDE4fQ.7teiXi8UKNQMlxXtA9Puy-Q_HJAeV0kRoqqgjO3qs8k

你真的明白了吗?

回答下面这个问题:如果类 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");时有以下代码:
Image: https://uploader.shimo.im/f/j7jalpGRyOGJ0Ghs.png!thumbnail?accessToken=eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE3MDkyNzg0NzUsImZpbGVHVUlEIjoiNXhrR29HeEVqT0N6MXprWCIsImlhdCI6MTcwOTI3ODE3NSwiaXNzIjoidXBsb2FkZXJfYWNjZXNzX3Jlc291cmNlIiwidXNlcklkIjo5NDQ4NDE4fQ.7teiXi8UKNQMlxXtA9Puy-Q_HJAeV0kRoqqgjO3qs8k
通过 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();这样的代码,这些是在干嘛?后续再跟大家唠唠吧。

  • 16
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值