Java是如何实现自己的SPI机制的?
1 引言
本篇我们来探究Java的何实SPI机制的相关源码。
2 什么是现自SPI机制
那么,什么是己的I机SPI机制呢?
SPI是Service Provider Interface 的简称,即服务提供者接口的何实意思。根据字面意思我们可能还有点困惑,现自SPI说白了就是己的I机一种扩展机制,我们在相应配置文件中定义好某个接口的何实实现类,然后再根据这个接口去这个配置文件中加载这个实例类并实例化,现自其实SPI就是己的I机这么一个东西。说到SPI机制,何实我们最常见的现自就是Java的SPI机制,此外,己的I机还有Dubbo和SpringBoot自定义的何实SPI机制。
有了SPI机制,现自那么就为一些框架的己的源码库I机灵活扩展提供了可能,而不必将框架的一些实现类写死在代码里面。
那么,某些框架是如何利用SPI机制来做到灵活扩展的呢?下面举几个栗子来阐述下:
JDBC驱动加载案例:利用Java的SPI机制,我们可以根据不同的数据库厂商来引入不同的JDBC驱动包; SpringBoot的SPI机制:我们可以在spring.factories中加上我们自定义的自动配置类,事件监听器或初始化器等; Dubbo的SPI机制:Dubbo更是把SPI机制应用的淋漓尽致,Dubbo基本上自身的每个功能点都提供了扩展点,比如提供了集群扩展,路由扩展和负载均衡扩展等差不多接近30个扩展点。如果Dubbo的某个内置实现不符合我们的需求,那么我们只要利用其SPI机制将我们的实现替换掉Dubbo的实现即可。上面的三个栗子先让我们直观感受下某些框架利用SPI机制是如何做到灵活扩展的。
3 如何使用Java的SPI?
我们先来看看如何使用Java自带的香港云服务器SPI。先定义一个Developer接口
// Developer.java package com.ymbj.spi; public interface Developer { void sayHi(); }再定义两个Developer接口的两个实现类:
// JavaDeveloper.java package com.ymbj.spi; public class JavaDeveloper implements Developer { @Override public void sayHi() { System.out.println("Hi, I am a Java Developer."); } } // PythonDeveloper.java package com.ymbj.spi; public class PythonDeveloper implements Developer { @Override public void sayHi() { System.out.println("Hi, I am a Python Developer."); } }然后再在项目resources目录下新建一个META-INF/services文件夹,然后再新建一个以Developer接口的全限定名命名的文件,文件内容为:
// com.ymbj.spi.Developer文件 com.ymbj.spi.JavaDeveloper com.ymbj.spi.PythonDeveloper最后我们再新建一个测试类JdkSPITest:
// JdkSPITest.java public class JdkSPITest { @Test public void testSayHi() throws Exception { ServiceLoader<Developer> serviceLoader = ServiceLoader.load(Developer.class); serviceLoader.forEach(Developer::sayHi); } }运行上面那个测试类,运行成功结果如下截图所示:
由上面简单的Demo我们知道了如何使用Java的SPI机制来实现扩展点加载。
4 Java的SPI机制的源码分析
通过前面扩展Developer接口的简单Demo,我们看到Java的SPI机制实现跟ServiceLoader这个类有关,那么我们先来看下ServiceLoader的类结构代码:
// ServiceLoader实现了【Iterable】接口 public final class ServiceLoader<S> implements Iterable<S>{ private static final String PREFIX = "META-INF/services/"; // The class or interface representing the service being loaded private final Class<S> service; // The class loader used to locate, load, and instantiate providers private final ClassLoader loader; // The access control context taken when the ServiceLoader is created private final AccessControlContext acc; // Cached providers, in instantiation order private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); // The current lazy-lookup iterator private LazyIterator lookupIterator; // 构造方法 private ServiceLoader(Class<S> 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(); } // ...暂时省略相关代码 // ServiceLoader的内部类LazyIterator,实现了【Iterator】接口 // Private inner class implementing fully-lazy provider lookup private class LazyIterator implements Iterator<S>{ Class<S> service; ClassLoader loader; Enumeration<URL> configs = null; Iterator<String> pending = null; String nextName = null; private LazyIterator(Class<S> service, ClassLoader loader) { this.service = service; this.loader = loader; } // 覆写Iterator接口的hasNext方法 public boolean hasNext() { // ...暂时省略相关代码 } // 覆写Iterator接口的next方法 public S next() { // ...暂时省略相关代码 } // 覆写Iterator接口的remove方法 public void remove() { // ...暂时省略相关代码 } } // 覆写Iterable接口的iterator方法,返回一个迭代器 public Iterator<S> iterator() { // ...暂时省略相关代码 } // ...暂时省略相关代码 }可以看到,ServiceLoader实现了Iterable接口,覆写其iterator方法能产生一个迭代器;同时ServiceLoader有一个内部类LazyIterator,而LazyIterator又实现了Iterator接口,说明LazyIterator是一个迭代器。
4.1 ServiceLoader.load方法,为加载服务提供者实现类做前期准备
那么我们现在开始探究Java的SPI机制的源码,亿华云 先来看JdkSPITest的第一句代码ServiceLoader<Developer> serviceLoader = ServiceLoader.load(Developer.class);中的ServiceLoader.load(Developer.class)的源码:
// ServiceLoader.java public static <S> ServiceLoader<S> load(Class<S> service) { //获取当前线程上下文类加载器 ClassLoader cl = Thread.currentThread().getContextClassLoader(); // 将service接口类和线程上下文类加载器作为参数传入,继续调用load方法 return ServiceLoader.load(service, cl); }我们再来看下ServiceLoader.load(service, cl);方法:
// ServiceLoader.java public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) { // 将service接口类和线程上下文类加载器作为构造参数,新建了一个ServiceLoader对象 return new ServiceLoader<>(service, loader); }继续看new ServiceLoader<>(service, loader);是如何构建的?
// ServiceLoader.java private ServiceLoader(Class<S> 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(); }可以看到在构建ServiceLoader对象时除了给其成员属性赋值外,还调用了reload方法:
// ServiceLoader.java public void reload() { providers.clear(); lookupIterator = new LazyIterator(service, loader); }可以看到在reload方法中又新建了一个LazyIterator对象,然后赋值给lookupIterator。
// ServiceLoader$LazyIterator.java private LazyIterator(Class<S> service, ClassLoader loader) { this.service = service; this.loader = loader; }可以看到在构建LazyIterator对象时,也只是给其成员变量service和loader属性赋值呀,我们一路源码跟下来,也没有看到去META-INF/services文件夹加载Developer接口的实现类!这就奇怪了,我们都被ServiceLoader的load方法名骗了。
还记得分析前面的代码时新建了一个LazyIterator对象吗?Lazy顾名思义是懒的意思,Iterator就是迭代的意思。我们此时猜测那么LazyIterator对象的作用应该就是在迭代的时候再去加载Developer接口的实现类了。
4.2 ServiceLoader.iterator方法,实现服务提供者实现类的懒加载
我们现在再来看JdkSPITest的第二句代码serviceLoader.forEach(Developer::sayHi);,执行这句代码后最终会调用serviceLoader的iterator方法:
// serviceLoader.java public Iterator<S> iterator() { return new Iterator<S>() { Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator(); public boolean hasNext() { if (knownProviders.hasNext()) return true; // 调用lookupIterator即LazyIterator的hasNext方法 // 可以看到是委托给LazyIterator的hasNext方法来实现 return lookupIterator.hasNext(); } public S next() { if (knownProviders.hasNext()) return knownProviders.next().getValue(); // 调用lookupIterator即LazyIterator的next方法 // 可以看到是委托给LazyIterator的next方法来实现 return lookupIterator.next(); } public void remove() { throw new UnsupportedOperationException(); } }; }可以看到调用serviceLoader的iterator方法会返回一个匿名的迭代器对象,而这个匿名迭代器对象其实相当于一个门面类,其覆写的hasNext和next方法又分别委托LazyIterator的hasNext和next方法来实现了。
我们继续调试,发现接下来会进入LazyIterator的hasNext方法:
// serviceLoader$LazyIterator.java public boolean hasNext() { if (acc == null) { // 调用hasNextService方法 return hasNextService(); } else { PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } }继续跟进hasNextService方法:
// serviceLoader$LazyIterator.java private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { // PREFIX = "META-INF/services/" // service.getName()即接口的全限定名 // 还记得前面的代码构建LazyIterator对象时已经给其成员属性service赋值吗 String fullName = PREFIX + service.getName(); // 加载META-INF/services/目录下的接口文件中的服务提供者类 if (loader == null) configs = ClassLoader.getSystemResources(fullName); else // 还记得前面的代码构建LazyIterator对象时已经给其成员属性loader赋值吗 configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } // 返回META-INF/services/目录下的接口文件中的服务提供者类并赋值给pending属性 pending = parse(service, configs.nextElement()); } // 然后取出一个全限定名赋值给LazyIterator的成员变量nextName nextName = pending.next(); return true; }可以看到在执行LazyIterator的hasNextService方法时最终将去META-INF/services/目录下加载接口文件的内容即加载服务提供者实现类的全限定名,然后取出一个服务提供者实现类的全限定名赋值给LazyIterator的成员变量nextName。到了这里,我们就明白了LazyIterator的作用真的是懒加载,在用到的时候才会真正去加载服务提供者实现类。
思考:为何这里要用懒加载呢?懒加载的思想是怎样的呢?懒加载有啥好处呢?你还能举出其他懒加载的案例吗?
同样,执行完LazyIterator的hasNext方法后,会继续执行LazyIterator的next方法:
// serviceLoader$LazyIterator.java public S next() { if (acc == null) { // 调用nextService方法 return nextService(); } else { PrivilegedAction<S> action = new PrivilegedAction<S>() { public S run() { return nextService(); } }; return AccessController.doPrivileged(action, acc); } }我们继续跟进nextService方法:
// serviceLoader$LazyIterator.java private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); // 还记得在hasNextService方法中为nextName赋值过服务提供者实现类的全限定名吗 String cn = nextName; nextName = null; Class<?> c = null; try { // 【1】去classpath中根据传入的类加载器和服务提供者实现类的全限定名去加载服务提供者实现类 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 { // 【2】实例化刚才加载的服务提供者实现类,并进行转换 S p = service.cast(c.newInstance()); // 【3】最终将实例化后的服务提供者实现类放进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 }可以看到LazyIterator的nextService方法最终将实例化之前加载的服务提供者实现类,并放进providers集合中,随后再调用服务提供者实现类的方法(比如这里指JavaDeveloper的sayHi方法)。注意,这里是加载一个服务提供者实现类后,若main函数中有调用该服务提供者实现类的方法的话,紧接着会调用其方法;然后继续实例化下一个服务提供者类。
因此,我们看到了ServiceLoader.iterator方法真正承担了加载并实例化META-INF/services/目录下的接口文件里定义的服务提供者实现类。
设计模式:可以看到,Java的SPI机制实现代码中应用了迭代器模式,迭代器模式屏蔽了各种存储对象的内部结构差异,提供一个统一的视图来遍历各个存储对象(存储对象可以为集合,数组等)。java.util.Iterator也是迭代器模式的实现:同时Java的各个集合类一般实现了Iterable接口,实现了其iterator方法从而获得Iterator接口的实现类对象(一般为集合内部类),然后再利用Iterator对象的实现类的hasNext和next方法来遍历集合元素。
5 JDBC驱动加载源码解读
前面分析了Java的SPI机制的源码实现,现在我们再来看下Java的SPI机制的实际案例的应用。
我们都知道,JDBC驱动加载是Java的SPI机制的典型应用案例。JDBC主要提供了一套接口规范,而这套规范的api在java的核心库(rt.jar)中实现,而不同的数据库厂商只要编写符合这套JDBC接口规范的驱动代码,那么就可以用Java语言来连接数据库了。
java的核心库(rt.jar)中跟JDBC驱动加载的最核心的接口和类分别是java.sql.Driver接口和java.sql.DriverManager类,其中java.sql.Driver是各个数据库厂商的驱动类要实现的接口,而DriverManager是用来管理数据库的驱动类的,值得注意的是DriverManager这个类有一个registeredDrivers集合属性,用来存储Mysql的驱动类。
// DriverManager.java // List of registered JDBC drivers private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();这里以加载Mysql驱动为例来分析JDBC驱动加载的源码。
我们的项目引入mysql-connector-java依赖(这里的版本是5.1.47)后,那么Mysql的驱动实现类文件如下图所示:
可以看到Mysql的驱动包中有两个Driver驱动类,分别是com.mysql.jdbc.Driver和com.mysql.fabric.jdbc.FabricMySQLDriver,默认情况下一般我们只用到前者。
5.1 利用Java的SPI加载Mysql的驱动类
那么接下来我们就来探究下JDBC驱动加载的代码是如何实现的。
先来看一下一个简单的JDBC的测试代码:
// JdbcTest.java public class JdbcTest { public static void main(String[] args) { Connection connection = null; Statement statement = null; ResultSet rs = null; try { // 注意:在JDBC 4.0规范中,这里可以不用再像以前那样编写显式加载数据库的代码了 // Class.forName("com.mysql.jdbc.Driver"); // 获取数据库连接,注意【这里将会加载mysql的驱动包】 /