Java SPI机制与Thread Context Classloader

2021/4/10 18:13:02

本文主要是介绍Java SPI机制与Thread Context Classloader,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

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。



这篇关于Java SPI机制与Thread Context Classloader的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程