AI智能
改变未来

Java SPI机制与Thread Context Classloader


SPI是什么

SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦

SPI整体机制图如下:

当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的实现的工具类是:java.util.ServiceLoader。

应用场景

SPI扩展机制应用场景有很多,比如Common-Logging,JDBC,Dubbo,Cipher等等。

SPI流程:

  1. 定义接口标准
  2. 第三方提供具体实现: 实现具体方法, 配置 META-INF/services/${interface_name} 文件
  3. 开发者使用

比如JDBC场景下:

  • 首先在Java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商提供。
  • 在MySQL的jar包mysql-connector-java-6.0.6.jar中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现。
  • 同样在PostgreSQL的jar包PostgreSQL-42.0.0.jar中,也可以找到同样的配置文件,文件内容是org.postgresql.Driver,这是PostgreSQL对Java的java.sql.Driver的实现。

我们也可以自定义SPI。

示例

先定义一个接口

package org.ifool.spiDemo;public interface HelloSpi {public void sayHello();}

定义两个实现类

HelloImpl1:

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.HelloImpl1org.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 1Hello 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 1Exception in thread \"main\" java.util.ServiceConfigurationError: org.ifool.spiDemo.HelloSpi: Provider org.ifool.spiDemo.HelloImpl2 could not be instantiatedat 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 loadedprivate final Class<S> service;// The class loader used to locate, load, and instantiate providersprivate final ClassLoader loader;// The access control context taken when the ServiceLoader is createdprivate final AccessControlContext acc;//把得到的Provider实例放到一个LinkedHashMap中// Cached providers, in instantiation orderprivate LinkedHashMap<String,S> providers = new LinkedHashMap<>();// The current lazy-lookup iteratorprivate 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,继续调用reloadprivate 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}

JDBC分析

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。

Thread Context ClassLoader

前⾯多次出现了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的代码

@CallerSensitivepublic 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) throwsSQLException {/** 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。

赞(0) 打赏
未经允许不得转载:爱站程序员基地 » Java SPI机制与Thread Context Classloader