前言
做过java web开发的小伙伴大多数时候都需要链接数据库,这个时候就需要配置数据库引擎DriverClassName
参数,这样我们的java应用才能通过数据库厂商给的Driver
与指定的数据库建立通信。但是这里就有一个疑问:
java.sql.Driver
是jdk自带的接口,它是由BoostrapClassLoader加载的,DriverClassName
是外部厂商提供的具体实现,是由AppClassLoader加载的,要建立与数据库的通信,必然是通过java.sql.Driver
接口方法发起的,那么在java.sql.Driver
是如何拿到具体实现的呢?它是不是违背了ClassLoader的双亲委派模式呢?
如何绕过双亲委派模式
为了拿到AppClassLoader中加载的java.sql.Driver
实现类,我们可以查看一下DriverManager
是怎么处理的:
public static Driver getDriver(String url) throws SQLException { println("DriverManager.getDriver("" + url + "")"); ensureDriversInitialized(); ...... } private static void ensureDriversInitialized() { ...... AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { // 核心代码ServiceLoader ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); try { while (driversIterator.hasNext()) { driversIterator.next(); } } catch (Throwable t) { // Do nothing } return null; } }); ...... } 复制代码
我们最终可以发现,DriverManager
通过ServiceLoader.load(Driver.class)
就拿到了我们配置的DriverClassName
实现类。这就实现在DriverManager
中拿到了外部提供的Driver
实现,绕过来双亲委派模式。
ServiceLoader实现机制
我们来看一下ServiceLoader
是如何实现SPI机制的,先从ServiceLoader.load()
方法入手:
public static <S> ServiceLoader<S> load(Class<S> service) { // 从当前线程中获取ClassLoader ClassLoader cl = Thread.currentThread().getContextClassLoader(); // 创建一个ServiceLoader对象 return new ServiceLoader<>(Reflection.getCallerClass(), service, cl); } 复制代码
1.从当前线程中获取ClassLoader;因为在创建AppClassLoader后,将AppClassLoader设置进当前线程的上下文中;
2.根据ClassLoader以及目标接口类创建一个ServiceLoader对象;
其实ServiceLoader
核心代码在hasNext()
方法中:
@Override public boolean hasNext() { if (acc == null) { return hasNextService(); } else { PrivilegedAction<Boolean> action = new PrivilegedAction<>() { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } 复制代码
最终都会调用到hasNextService()
方法中:
private boolean hasNextService() { // nextProvider默认为null,如果通过next()取出来了,nextProvider就会变成null while (nextProvider == null && nextError == null) { try { // 找到目标实现类 Class<?> clazz = nextProviderClass(); if (clazz == null) return false; if (clazz.getModule().isNamed()) { // ignore class if in named module continue; } // 判断service接口是否和clazz有父子关系 if (service.isAssignableFrom(clazz)) { Class<? extends S> type = (Class<? extends S>) clazz; // 获取无参构造函数 Constructor<? extends S> ctor = (Constructor<? extends S>)getConstructor(clazz); // 包装成一个ProviderImpl对象 ProviderImpl<S> p = new ProviderImpl<S>(service, type, ctor, acc); // 并赋值给nextProvider nextProvider = (ProviderImpl<T>) p; } else { fail(service, clazz.getName() + " not a subtype"); } } catch (ServiceConfigurationError e) { nextError = e; } } return true; } 复制代码
外部提供的实现类一定要有一个无参构造函数,否则会导致ServiceLoader加载失败;
我们下面再继续深入看看ServiceLoader
是怎么找到实现类的:
private Class<?> nextProviderClass() { if (configs == null) { try { // 拼接文件名:"META-INF/services/接口名称" // 比如接口名为:java.sql.Driver, // 那么文件路径就是:"META-INF/services/java.sql.Driver" String fullName = PREFIX + service.getName(); // 没有指定ClassLoader,就通过getSystemClassLoader()加载目标文件 if (loader == null) { configs = ClassLoader.getSystemResources(fullName); } else if (loader == ClassLoaders.platformClassLoader()) { // 如果是platformClassLoader,它没有class path,那么看看BootLoader有没有class path if (BootLoader.hasClassPath()) { configs = BootLoader.findResources(fullName); } else { configs = Collections.emptyEnumeration(); } } else { // 通过指定classLoader加载目标文件 configs = loader.getResources(fullName); } } catch (IOException x) { fail(service, "Error locating configuration files", x); } } // 上面代码只会执行一次,这样configs就不会为null,下次进来直接取下一个实现类 // 把configs内容解析成一个迭代器 while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return null; } pending = parse(configs.nextElement()); } // 通过迭代器获取下一个实现类名称 String cn = pending.next(); try { // 通过类名反射成Class对象 return Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); return null; } } 复制代码
1.实现类的载入是因为在
META-INF/services/
文件夹中创建了以目标接口名称命名的文件,并在里面写上了实现类的全路径类名。2.ServiceLoader通过ClassLoader从class path中载入目标文件里面的内容,并解析出实现类的全路径类名;
3.最终通过反射的方式创建出实现类的Class对象,这样就完成了SPI的实现;
SPI在各个框架上的应用
除了在数据库Driver
上使用了SPI
,我们还可以发现SPI
在各个框架上都有大量的应用。比如我最近在看的Seata分布式事务框架,里面就有用到SPI
:io.seata.common.loader.EnhancedServiceLoader
另一个就是我们经常使用的mysql-connector-java以及阿里的Druid:
小结
通过以上源码分析以及示例演示,我们简单做一个小结:
1.ServiceLoader打破双亲委派模式的方式通过获取当前线程上下文中的ClassLoader完成的;
2.
SPI
的实现类名称必须放在META-INF/services/
文件夹下面,以目标接口名称作为文件名称,文件内容为目标实现类全路径类名;3.目标实现类必须要有一个无参构造函数,否则
SPI
会失败;