从JDBC Driver 看 Java 的 SPI 机制与应用
发布网友
发布时间:2024-09-29 08:02
我来回答
共1个回答
热心网友
时间:2024-10-01 19:38
JDBC API 是 JDK 标准的一部分,定义了一组 Java 与数据库交互的接口。数据库种类繁多,具体如何交互的实现由数据库厂商提供。以 MySQL 为例,在 JDBC API 4.0 版本之后,我们只需引入提供的 drivermysql-connector-java 作为依赖,然后就可以调用 JDBC API 创建连接并执行 SQL 与数据库交互了。然而,我们的程序是如何定位到 driver 具体的位置并正确加载的呢?这就涉及到 SPI 机制了。
SPI(Service Provider Interface)可以说是一种面向扩展的设计模式。应用的核心逻辑通过面向接口的编程思路,并设计可扩展点。在运行时,通过服务发现机制加载扩展点的具体实现,从而达到在不修改核心逻辑的情况下,具体实现可灵活替换的效果。
Java 6 提供了 SPI 机制,它包含了两类角色:服务定义者和服务实现者。服务定义者涉及到的概念有:
服务实现者涉及到的概念有:
接下来,我们通过一个简单的案例来聊聊 SPI 机制的原理。例如,我们的服务需要对消费者提供支付能力,但实际的支付能力可能由支付宝或者微信提供。作为服务定义者,我们首先定义服务的标准接口,它本身就是一个普通的 Java Interface,比如我们定义我们的 SPI:
这个接口应该被公开出来供服务提供者实现,我们可以把它放到一个独立的包中去发布,比如叫做 my-system-spi。接下来就是服务提供者去实现了。首先引入包含 SPI 定义的包:
然后进行对它的实现:
同时,服务提供者需要对外告知自己有对某个 SPI 的实现。告知的方式有一个约定,就是在 Jar 包的 META-INF/services 文件夹中,定义一个以 SPI 全限定名为文件名的文件,文件内定义 SPI 实现类的全限定名,比如我们需要创建一个 META-INF/services/me.leozdgao.demo.spi.Payment 文件,文件的内容如下:
如果一个 Jar 包中有多个实现,则可以都列出来并通过换行符分割即可。
服务提供者完成实现后,发布自己的 Jar 包。那么接下来我们就需要去加载它了,这就涉及到最关键的 ServiceLoader。我们先看看如何使用:
可以看到我们通过调用 ServiceLoader.load 方法并传入我们的 SPI 接口来创建了一个 ServiceLoader 实例。由于 ServiceLoader 实现了 Iterator 迭代器接口,通过访问迭代器就可以获取实现了 SPI 的服务。如果有多个 SPI 的实现的话,具体采用哪个就需要自行处理判断了。
这样我们就在运行时顺利完成了服务的加载。未来我们如果要对 SPI 的实现要做替换,也完全不需要修改我们的逻辑代码。
ServiceLoader 的服务加载实现原理是,ServiceLoader.load 方法本质就是创建一个 ServiceLoader 实例,而服务加载主要在 ServiceLoader 的 Lazy Iterator 实现中。我们来看看迭代器方法的实现逻辑:
具体源码请参考 JDK 源码:java.util.ServiceLoader
由于是 Lazy Iterator,ServiceLoader 实例并不会一开始就去找到所有的实现,而是在不断的调用迭代器的过程中去懒加载实现类、完成实例化,并将实现类实例化的结果缓存起来。服务的初始化逻辑也反映出了两个约定:
MySQL Driver 如何实现 SPI:上面介绍完 SPI 的实现机制后,再来回答开头的问题:我们的程序是如何定位到 driver 具体的位置并正确加载的?这个就是 DriverManager 的实现了。
在 DriverManager 的静态方法 getDrivers、getDriver、drivers、getConnection 中都会调用一个 ensureDriversInitialized 方法,这个方法会保证 driver 的初始化并仅执行一次,我们具体看一下它的实现逻辑:
具体源码请参看 JDK 源码:java.sql.DriverManager
通过两种方式进行 driver 的初始化,一种是通过系统参数 jdbc.drivers 指定,通过反射的方式调用 Class.forName 进行初始化,另一种就是利用 ServiceLoader.load(Driver.class) 找到服务提供方,通过调用迭代器触发服务的初始化。
服务具体的初始化方式利用的是类的静态代码块机制,以 MySQL 的 driver 为例:
可以看到 Driver 的实现包含一个静态代码块,在通过反射 Class.forName 或者被 ServiceLoader 迭代器初始化时,都可以触发它的执行,在这里调用了 DriverManager.registerDriver 进行了注册。
那么如果发现了多个 driver 的话,要选择哪一个具体的实现呢?那就是 JDBC URL 了。driver 的实现有一个约定,如果 driver 根据 JDBC URL 判断不是自己可以处理的连接就直接返回空,DriverManager 就是基于这个约定直到找到第一个不返回 null 的连接为止。
总结:本文聊了聊 SPI 的设计思想,分析了 Java SPI 的机制与 ServiceLoader 服务加载的实现原理,并且借 JDBC API 为例看了具体的应用。可以发现,Java 提供了一种不依赖额外框架的标准的本地服务发现机制,我们可以基于这个机制让我们的应用根据灵活性和可扩展性,在服务端基础库或者是安卓生态下都可以看到它的影子。只不过除了本地的服务发现之外,应该选择哪一个服务实现仍然是在设计扩展功能时需要额外考虑的部分。
总之了解 Java SPI 更重要的是学习 SPI 设计思想的一种实践。实际上你完全可以实现一个自己版本的 ServiceLoader(根据具体情况定义一个自己的服务发现机制,甚至 Jar 包也完全可以是通过远程加载的)来为你的应用提供一个独特的可扩展机制。