SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,能够用来启用框架扩展和替换组件,主要是被框架的开发人员使用,好比java.sql.Driver接口,其余不一样厂商能够针对同一接口作出不一样的实现,MySQL和PostgreSQL都有不一样的实现提供给用户,而Java的SPI机制能够为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序以外,在模块化设计中这个机制尤为重要,其核心思想就是 解耦。java
SPI总体机制图以下:mysql
当服务的提供者提供了一种接口的实现以后,须要在classpath下的META-INF/services/目录里建立一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其余的程序须要这个服务的时候,就能够经过查找这个jar包(通常都是以jar包作依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,能够根据这个类名进行加载实例化,就可使用该服务了。JDK中查找服务的实现的工具类是:java.util.ServiceLoader。sql
SPI扩展机制应用场景有不少,好比Common-Logging,JDBC,Dubbo,Cipher等等。数据库
SPI流程:bootstrap
好比JDBC场景下:tomcat
咱们也能够自定义SPI。app
先定义一个接口框架
package org.ifool.spiDemo; public interface HelloSpi { public void sayHello(); }
定义两个实现类maven
HelloImpl1:ide
package org.ifool.spiDemo; public class HelloImpl1 implements HelloSpi { public void sayHello() { System.out.println("Hello Impl 1"); } }
HelloImpl2:
package org.ifool.spiDemo; public class HelloImpl2 implements HelloSpi { public void sayHello() { System.out.println("Hello Impl 2"); } }
在META-INF/services(对于maven工程可在src/main/resources下新建META-INF目录)建立文件,名字就是org.ifool.spiDemo.HelloSpi,而后内容为两个实现类。
内容以下:
org.ifool.spiDemo.HelloImpl1 org.ifool.spiDemo.HelloImpl2
也就是说org.ifool.spiDemo.HelloSpi在这里有两个实现类。
main函数中用ServiceLoader进行加载
package org.ifool.spiDemo; import java.util.ServiceLoader; public class App { public static void main(String[] args) { ServiceLoader<HelloSpi> serviceLoader = ServiceLoader.load(HelloSpi.class); for (HelloSpi helloSPI : serviceLoader) { helloSPI.sayHello(); } } }
运行效果以下:
Hello Impl 1 Hello Impl 2
这个ServiceLoader实现了Iterable。
public final class ServiceLoader<S> implements Iterable<S>
也就是说,对于一个定义好的SPI接口,若是在类路径下存在这个接口的实现,那么咱们就能够把使用ServiceLoader把这个接口的实现类都加载进来,而且每一个实现类都放一个实例到这个ServiceLoader,这个实例是经过newInstance()实现的,因此实现类必须有一个无参构造函数。
每一个接口,能够有多个实现类,可是咱们只能顺序的遍历ServiceLoader来逐个获取,无法经过map.get()使用名字获取一个实现对象,并且这个过程是懒加载的,只有真正遍历的时候才会加载并建立实现类。
假如咱们把HelloImpl2修改一下,增长一个有参数的构造函数,就会报错
package org.ifool.spiDemo; public class HelloImpl2 implements HelloSpi { public void sayHello() { System.out.println("Hello Impl 2"); } public HelloImpl2(int a) { a = 5; } }
再次执行会抛异常:
Hello Impl 1 Exception in thread "main" java.util.ServiceConfigurationError: org.ifool.spiDemo.HelloSpi: Provider org.ifool.spiDemo.HelloImpl2 could not be instantiated at java.util.ServiceLoader.fail(ServiceLoader.java:232)
这个异常并非在ServiceLoader.load(HelloSpi.class)时抛的,由于HelloImpl1已经被实例化了,而是在遍历ServiceLoader时抛的异常,说明实现类的加载和建立是lazy模式的。
大多数框架使用ServiceLoader的时候,并不必定须要建立的这个对象,只是须要它作类的加载以及一些初始化工做。下面分析一下是怎么实现的。
ServiceLoader这个类不复杂,调用它加载接口的实现类时,它会到各个jar包中的META-INF/services中寻找实现类,使用class.forName加载类,而后用newInstance得到一个实例,再放到Map中,由于这个Map是private的,因此外界无法使用get方法获取实例。
下面是它的成员变量,能够看到META-INF/services是写死在代码里的。
public final class ServiceLoader<S> implements Iterable<S> { //到META-INF/services中搜索相应的ServiceProvider类名 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; //把获得的Provider实例放到一个LinkedHashMap中 // Cached providers, in instantiation order private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); // The current lazy-lookup iterator private LazyIterator lookupIterator;
它用了一个懒加载的策略,
//在调用ServiceLoader.load(HelloSpi.class)的时候,会传入一个ContextClassLoader,而后继续调用ServiceLoader.load(HelloSpi.class,cl) public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } //这个函数是private的,若是cl为空的话,使用systemClassLoader,继续调用reload 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(); } //reload清空providers,而后新建一个LazyIterator,这个其实只是把实现类的名称记下来了 public void reload() { providers.clear(); lookupIterator = new LazyIterator(service, loader); } //真正要获取Service实例的时候,才会加载类,而且new一个实例放到providers中 private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); 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()); //new一个实例,而且以类名为key放到map里 providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen }
Java的JDBC Driver接口在rt.jar里,名字是java.sql.Driver,它提供了几个接口,主要的是connect接口,而后具体实现是由厂商实现的。
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; int getMajorVersion(); int getMinorVersion(); boolean jdbcCompliant(); public Logger getParentLogger() throws SQLFeatureNotSupportedException; }
咱们能够看到在mysql-connector里的META-INF/services里有一个文件java.sql.Driver,它的实现是com.mysql.cj.jdbc.Driver
com.mysql.cj.jdbc.Driver
咱们使用JDBC的时候,都是从DriverManager开始的,例以下面获取Connection
package org.ifool.spiDemo; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class JDBCDemo { public static void main(String[] args) throws SQLException { //Class.forName("com.mysql.cj.jdbc.Driver"); 已经不须要了 String url = "jdbc:mysql://localhost:3306/mysql?serverTimezone=GMT%2B8"; String user = "root"; String password = "123456"; Connection connections = DriverManager.getConnection(url, user, password); } }
DriverManager在初始化的时候,在它的static代码块中,会使用ServiceLoader加载全部的java.sql.Driver的实现类,这样各个java.sql.Driver的实现类会被加载,它们的static块也会被执行,同时newInstance建立一个实例,可是这里没用到。
/** * Load the initial JDBC drivers by checking the System property * jdbc.properties and then use the {@code ServiceLoader} mechanism */ /** 先使用老的方式,在jdbc.properties里找提供者,这是为了兼容老版本,再使用ServiceLoader机制**/ static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } //使用ServiceLoader加载实现类,必须调用iterator.next()才会真正加载,由于是懒加载的,这里的做用只是加载一下类而已,实际没用到初始化的实例,可是类里的static块会被执行 private static void loadInitialDrivers() { String drivers; try { drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } // If the driver is packaged as a Service Provider, load it. // Get all the drivers through the classloader // exposed as a java.sql.Driver.class service. // ServiceLoader.load() replaces the sun.misc.Providers() AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); /* Load these drivers, so that they can be instantiated. * It may be the case that the driver class may not be there * i.e. there may be a packaged driver with the service class * as implementation of java.sql.Driver but the actual class * may be missing. In that case a java.util.ServiceConfigurationError * will be thrown at runtime by the VM trying to locate * and load the service. * * Adding a try catch block to catch those runtime errors * if driver not available in classpath but it's * packaged as service and that service is there in classpath. */ try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } });
假设咱们的类路径里有mysql和db2的实现类,那么它们都会被初始化,看一下mysql的实现,它在static块中调用了DriverManager把本身注册到了DriverManager中。一样,DB2可能也会作一些相似的操做。
public class Driver extends NonRegisteringDriver implements java.sql.Driver { // // Register ourselves with the DriverManager // static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } } /** * Construct a new driver and register it with DriverManager * */ public Driver() throws SQLException { // Required for Class.forName().newInstance() } }
咱们再看DriverManager的getConnection函数,它会遍历registeredDrivers,选择出Allowed的driver,尝试用这个Driver去connect, 这个过程可能会有用db2的driver去连mysql数据库,可是db2 driver链接的时候,根据url,发现不是db2数据库,则立马返回失败,尝试用下一个driver去连。判断是否Allowed,用的是isDriverAllowed的,这个后面再说。
// Worker method called by the public getConnection() methods. private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws SQLException { /* * When callerCl is null, we should check the application's * (which is invoking this class indirectly) * classloader, so that the JDBC driver class outside rt.jar * can be loaded from here. */ ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; synchronized(DriverManager.class) { // synchronize loading of the correct classloader. if (callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader(); } } if(url == null) { throw new SQLException("The url cannot be null", "08001"); } println("DriverManager.getConnection(\"" + url + "\")"); // Walk through the loaded registeredDrivers attempting to make a connection. // Remember the first exception that gets raised so we can reraise it. SQLException reason = null; for(DriverInfo aDriver : registeredDrivers) { // If the caller does not have permission to load the driver then // skip it. if(isDriverAllowed(aDriver.driver, callerCL)) { try { println(" trying " + aDriver.driver.getClass().getName()); Connection con = aDriver.driver.connect(url, info); if (con != null) { // Success! println("getConnection returning " + aDriver.driver.getClass().getName()); return (con); } } catch (SQLException ex) { if (reason == null) { reason = ex; } } } else { println(" skipping: " + aDriver.getClass().getName()); } } // if we got here nobody could connect. if (reason != null) { println("getConnection failed: " + reason); throw reason; } println("getConnection: no suitable driver found for "+ url); throw new SQLException("No suitable driver found for "+ url, "08001"); }
为何须要在下面的函数里判断⼀下是否容许使用呢?DriverManager管理着JVM⾥全部的Driver,可是同⼀个Driver可能被加载屡次,⽐如tomcat⾥,多个应⽤都会加载mysql的driver,可是DriverManager在选择的时候,必须选择与调⽤者的classloader⼀样的Driver。
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) { boolean result = false; if(driver != null) { Class<?> aClass = null; try { aClass = Class.forName(driver.getClass().getName(), true, classLoader); } catch (Exception ex) { result = false; } result = ( aClass == driver.getClass() ) ? true : false; } return result; }
这个⽅法⽐较特别的地⽅在于,它拿到⼀个Driver,而后判断这个Driver是否是与caller⽤的同⼀个classloader,若是不是的话,那么调⽤forName的时候,正好⼜⽤这个caller的classloader加载了⼀个Driver放到了registeredDrivers⾥⾯,咱们看⼀个实例。
在⼀个tomcat⾥,有两个war包,都⽤的同⼀个版本的mysql驱动,tomcat重启的时候,两个war包会有⼀个先调⽤DriverManager.getConnection(),接着另⼀个调⽤。
war1调⽤DriverManager.getConnection()
DriverManager经过SPI机制把全部的jdbc driver都加载⼀次,这时候使⽤的类加载器是war1的,咱们记做war1loader,DriverManager经过war1loader加载mysql Driver,mysql Driver主动register⾃⼰,这时候registeredDrivers的结果以下:
registeredDrivers={"war1loader: com.mysql.cj.jdbc.Driver"}
接下来,war2调⽤DriverManager.getConnection(),由于Drivermanager已经初始化过了,因此SPI那⼀套流程不会走了。会遍历registeredDrivers,而且判断是不是⾃⼰加载的,war2的加载器为war2loader,在iisDriverAllowed中,会调⽤
class.forName("com.mysql.cj.jdbc.Driver", true, war2loader)
显然这个结果与已经存在的不⼀致,可是,咱们⽤war2loader加载驱动,会再次调⽤Driver的初始化,
它继续调⽤register,因此如今的结果就是
registeredDrivers={"war1loader: com.mysql.cj.jdbc.Driver", "war2loader: com.mysql.cj.jdbc.Driver"}
由于registeredDrivers是CopyOnWriteList,循环会继续往下走,下⼀次就能走过isAllowed,而后能够调⽤connect。
前⾯屡次出现了ContextClassloader,没有展开解释,ContextClassLoader这个机制不太好理解,咱们 先来看⼀下双亲委派机制。
底层的类加载器要加载⼀个类时,先向上委托,有没有发现⼀个特色, 这种双亲委派机制,直接⽗加载器是惟⼀的,因此向上委托,是不会有⼆义性的(OSGI不在讨论范围内)。 可是,假如在上层的类(例如DriverManager,它是由bootstrap classloader加载的)⾥要加载底层的类,它会⽤⾃⼰的加载器去加载,对于SPI来讲,它的实现类都是在下层的,须要由下层的classloader加载,
仍是以DriverManager为例,假设它在⾃⼰的代码⾥调⽤(虽然没有在代码⾥写上mysql,可是只要把mysql的jar包放在这,Drivermanager最终会扫描到而且调⽤class.forName("com.mysql.c.jdbc.driver")的,只是它传了classloader):
class.forName("com.mysql.cj.jdbc.driver");
咱们看forName的代码
@CallerSensitive public static Class<?> forName(String className) throws ClassNotFoundException { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); }
此处会寻找caller的类,而后找它的classloader,DriverManager调⽤的forName,因此此处的caller就是DriverManager.class,可是咱们知道DriverManager是bootstrap加载的,那此处获取classloader就是null。forName0是native⽅法,它发现classloader是null就尝试⽤bootstrap加载,可是咱们要加载的是mysql的类,bootstrap确定是不能加载的。
假设咱们的委派链是个单纯的单链表,那么咱们⽤⼀个双向链表向下委托就⾏了,可是这种机制的委托链并非单链表,因此向下委托是有⼆义性的。
那怎么办呢?谁调⽤我,我就⽤谁的加载器,这个加载器放在哪呢,就跟线程绑定,也就是Thread Context ClassLoader。
因此DriverManager在实际调⽤forName的时候,要⽤ContextClassLoader。 它⼀共有两处会加载类
⼀处是类初始化调⽤ServiceLoader的时候,咱们知道ServiceLoader使⽤的是contextClassloader。
public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
⼀处是getConnection的时候,先检查⼀下caller的classloader,若是是null的话就使⽤ContextClassloader,在isDriverAllowed⾥加载类
// Worker method called by the public getConnection() methods. private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws SQLException { /* * When callerCl is null, we should check the application's * (which is invoking this class indirectly) * classloader, so that the JDBC driver class outside rt.jar * can be loaded from here. */ ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; synchronized(DriverManager.class) { // synchronize loading of the correct classloader. if (callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader(); } } ..... if(isDriverAllowed(aDriver.driver, callerCL)) { .....
Thread Context ClassLoader意义就是:⽗Classloader可使⽤当前线程Thread.currentthread().getContextLoader()中指定的classloader中加载的类。颠覆了⽗ClassLoader不能使⽤⼦Classloader或者是其它没有直接⽗⼦关系的Classloader中加载的类这种状况。这个就是Thread Context ClassLoader的意义。⼀个线程的默认ContextClassLoader是继承⽗线程的,能够调⽤set从新 设置,若是在main线程⾥查看,它就是AppClassLoader。