转载自https://mp.weixin.qq.com/s?__biz=MzIxNDY0MTg2MA==&mid=2247483935&idx=1&sn=e6da46cfe2df2812fd2b9e24253ec246&chksm=97a53fb4a0d2b6a2896b5c0850e83a7852ad08fbe0939bb61d04982bc0d03d3f6da25ee56dbf&scene=21#wechat_redirect
SPI(Service Provider Interface)是JDK内置的一种提供服务发现的机制。如果你读过dubbo的源码,你就一定对SPI机制不陌生,Dubbo基于SPI机制提供了很多扩展功能,实现了微内核+插件的体系。如果你没有用过dubbo,没关系,JDBC你总该用过吧,还记得创建连接的写法吗?
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
只需要一行代码,(都不需要指定Class.forName("com.mysql.jdbc.Driver");) 再提供商不同厂商的jar包,就可以轻松创建连接了,这其中的奥秘就要归功于SPI机制。
在java中根据一个子类获取其父类或接口信息非常方便,但是根据一个接口获取该接口的所有实现类却没那么容易。有一种比较笨的办法就是扫描classpath下所有的class与jar包中的class,接着用ClassLoader加载进来,再判断是否是给定接口的子类。但是这种方法的代价太大,一般不会使用。
根据这个问题,java推出了ServiceLoader类来提供服务发现机制,动态的为某个接口寻找服务实现,这种机制有点类似IOC思想,将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
当服务的提供者提供了服务接口的一种实现之后,必须根据SPI约定在 META-INF/services/
目录里创建一个以服务接口命名的文件,该文件里写的就是实现该服务接口的具体实现类。当程序调用ServiceLoader的load方法的时候,ServiceLoader能够通过约定的目录找到指定的文件,并装载实例化,完成服务的发现。
我们通过一个例子来加深对SPI机制的理解:
首先我们提供一个接口类 Animal
以及它的两个实现类 Dog和Pig
(都在包com.github.spi下):
public interface Animal {
void eat();
}
public class Dog implements Animal {
@Override
public void eat() {
System.out.println("Dog eating...");
}
}
public class Pig implements Animal {
@Override
public void eat() {
System.out.println("Pig eating...");
}
}
接着在classpath下创建文件夹 META-INF/services
,在文件夹中新建一个文件 com.github.spi.Animal
并在文件中写入具体的实现类:
com.github.spi.Pig
com.github.spi.Dog
接着,我们就可以利用ServiceLoader进行服务发现了:
public class SPITest {
public static void main(String[] args) {
ServiceLoader
Iterator
while (iterator.hasNext()) {
Animal animal = iterator.next();
animal.eat();
}
}
}
结果验证了我们的猜想,dog和pig的eat方法依次被调用了。我们顺着好奇心看看load方法是如何实现的。
public static
ServiceLoader load(Class service) {
// 获取当前调用线程的类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static
ServiceLoader load(Class service, ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}
load方法仅仅获取了当前调用线程的类加载器实例化之后就返回了。我们接着看iterator方法做了什么:
public Iterator
iterator() {
return new Iterator
() {
// 缓存第一次查找发现的服务类,下次再进行遍历直接返回
Iterator
= providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
hasNext和next方法都调用了lookupIterator,这个类是在load的时候调用构造函数实例化的时候初始化的。看类名就能知道它是懒加载的意思(LazyIterator):
private ServiceLoader(Class
svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
// 重新加载
reload();
}
public void reload() {
// 清空缓存
providers.clear();
// 初始化LazyIterator
lookupIterator = new LazyIterator(service, loader);
}
我们直接看LazyIterator里对应hasNext和next的两个方法:
Class
service;
ClassLoader loader;
Enumeration
Iterator
String nextName = null;
private LazyIterator(Class
service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// 通过类加载器加载classpath:META-INF/services/serviceName
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 解析文件里的值,多行为多个值,返回一个Iterator
pending = parse(service, configs.nextElement());
}
// 下一个要获取的实现类类名全称
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
// 拿到hasNext赋值的nextName
String cn = nextName;
nextName = null;
Class> c = null;
try {
// 加载类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service, "Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
// 放入providers里面,下次遍历的时候可以直接用
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service, "Provider " + cn + " could not be instantiated", x);
}
throw new Error(); // This cannot happen
}
系统的ServiceLoader通过返回一个Iterator对象能够做到对服务实例的懒加载,只有当调用iterator.next()方法时才会实例化下一个服务实例,只有需要使用的时候才进行实例化。
看到这里,应该明白了ServiceLoader所干的事了。首先根据约定的包获取到对应的接口文件,接着解析出文件中的所有服务实现类并加载实例化。
回到之前的一个问题,为什么只需要下面的一行代码,再提供商不同厂商的jar包,就可以轻松创建连接了呢?
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
DriverManager中有一个静态代码块,在调用getConnection之前就会被调用。
static {
loadInitialDrivers();
}
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction
public String run() {
// 1、处理系统属性jdbc.drivers配置的值
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction
public Void run() {
// 2、处理通过ServiceLoader加载的Driver类
ServiceLoader
Iterator
// 加载配置在META-INF/services/java.sql.Driver文件里的Driver实现类
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// 忽略异常
}
return null;
}
});
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
for (String aDriver : driversList) {
try {
// 3、加载driver类
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
}
}
}
这个代码主要分三块来看:
第一部分:当用户配置了 jdbc.drivers
时,获取到对应的值,步骤3有用。
第二部分:通过ServiceLoader加载Driver类,得到所有不同数据库厂商的Driver类。
比如你引入了mysql的jar包 mysql-connector-java
,打开jar包后会发现它按照ServiceLoader的要求提供了 META-INF/services
包,并且包下面有一个叫 java.sql.Driver
的文件,文件的内容为: com.mysql.jdbc.Driver
,当然如果你还引入了oracle的jar包,你会发现它也有一个一样的文件,不过文件的内容为: oracle.jdbc.OracleDriver
。也就是load这一步会把所有配置的Driver类都获取到。
接着拿到所有的驱动类后进行迭代并调用next加载驱动类,所以触发了类加载,我们以mysql的Driver类来看:
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
它反过来调用了DriverManager的registerDriver方法来注册了自己的Driver类。
// 缓存已注册的Driver类
private final static CopyOnWriteArrayList
public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException {
registerDriver(driver, null);
}
public static synchronized void registerDriver(java.sql.Driver driver, DriverAction da) throws SQLException {
if(driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
throw new NullPointerException();
}
}
因为注册Driver类是一次性操作,后面都是读操作,所以这里用了CopyOnWriteArrayList这样一个应用在读多写少场景的并发List。
第三部分:如果拿到了第一部分的值,根据 :
拆分多个驱动实现类,并手动调用Class.forName进行类的加载,从而让不同的驱动类调用刚才说过的registerDriver方法。
因为第二部分已经做过同样的事,所以用户没有必要配置 jdbc.drivers
。
回到最开始的调用,我们来看看getConnection方法,因为所有的获取连接都会调用下面这个方法,这里就只列出它来。
private static Connection getConnection(
String url, java.util.Properties info, Class> caller) throws SQLException {
// 获取调用类的ClassLoader
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {
// 校验调用类是否有权限加载Driver
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
// 调用connect建立连接
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
}
}
// if we got here nobody could connect.
if (reason != null) {
throw reason;
}
throw new SQLException("No suitable driver found for "+ url, "08001");
}
首先拿到调用类的classLoader和缓存的驱动类的classLoader进行比较,是同一个classLoader才放行,继续调用connect建立连接,下面是isDriverAllowed的源码。
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class> aClass = null;
try {
// 使用传入的类加载器进行driver类的加载,后面会把加载完的类和driver的类进行比较
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
// 比较是否是同一个类加载器加载的class,如果是同一个返回true允许加载driver,否则不允许
result = ( aClass == driver.getClass() ) ? true : false;
}
return result;
}
最后我们回过头来看看这发生的一切,是不是豁然开朗了许多。JDBC使用了SPI机制,让所有的任务都交给不同的数据库厂商各自去完成,无论是实现Driver接口,还是SPI要求的接口文件,都做到了让用户不需要关心一点细节,一行代码建立连接,So Cool~