概述
导言
在看双亲委派机制的时候,有人提到SPI
的概念,于是学习了一番。SPI
,即Service Provider Interface
,服务提供者的接口。这个SPI
接口是由服务端提供和调用的,这个接口定义了一些规范,但不提供具体的接口功能,功能由客户端决定,客户端根据需要实现这个接口,并进行配置,将接口的实现实例和配置打包部署在服务端,服务端不会直接调用接口的实现实例,而是 调用SPI
接口,其产生的行为取决于配置文件。
SPI
设计思想如下图所示:
SPI
接口一般由客户端和服务端共同确定(更多是服务端根据不同客户端的需求,形成一个兼容并包的接口)。客户端根据需求实现这个接口,得到一个自己的实例。服务端会有一个以这个SPI
接口为文件名的配置文件,配置文件当中指定服务端将调用哪个客户端的接口实例。
SPI
的本质是多态,多态的好处是在解耦的同时,实现了适配。
SPI的实现之ServiceLoader
Java是通过ServiceLoader
提供SPI
功能的,我们来模拟服务端、客户端的角色,理解SPI
是怎么工作的。
场景:不同的数据库想要通过Java连接,必须实现一个规范的接口,并提供相应的JDBC驱动。
一、服务端与客户端协商提供统一的SPI接口
新建一个Java项目,给出统一的SPI
接口,代码如下,其中有一个connect()
方法。
package com.java.db;
/**
* @ClassName DBDriver
* @Description TODO
* @Date 2021/1/11 22:19
*/
public interface DBDriver {
public String connect();
}
把这个项目打包成jar
包,记名为DBDriverInterface.jar
。
二、客户端提供带有配置文件的驱动类
新建一个Java项目,创建一个MySQLDriver
类,实现DBDriver
接口,因此这个项目中需要导入DBDriverInterface.jar
包,目录结构如下图所示:
代码如下:
package com.java.mysql.godriver;
import com.java.db.DBDriver;
/**
* @ClassName MySQLDriver
* @Description TODO
* @Date 2021/1/11 22:21
*/
public class MySQLDriver implements DBDriver {
public String connect() {
System.out.println("MySQL连接成功");
return "一个MySQL连接实例";
}
}
在resources
目录下,添加META-INFservices
目录,并创建文本文件,文件名为com.java.mysql.DBDriver
,就是SPI
接口的全名
,写入以下内容,这就是接口实现类的全名
:
com.java.mysql.godriver.MySQLDriver
把这个项目打包成jar
包,记名为MySQLDBDriver.jar
。
三、服务端利用ServiceLoader加载驱接口
利用ServiceLoader
加载接口的class
对象,就可以加载配置文件当中实现了这个接口的所有驱动类,并执行相应的动作。
新建一个Java项目,导入上述两个包:DBDriverInterface.jar
包和MySQLDBDriver.jar
包,并利用ServiceLoader
去加载MySQL数据库连接驱动,目录结构如下所示:
主类代码如下:
package com.java.db;
import java.util.Iterator;
import java.util.ServiceLoader;
/**
* @ClassName Main
* @Description TODO
* @Date 2021/1/11 22:43
*/
public class Main {
public static void main(String[] args) {
// 加载
ServiceLoader<DBDriver> drivers = ServiceLoader.load(DBDriver.class);
Iterator<DBDriver> iterator = drivers.iterator();
// 加载了哪些内容?
while (iterator.hasNext()){
DBDriver next = iterator.next();
String connect = next.connect();
System.out.println(connect);
}
}
}
结果如下所示:
MySQL连接成功
一个MySQL连接实例
Process finished with exit code 0
主方法当中,通过ServiceLoader
加载的class
对象是DBDriver.class
,没有指明是哪个实现类,但在配置文件当中指明了。
如果你想实现其他数据库,例如Oracle
、redis
等,可以按照第二步
的方法操作。
JDBC4.0之ServiceLoader
MySQL驱动源码
JDBC4.0开始使用ServiceLoader的方式加载数据库连接的驱动,导入mysql-connector-java-xxxx.jar
包之后,连接MySQL
的代码如下:
package com.java.www.day20210111.dbconnection;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
/**
* @ClassName MySQLConnectionTest
* @Description TODO
* @Date 2021/1/11 19:48
*/
public class MySQLConnectionTest {
private final static String DB_URL = "jdbc:mysql://ip:3306/dbname?useSSL=true&useUnicode=true&characterEncoding=utf-8";
private final static String DB_USER = "root";
private final static String DB_PASSWD = "123456";
public static void main(String[] args) {
try {
// Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWD);
System.out.println(connection);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
JDBC4.0
之后就不需要Class.forName("com.mysql.jdbc.Driver");
这句代码了,因为已经通过ServiceLoader
的方式进行加载了。我们既然已经知道了ServiceLoader
可以通过配置文件加载类,去查找一下mysql-connector-java-xxxx.jar
包,展开如下图所示:
META-INFservices
目录下的文件名为java.sql.Driver
,根据前面的讨论可知,这是SPI
接口名,说明在DriverManager
类当中通过SeriviceLoader
加载了java.sql.Driver
这个接口,这是JDK
的核心类库,点进源码看一下:
package java.sql;
import java.util.logging.Logger;
public interface Driver {
Connection connect(String url, java.util.Properties info)
throws SQLException;
boolean acceptsURL(String url) throws SQLException;
DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
throws SQLException;
// 省略代码
}
java.sql.Driver
接口当中,有很多方法。DriverManager
获取连接对象调用的是connect()
方法,因此可以推断:MySQL驱动对这个接口的实现类当中一定有这个方法的实现,因此再看一下mysql-connector-java-xxxx.jar
包的配置文件的内容:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
对于com.mysql.jdbc.Driver
而言,加载的类是com.mysql.jdbc
包下的Driver.java
的字节码文件,点进字节码看一下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.mysql.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
这个类名和其实现的java.sql.Driver
同名(没关系),但没有实现java.sql.Driver
的connect()
等方法,所以应该是在其父类NonRegisteringDriver
当中有实现,点开源码可以看到有SPI
接口java.sql.Driver
中的方法:
看一下connect()
方法源码:
鉴于我们讨论的是ServiceLoader
,这个源码先不看。
回到com.mysql.jdbc.Driver.class
字节码中,静态代码块里面调用了一句代码:
DriverManager.registerDriver(new Driver());
即注册了一个Driver
实例,点进registerDriver()
源码:
registerDriver()
方法中,将当前的这个Driver()
实例添加到registeredDrivers
中,registeredDrivers
是一个全局的集合对象:
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers =
new CopyOnWriteArrayList<>();
源码看到这里,居然还没谈到ServiceLoader
。再看看DriverManager.java
,代码中搜索一下ServiceLoader.load
或者Driver.class
,发现:
这是明显在加载SPI
接口的class
对象,这个loadInitialDrivers()
是在静态代码块中执行的:
打破双亲委派之线程上下文加载器
在上面的代码中,Driver.class
是一个class
对象被ServiceLoader.load()
加载了,说明ServiceLoader.load()
应该有一个类加载器,点进load()
源码:
可以看到关键代码:Thread.currentThread().getContextClassLoader();
,下面捋一捋:
DriverManager.java
是java.sql
下的类,属于JDK核心类库,只能由根加载器加载:
com.mysql.jdbc.Driver.class
是第三方类,只能由应用加载器加载。
我们看到的是在DriverManager.java
当中加载了java.sql.Driver
,虽然java.sql.Driver
也是JDK核心类库,但我们知道本质上是加载的第三方类com.mysql.jdbc.Driver.class
,问题是DriverManager.java
是只能由根加载器加载,那么它为什么可以去加载第三方类库呢?
以前,我们需要通过Class.forName("com.mysql.jdbc.Driver");
加载,这是通过应用加载器加载的。JDBC4.0
之后就不需要这行代码了,因为DriverManager.java
中有这句代码:
Thread.currentThread().getContextClassLoader();
测一下这句代码可以得到什么。
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println("loader = " + loader);
这句代码获取的是应用加载器:
loader = sun.misc.Launcher$AppClassLoader@18b4aac2
Process finished with exit code 0
这就是线程上下文类加载器
,虽然只有一句代码,但是这种办法实现了在父加载器中加载了子加载器,弥补了双亲委派机制的缺陷。使用线程上下文类加载器
是打破双亲委派机制的两种方式之一。
至于接下来,当前线程的应用加载器如何加载第三方的类,可以再继续看load()
源码,发现是通过LazyIterator
进行懒加载的。
最后
以上就是完美水杯为你收集整理的[设计与架构]Java当中的SPI(Service Provider Interface)设计思想(由双亲委派引出的问题)导言SPI的实现之ServiceLoaderJDBC4.0之ServiceLoader的全部内容,希望文章能够帮你解决[设计与架构]Java当中的SPI(Service Provider Interface)设计思想(由双亲委派引出的问题)导言SPI的实现之ServiceLoaderJDBC4.0之ServiceLoader所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复