您现在的位置是:首页 > 文章详情

Java SPI机制与Thread Context Classloader

日期:2021-04-10点击:321

SPI是什么

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

SPI整体机制图如下:

Java SPI机制与Thread Context Classloader

当服务的提供者提供了一种接口的实现之后,需要在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,然后内容为两个实现类。

Java SPI机制与Thread Context Classloader

内容如下:

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 }

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这个机制不太好理解,我们 先来看⼀下双亲委派机制。

Java SPI机制与Thread Context Classloader

底层的类加载器要加载⼀个类时,先向上委托,有没有发现⼀个特点, 这种双亲委派机制,直接⽗加载器是唯⼀的,所以向上委托,是不会有⼆义性的(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。

原文链接:https://blog.51cto.com/nxlhero/2697891
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章