Java SPI機制與Thread Context Classloader

SPI是什麼

SPI(Service Provider Interface),是JDK內置的一種 服務提供發現機制,能夠用來啓用框架擴展和替換組件,主要是被框架的開發人員使用,好比java.sql.Driver接口,其餘不一樣廠商能夠針對同一接口作出不一樣的實現,MySQL和PostgreSQL都有不一樣的實現提供給用戶,而Java的SPI機制能夠爲某個接口尋找服務實現。Java中SPI機制主要思想是將裝配的控制權移到程序以外,在模塊化設計中這個機制尤爲重要,其核心思想就是 解耦java

SPI總體機制圖以下:mysql

Java SPI機制與Thread Context Classloader

當服務的提供者提供了一種接口的實現以後,須要在classpath下的META-INF/services/目錄裏建立一個以服務接口命名的文件,這個文件裏的內容就是這個接口的具體的實現類。當其餘的程序須要這個服務的時候,就能夠經過查找這個jar包(通常都是以jar包作依賴)的META-INF/services/中的配置文件,配置文件中有接口的具體實現類名,能夠根據這個類名進行加載實例化,就可使用該服務了。JDK中查找服務的實現的工具類是:java.util.ServiceLoader。sql

應用場景

SPI擴展機制應用場景有不少,好比Common-Logging,JDBC,Dubbo,Cipher等等。數據庫

SPI流程:bootstrap

  1. 定義接口標準
  2. 第三方提供具體實現: 實現具體方法, 配置 META-INF/services/${interface_name} 文件
  3. 開發者使用

好比JDBC場景下:tomcat

  • 首先在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。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,而後內容爲兩個實現類。

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。

相關文章
相關標籤/搜索