Java中SPI原理

1 SPI是什麼

SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實現或者擴展的API,它能夠用來啓用框架擴展和替換組件。常見的 SPI 有 JDBC、日誌門面接口、Spring、SpringBoot相關starter組件、Dubbo、JNDI等。java

Java SPI 其實是「基於接口的編程+策略模式+配置文件」組合實現的動態加載機制,在JDK中提供了工具類:「java.util.ServiceLoader」來實現服務查找。mysql

系統設計之初爲了各功能模塊之間解耦,通常都是基於接口編程,模塊之間不對實現類進行硬編碼。一旦代碼裏涉及具體的實現類的耦合,就違反了可插拔、閉開等原則,若是咱們但願實如今模塊裝配的時候可以不在程序硬編碼指定,那就須要一種服務發現的機制(PS:不要和如今微服務的服務發現機制搞混淆了)。web

Java SPI就是提供這樣的一個機制:爲某個接口尋找服務實現的機制。相似IOC的思想,就是將裝配的控制權移到程序以外,在模塊化設計中這個機制尤爲重要。因此SPI的核心思想就是解耦。spring

這些 SPI 的接口由 Java 核心庫來提供(由啓動類加載器Bootstrap Classloader負責加載),而這些 SPI 的實現代碼則是做爲 Java 應用所依賴的 jar 包被包含進類路徑(CLASSPATH)裏。SPI接口中的代碼常常須要加載具體的實現類。那麼問題來了,SPI的接口是Java核心庫的一部分,是由啓動類加載器(Bootstrap Classloader)來加載的;SPI的實現類是由系統類加載器(System ClassLoader)來加載的。啓動類加載器是沒法找到 SPI 的實現類的,由於依照雙親委派模型,BootstrapClassloader沒法委派AppClassLoader來加載類。因而加載SPI實現類的重任就落到了線程上下文類加載器(破壞了「雙親委派模型」,能夠在執行線程中拋棄雙親委派加載鏈模式,使程序能夠逆向使用類加載器)的身上。關於類加載器這部分後面將有文章單獨講,這裏很少說了。sql

總體機制圖以下:
clipboard.png
通俗的講就是JDK提供了一種幫助第三方實現者加載服務(如數據庫驅動、日誌庫)的便捷方式,只要第三方遵循約定(把類名寫在/META-INF/services裏),當服務啓動時就會去掃描全部jar包裏符合約定的類名,再調用forName加載,因爲啓動類加載器無法加載實現類,就把加載它的任務交給了線程上下文類加載器。數據庫

2 使用介紹

要使用Java SPI,須要遵循以下約定:
一、當服務提供者提供了接口的一種具體實現後,在jar包的META-INF/services目錄下建立一個以「接口全限定名」爲命名的文件,內容爲實現類的全限定名;
二、接口實現類所在的jar包放在主程序的classpath中;
三、主程序經過java.util.ServiceLoder動態裝載實現模塊,它經過掃描META-INF/services目錄下的配置文件找到實現類的全限定名,把類加載到JVM;
四、SPI的實現類必須攜帶一個不帶參數的構造方法;apache

3 原理解析

首先看ServiceLoader類的簽名類的成員變量:編程

public final class ServiceLoader<S> implements Iterable<S>{
private static final String PREFIX = "META-INF/services/";

    // 表明被加載的類或者接口
    private final Class<S> service;

    // 用於定位,加載和實例化providers的類加載器
    private final ClassLoader loader;

    // 建立ServiceLoader時採用的訪問控制上下文
    private final AccessControlContext acc;

    // 緩存providers,按實例化的順序排列
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 懶查找迭代器(內部類,真正加載服務類)
    private LazyIterator lookupIterator;
    ......
}

參考具體ServiceLoader具體源碼,代碼量很少,梳理了一下,實現的流程以下:緩存

  1. 應用程序調用ServiceLoader.load方法

load方法建立了一些屬性,重要的是實例化了內部類,LazyIterator。最後返回ServiceLoader的實例。tomcat

public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
    //load方中初始化了ServiceLoader對象
    return new ServiceLoader<>(service, loader);
}

//
public final class ServiceLoader<S> implements Iterable<S>
    // ServiceLoader構造器
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        //要加載的接口
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        //(ClassLoader類型,類加載器)
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        //(AccessControlContext類型,訪問控制器)
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        //(LinkedHashMap<String,S>類型,用於緩存加載成功的類)
        providers.clear();//先清空
        //實例化內部類(實現迭代器功能)
        LazyIterator lookupIterator = new LazyIterator(service, loader);
    }
}
//查找實現類和建立實現類的過程,都在LazyIterator完成。
private class LazyIterator implements Iterator<S>{
    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null; 
    private boolean hasNextService() {
        //第二次調用的時候,已經解析完成了,直接返回
        if (nextName != null) {
            return true;
        }
        if (configs == null) {
             try {
                 //META-INF/services/ 加上接口的全限定類名,就是文件服務類的文件
                //META-INF/services/com.viewscenes.netsupervisor.spi.SPIService
                String fullName = PREFIX + service.getName();
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else
                    configs = loader.getResources(fullName);//將文件路徑轉成URL對象
            } catch (IOException x) {
                fail(service, "Error locating configuration files", x);
            }   
        }
        while ((pending == null) || !pending.hasNext()) {
             if (!configs.hasMoreElements()) {
                return false;
            }
            //解析URL文件對象,讀取內容,最後返回
            pending = parse(service, configs.nextElement());
        }
        //拿到第一個實現類的類名
        nextName = pending.next();
        return true;
    }
}
  1. 應用程序經過迭代器接口獲取對象實例

ServiceLoader先判斷成員變量providers對象中(LinkedHashMap<String,S>類型)是否有緩存實例對象,若是有緩存,直接返回。
若是沒有緩存,執行類的裝載,實現以下:

//當咱們調用iterator.hasNext和iterator.next方法的時候,實際上調用的都是LazyIterator的相應方法。
public Iterator<S> iterator() {
        return new Iterator<S>() {

            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();

            public boolean hasNext() {
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }

            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }

        };
    }


//因此,咱們重點關注lookupIterator.hasNext()方法,它最終會調用到hasNextService。
private class LazyIterator implements Iterator<S>{
    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null; 
    private boolean hasNextService() {
        //第二次調用的時候,已經解析完成了,直接返回
        if (nextName != null) {
            return true;
        }
        if (configs == null) {
             try {
                 //META-INF/services/ 加上接口的全限定類名,就是文件服務類的文件
                //META-INF/services/com.viewscenes.netsupervisor.spi.SPIService
                String fullName = PREFIX + service.getName();
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else
                    configs = loader.getResources(fullName);//將文件路徑轉成URL對象
            } catch (IOException x) {
                fail(service, "Error locating configuration files", x);
            }   
        }
        while ((pending == null) || !pending.hasNext()) {
             if (!configs.hasMoreElements()) {
                return false;
            }
            //解析URL文件對象,讀取內容,最後返回
            pending = parse(service, configs.nextElement());
        }
        //拿到第一個實現類的類名
        nextName = pending.next();
        return true;
    }
}

(1) 讀取META-INF/services下的配置文件,得到全部能被實例化的類的名稱,值得注意的是,ServiceLoader能夠跨越jar包獲取META-INF下的配置文件,具體加載配置的實現代碼以下:

try {
    String fullName = PREFIX + service.getName();
    if (loader == null)
        configs = ClassLoader.getSystemResources(fullName);
    else
        configs = loader.getResources(fullName);
} catch (IOException x) {
    fail(service, "Error locating configuration files", x);
}

(2) 經過反射方法Class.forName()加載類對象,並用instance()方法將類實例化。
固然,調用next方法的時候,實際調用到的是,lookupIterator.nextService。它經過反射的方式,建立實現類的實例並返回。

private class LazyIterator implements Iterator<S>{
    private S nextService() {
        //全限定類名
        String cn = nextName;
        nextName = null;
        //建立類的Class對象
        Class<?> c = Class.forName(cn, false, loader);
        //經過newInstance實例化
        S p = service.cast(c.newInstance());
        //放入集合,返回實例
        providers.put(cn, p);
        return p; 
    }
}

(3) 把實例化後的類緩存到providers對象中,(LinkedHashMap<String,S>類型)
而後返回實例對象。

4 常見場景分析

適用於:調用者根據實際使用須要,啓用、擴展、或者替換框架的實現策略。
下面分別針對JDBC、Spring、Dubbo、tomcat、日誌作簡要分析

4.1 JDBC中的應用

SPI機制爲不少框架的擴展提供了可能,JDBC就應用到了這一機制。

// 加載Class到AppClassLoader(系統類加載器),而後註冊驅動類
// Class.forName("com.mysql.jdbc.Driver").newInstance(); 
String url = "jdbc:mysql://localhost:3306/testdb";    
// 經過java庫獲取數據庫鏈接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");

以上就是mysql註冊驅動及獲取connection的過程,各位能夠發現常常寫的Class.forName被註釋掉了,但依然能夠正常運行,這是爲何呢?這是由於從Java1.6開始自帶的jdbc4.0版本已支持SPI服務加載機制,只要mysql的jar包在類路徑中,就能夠註冊mysql驅動。

那究竟是在哪一步自動註冊了mysql driver的呢?重點就在DriverManager.getConnection()中。咱們都是知道調用類的靜態方法會初始化該類,進而執行其靜態代碼塊,DriverManager的靜態代碼塊就是:

4.1.1 加載

它在靜態代碼塊裏面作了一件比較重要的事。很明顯,它已經經過SPI機制,把數據庫驅動鏈接初始化了。

public class DriverManager {
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
}

先看下MySQL的jar包,文件內容爲:com.mysql.cj.jdbc.Driver。
clipboard.png
具體過程還得看loadInitialDrivers,它在裏面查找的是Driver接口的服務類,因此它的文件路徑就是:META-INF/services/java.sql.Driver。

public class DriverManager {
    
    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;
        }
    
        // 經過SPI加載驅動類
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                //很明顯,它要加載Driver接口的服務類,Driver接口的包爲:java.sql.Driver
                //因此它要找的就是META-INF/services/java.sql.Driver文件
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    //查到以後建立對象
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                    // Do nothing
                }
                return null;
            }
        });
         // 繼續加載系統屬性中的驅動類
        if (drivers == null || drivers.equals("")) {
            return;
        }
        
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                // 使用AppClassloader加載
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }
}

從上面能夠看出JDBC中的DriverManager的加載Driver的步驟順序依次是:

  1. 經過SPI方式,讀取 META-INF/services 下文件中的類名,使用線程上下文類加載器加載;
  2. 經過System.getProperty("jdbc.drivers")獲取設置,而後經過系統類加載器加載。

下面詳細分析SPI加載的那段代碼。

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
    //查到以後建立對象
    while(driversIterator.hasNext()) {
        driversIterator.next();
    }
} catch(Throwable t) {
    // Do nothing
}

注意driversIterator.next()最終就是調用Class.forName(DriverName, false, loader)方法,也就是最開始咱們註釋掉的那一句代碼。講到這裏,那句因SPI而省略的代碼如今解釋清楚了,那咱們繼續看給這個方法傳的loader是怎麼來的。

由於這句Class.forName(DriverName, false, loader)代碼所在的類在java.util.ServiceLoader類中,而ServiceLoader.class又加載在BootrapLoader中,所以傳給 forName 的 loader 必然不能是BootrapLoader。這時候只能使用線程上下文類加載器了,也就是說把本身加載不了的類加載到線程上下文類加載器中(經過Thread.currentThread()獲取)。上面那篇文章末尾也講到了線程上下文類加載器默認使用當前執行的代碼所在應用的系統類加載器AppClassLoader。

再看下看ServiceLoader.load(Class)的代碼,的確如此:

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
//看load方法,調用了帶有service, loader參數的構造器
public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
{
    return new ServiceLoader<>(service, loader);
}
//若是傳過來的類加載器c1=null則賦值爲系統類加載器
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;
        。。。
    }

ContextClassLoader默認存放了AppClassLoader的引用,因爲它是在運行時被放在了線程中,因此無論當前程序處於何處(BootstrapClassLoader或是ExtClassLoader等),在任何須要的時候均可以用Thread.currentThread().getContextClassLoader()取出應用程序類加載器來完成須要的操做。

4.1.2 建立實例

上一步已經找到了MySQL中的com.mysql.cj.jdbc.Driver全限定類名,當調用next方法時,就會建立這個類的實例。它就完成了一件事,向DriverManager註冊自身的實例。

到這裏數據源驅動類已經加載到了線程上下文類環境中,下面將driver實例註冊到系統的java.sql.DriverManager類中。
com.mysql.jdbc.Driver加載後運行的靜態代碼塊:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        try {
            // Driver已經加載到線程上下文中了,此時調用DriverManager類的註冊方法往registeredDrivers集合中加入實例
            java.sql.DriverManager.registerDriver(new com.mysql.jdbc.Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
 public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

其實就是調用java.sql.DriverManager.registerDriver()方法將driver實例add到它的一個名爲registeredDrivers的靜態成員CopyOnWriteArrayList中 。

到此驅動註冊基本完成,接下來咱們回到最開始的那段樣例代碼:java.sql.DriverManager.getConnection()。它最終調用瞭如下方法:

在DriverManager.getConnection()方法就是建立鏈接的地方,它經過循環已註冊的數據庫驅動程序,調用其connect方法,獲取鏈接並返回。
private static Connection getConnection(
     String url, java.util.Properties info, Class<?> caller) throws SQLException {
     /* 傳入的caller由Reflection.getCallerClass()獲得,該方法
      * 可獲取到調用本方法的Class類,這兒獲取到的是當前應用的類加載器
      */
     ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
     synchronized(DriverManager.class) {
         if (callerCL == null) {
             callerCL = Thread.currentThread().getContextClassLoader();
         }
     }

     if(url == null) {
         throw new SQLException("The url cannot be null", "08001");
     }

     SQLException reason = null;
     // 遍歷註冊到registeredDrivers裏的Driver類
     for(DriverInfo aDriver : registeredDrivers) {
         // 檢查Driver類有效性
         if(isDriverAllowed(aDriver.driver, callerCL)) {
             try {
                 println("    trying " + aDriver.driver.getClass().getName());
                 // 調用com.mysql.jdbc.Driver.connect方法獲取鏈接
                 Connection con = aDriver.driver.connect(url, info);
                 if (con != null) {
                     // Success!
                     return (con);
                 }
             } catch (SQLException ex) {
                 if (reason == null) {
                     reason = ex;
                 }
             }

         } else {
             println("    skipping: " + aDriver.getClass().getName());
         }

     }
     throw new SQLException("No suitable driver found for "+ url, "08001");
 }
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
    boolean result = false;
    if(driver != null) {
        Class<?> aClass = null;
        try {
        // 傳入的classLoader爲調用getConnetction的當前類加載器,從中尋找driver的class對象
            aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
        } catch (Exception ex) {
            result = false;
        }
    // 注意,只有同一個類加載器中的Class使用==比較時纔會相等,此處就是校驗用戶註冊Driver時該Driver所屬的類加載器與調用時的是否同一個
    // driver.getClass()拿到就是當初執行Class.forName("com.mysql.jdbc.Driver")時的應用AppClassLoader
        result = ( aClass == driver.getClass() ) ? true : false;
    }
    return result;
}

因爲線程上下文類加載器本質就是當前應用類加載器,因此以前的初始化就是加載在當前的類加載器中,這一步就是校驗存放的driver是否屬於調用者的Classloader防止由於不一樣的類加載器致使類型轉換異常(ClassCastException)。

例如在下文中的tomcat裏,多個webapp都有本身的Classloader,若是它們都自帶 mysql-connect.jar包,那底層Classloader的DriverManager裏將註冊多個不一樣類加載器的Driver實例,想要區分只能靠線程上下文類加載器了。

4.2 common-logging

apache最先提供的日誌的門面接口。只有接口,沒有實現。具體方案由各提供商實現,發現日誌提供商是經過掃描 META-INF/services/org.apache.commons.logging.LogFactory配置文件,經過讀取該文件的內容找到日誌提供商實現類。只要咱們的日誌實現裏包含了這個文件,並在文件裏制定LogFactory工廠接口的實現類便可。

4.3 Tomcat中的類加載器

在Tomcat目錄結構中,有三組目錄(「/common/」,「/server/」和「shared/」)能夠存放公用Java類庫,此外還有第四組Web應用程序自身的目錄「/WEB-INF/」,把java類庫放置在這些目錄中的含義分別是:

  • 放置在common目錄中:類庫可被Tomcat和全部的Web應用程序共同使用。
  • 放置在server目錄中:類庫可被Tomcat使用,但對全部的Web應用程序都不可見。
  • 放置在shared目錄中:類庫可被全部的Web應用程序共同使用,但對Tomcat本身不可見。
  • 放置在/WebApp/WEB-INF目錄中:類庫僅僅能夠被此Web應用程序使用,對Tomcat和其餘Web應用程序都不可見。

爲了支持這套目錄結構,並對目錄裏面的類庫進行加載和隔離,Tomcat自定義了多個類加載器,這些類加載器按照經典的雙親委派模型來實現,以下圖所示
clipboard.png
灰色背景的3個類加載器是JDK默認提供的類加載器,這3個加載器的做用前面已經介紹過了。而 CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebAppClassLoader 則是 Tomcat 本身定義的類加載器,它們分別加載 /common/、/server/、/shared/ 和 /WebApp/WEB-INF/ 中的 Java 類庫。其中 WebApp 類加載器和 Jsp 類加載器一般會存在多個實例,每個 Web 應用程序對應一個 WebApp 類加載器,每個 JSP 文件對應一個 Jsp 類加載器。

從圖中的委派關係中能夠看出,CommonClassLoader 能加載的類均可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 本身能加載的類則與對方相互隔離。WebAppClassLoader 可使用 SharedClassLoader 加載到的類,但各個 WebAppClassLoader 實例之間相互隔離。而 JasperLoader 的加載範圍僅僅是這個 JSP 文件所編譯出來的那一個 Class,它出現的目的就是爲了被丟棄:當服務器檢測到 JSP 文件被修改時,會替換掉目前的 JasperLoader 的實例,並經過再創建一個新的 Jsp 類加載器來實現 JSP 文件的 HotSwap 功能。

4.4 Spring加載問題

Tomcat 加載器的實現清晰易懂,而且採用了官方推薦的「正統」的使用類加載器的方式。這時做者提一個問題:若是有 10 個 Web 應用程序都用到了spring的話,能夠把Spring的jar包放到 common 或 shared 目錄下讓這些程序共享。Spring 的做用是管理每一個web應用程序的bean,getBean時天然要能訪問到應用程序的類,而用戶的程序顯然是放在 /WebApp/WEB-INF 目錄中的(由 WebAppClassLoader 加載),那麼在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去加載並不在其加載範圍的用戶程序(/WebApp/WEB-INF/)中的Class呢?其實spring根本不會去管本身被放在哪裏,它通通使用線程上下文類加載器來加載類,而線程上下文類加載器默認設置爲了WebAppClassLoader,也就是說哪一個WebApp應用調用了spring,spring就去取該應用本身的WebAppClassLoader來加載bean,簡直完美。

有興趣的能夠接着看看源碼分析的具體實現。在web.xml中定義的listener爲org.springframework.web.context.ContextLoaderListener,它最終調用了org.springframework.web.context.ContextLoader類來裝載bean,具體方法以下(刪去了部分不相關內容):

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    try {
        // 建立WebApplicationContext
        if (this.context == null) {
            this.context = createWebApplicationContext(servletContext);
        }
        // 將其保存到該webapp的servletContext中        
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
        // 獲取線程上下文類加載器,默認爲WebAppClassLoader
        ClassLoader ccl = Thread.currentThread().getContextClassLoader();
        // 若是spring的jar包放在每一個webapp本身的目錄中
        // 此時線程上下文類加載器會與本類的類加載器(加載spring的)相同,都是WebAppClassLoader
        if (ccl == ContextLoader.class.getClassLoader()) {
            currentContext = this.context;
        }
        else if (ccl != null) {
            // 若是不一樣,也就是上面說的那個問題的狀況,那麼用一個map把剛纔建立的WebApplicationContext及對應的WebAppClassLoader存下來
            // 一個webapp對應一個記錄,後續調用時直接根據WebAppClassLoader來取出
            currentContextPerThread.put(ccl, this.context);
        }
        
        return this.context;
    }
    catch (RuntimeException ex) {
        logger.error("Context initialization failed", ex);
        throw ex;
    }
    catch (Error err) {
        logger.error("Context initialization failed", err);
        throw err;
    }
}

具體說明都在註釋中,spring考慮到了本身可能被放到其餘位置,因此直接用線程上下文類加載器來解決全部可能面臨的狀況。

4.5 Dubbo框架中SPI機制分析

須要說明的是雖然Java 提供了對SPI機制的默認實現支持,可是並不表示全部的框架都會默認使用這種Java自帶的邏輯,SPI機制更多的是一種實現思想,而具體的實現邏輯,則是能夠本身定義的。例如咱們說Dubbo框架中大量使用了SPI技術,可是Dubbo並無使用JDK原生的ServiceLoader,而是本身實現了ExtensionLoader來加載擴展點,因此咱們看Dubbo框架源碼的時候,千萬不要被配置目錄是/META-INF/dubbo/internal,而不是META-INF/services/所迷惑了。
clipboard.png
相應地若是其餘框架中也使用了自定義的SPI機制實現,也不要疑惑,它們也只是從新定義了相似於ServiceLoader類的加載邏輯而已,其背後的設計思想和原理則都是同樣的!例如,以Dubbo中卸載協議的代碼舉例:

private void destroyProtocols() {
        ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
        for (String protocolName : loader.getLoadedExtensions()) {
            try {
                Protocol protocol = loader.getLoadedExtension(protocolName);
                if (protocol != null) {
                    protocol.destroy();
                }
            } catch (Throwable t) {
                logger.warn(t.getMessage(), t);
            }
        }
    }

而ExtensionLoader中掃描的配置路徑以下:

public class ExtensionLoader<T> {

    private static final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class);

    private static final String SERVICES_DIRECTORY = "META-INF/services/";

    private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";

    private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";

    private static final Pattern NAME_SEPARATOR = Pattern.compile("\\s*[,]+\\s*");

    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();

    private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();
}

以上經過對Java自帶SPI機制的示例以及對Dubbo和JDBC驅動框架中對SPI機制應用的分析,相信你們應該是有了一個整體的原理性的認識了。若是須要更加深刻的瞭解一些細節的實現邏輯就須要你們好好去看看ServiceLoader的源碼了,若是其餘框架單獨實現了SPI機制,其相應的實現加載工具類也須要具體看看它們的源碼是怎麼實現的了!

4.6 小結

經過上面的幾個案例分析,咱們能夠總結出線程上下文類加載器的適用場景:
當高層提供了統一接口讓低層去實現,同時又要是在高層加載(或實例化)低層的類時,必須經過線程上下文類加載器來幫助高層的ClassLoader找到並加載該類。
當使用本類託管類加載,然而加載本類的ClassLoader未知時,爲了隔離不一樣的調用者,能夠取調用者各自的線程上下文類加載器代爲託管。

5 擴展

既然咱們知道JDBC是這樣建立數據庫鏈接的,咱們能不能再擴展一下呢?若是咱們本身也建立一個java.sql.Driver文件,自定義實現類MyDriver,那麼,在獲取鏈接的先後就能夠動態修改一些信息。

仍是先在項目ClassPath下建立文件,文件內容爲自定義驅動類
clipboard.png
咱們的MyDriver實現類,繼承自MySQL中的NonRegisteringDriver,還要實現java.sql.Driver接口。這樣,在調用connect方法的時候,就會調用到此類,但實際建立的過程還靠MySQL完成。

package com.viewscenes.netsupervisor.spi

public class MyDriver extends NonRegisteringDriver implements Driver{
    static {
        try {
            java.sql.DriverManager.registerDriver(new MyDriver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
    public MyDriver()throws SQLException {}
    
    public Connection connect(String url, Properties info) throws SQLException {
        System.out.println("準備建立數據庫鏈接.url:"+url);
        System.out.println("JDBC配置信息:"+info);
        info.setProperty("user", "root");
        Connection connection =  super.connect(url, info);
        System.out.println("數據庫鏈接建立完成!"+connection.toString());
        return connection;
    }
}
--------------------輸出結果---------------------
準備建立數據庫鏈接.url:jdbc:mysql:///consult?serverTimezone=UTC
JDBC配置信息:{user=root, password=root}
數據庫鏈接建立完成!com.mysql.cj.jdbc.ConnectionImpl@7cf10a6f

6 優點&劣勢

1. 優點

  • 使用Java SPI機制的優點是實現解耦,使得第三方服務模塊的裝配控制的邏輯與調用者的業務代碼分離,而不是耦合在一塊兒。應用程序能夠根據實際業務狀況啓用框架擴展或替換框架組件。

2. 劣勢

  • 雖然ServiceLoader也算是使用的延遲加載,可是基本只能經過遍歷所有獲取,也就是接口的實現類所有加載並實例化一遍。若是你並不想用某些實現類,它也被加載並實例化了,這就形成了浪費。
  • 獲取某個實現類的方式不夠靈活,只能經過Iterator形式獲取,不能根據某個參數來獲取對應的實現類。
  • 多個併發多線程使用ServiceLoader類的實例是不安全的。
  • 使用線程上下文加載類,也要注意保證多個須要通訊的線程間的類加載器應該是同一個,防止由於不一樣的類加載器致使類型轉換異常(ClassCastException)。

聲明:爲避免重複造輪子,文章部份內容摘自網絡。

相關文章
相關標籤/搜索