SPI
0x01 What Is SPI
Service Provider Interface:是JDK内置的一种服务提供发现机制,是Java提供的一套用来被第三方实现或扩展的接口,它可以用来启动框架扩展和替换组件。SPI是为这些被扩展的API寻找服务实现。不同厂商可以针对同一接口做出不同的实现。
API:实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,无权选择不同实现。API被应用开发人员使用
SPI:应用方制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。SPI被框架扩展人员使用
如java.sql.Driver接口

0x02 Best Practice
接口:
实现类:
在classpath下面创建目录META-INF/services/,创建一个名字为上面接口全限定名的文件

测试:
0x03 SPI + JDBC
JDBC连接数据库经常是下面的写法,首先是注册驱动,告诉程序使用哪种数据库
驱动jar包下的META-INF/services文件夹中的java.sql.Driver文件(文件名为SPI接口)中已经把Driver类路径记录下来了
若程序中没有注册驱动,会先读取这个文件,自动注册驱动。
这里就用到了SPI技术,它通过在ClassPath路径下的META-INF/services文件夹查找文件,实现类自动加载

Java类加载过程中,有一步是初始化(Initialization),初始化阶段会执行被加载类的Static Blocks。

注册驱动时执行了Class.forName("com.mysql.cj.jdbc.Driver"),Class.forName()默认会进行类的初始化
com.mysql.cj.jdbc.Driver的静态代码中调用了DriverManager的registerDriver静态方法
因此JVM会尝试去加载DriverManager类,进而执行DriverManager的静态代码,调用类中的loadInitialDrivers方法
注意到ServiceLoader.load(Driver.class)进行了类加载



最后得到的driversIterator就是LazyIterator
懒迭代器,顾名思义,需要用的时候才迭代
hasNext => hasNextService

ServiceLoader<S>类有常量属性PREFIX = "META-INF/services/"
service.getName()获取接口全类名,拼接得到SPI文件名,读取得到实现类的类名
调用其next方法 => nextService方法
Thread.currentThread().getContextClassLoader();获取类加载器扩展的实现类通过
c = Class.forName(cn, false, loader);获取
Class.forName()默认使用调用者的ClassLoader,我们是在DriverManager类里调用ServiceLoader的,所以调用类也就是DriverManager,它的加载器是Bootstrap ClassLoader。我们知道Bootstrap ClassLoader加载rt.jar包下的所有类,要用Bootstrap ClassLoader去加载用户自定义的类是违背双亲委派的,所以使用Thread.currentThread().getContextClassLoader去指定AppClassLoader(ServiceLoader.load传入的)
查看ClassPath下有那些JDBC Driver

0x04 JDBC Driver后门
现在我们尝试自己编写一个后门驱动jar包,让用户引入后门jar包,在建立JDBC连接时,触发执行命令
jar中的META-INF下需要有services文件夹
services文件夹下为java.sql.Driver,内容为com.mysql.fake.jdbc.ShellDriver
这是为了让SPI找到我们要加载的类ShellDriver

执行JDBC连接数据库操作,成功弹出计算器
注意:Class.forName(className, initia, currentLoader)
第二个参数指定是否初始化,nextService中第二个参数为false。因此不会执行待加载类的静态代码。实际上是后面对加载的Class进行newInstance时才执行了静态代码

Last updated
Was this helpful?