Java是如何實現自己的SPI機制的? JDK源碼(一)

  • 2020 年 3 月 29 日
  • 筆記

註:該源碼分析對應JDK版本為1.8

1 引言

這是【源碼筆記】的JDK源碼解讀的第一篇文章,本篇我們來探究Java的SPI機制的相關源碼。

2 什麼是SPI機制

那麼,什麼是SPI機制呢?

SPI是Service Provider Interface 的簡稱,即服務提供者介面的意思。根據字面意思我們可能還有點困惑,SPI說白了就是一種擴展機制,我們在相應配置文件中定義好某個介面的實現類,然後再根據這個介面去這個配置文件中載入這個實例類並實例化,其實SPI就是這麼一個東西。說到SPI機制,我們最常見的就是Java的SPI機制,此外,還有Dubbo和SpringBoot自定義的SPI機制。

有了SPI機制,那麼就為一些框架的靈活擴展提供了可能,而不必將框架的一些實現類寫死在程式碼裡面。

那麼,某些框架是如何利用SPI機制來做到靈活擴展的呢?下面舉幾個栗子來闡述下:

  1. JDBC驅動載入案例:利用Java的SPI機制,我們可以根據不同的資料庫廠商來引入不同的JDBC驅動包;
  2. SpringBoot的SPI機制:我們可以在spring.factories中加上我們自定義的自動配置類,事件監聽器或初始化器等;
  3. Dubbo的SPI機制:Dubbo更是把SPI機制應用的淋漓盡致,Dubbo基本上自身的每個功能點都提供了擴展點,比如提供了集群擴展,路由擴展和負載均衡擴展等差不多接近30個擴展點。如果Dubbo的某個內置實現不符合我們的需求,那麼我們只要利用其SPI機制將我們的實現替換掉Dubbo的實現即可。

上面的三個栗子先讓我們直觀感受下某些框架利用SPI機制是如何做到靈活擴展的。

3 如何使用Java的SPI?

我們先來看看如何使用Java自帶的SPI。
先定義一個Developer介面

// Developer.java    package com.ymbj.spi;    public interface Developer {      void sayHi();  }  

再定義兩個Developer介面的兩個實現類:

// JavaDeveloper.java    package com.ymbj.spi;    public class JavaDeveloper implements Developer {        @Override      public void sayHi() {          System.out.println("Hi, I am a Java Developer.");      }  }  
// PythonDeveloper.java    package com.ymbj.spi;    public class PythonDeveloper implements Developer {        @Override      public void sayHi() {          System.out.println("Hi, I am a Python Developer.");      }  }  

然後再在項目resources目錄下新建一個META-INF/services文件夾,然後再新建一個以Developer介面的全限定名命名的文件,文件內容為:

// com.ymbj.spi.Developer文件    com.ymbj.spi.JavaDeveloper  com.ymbj.spi.PythonDeveloper  

最後我們再新建一個測試類JdkSPITest

// JdkSPITest.java    public class JdkSPITest {        @Test      public void testSayHi() throws Exception {          ServiceLoader<Developer> serviceLoader = ServiceLoader.load(Developer.class);          serviceLoader.forEach(Developer::sayHi);      }  }  

運行上面那個測試類,運行成功結果如下截圖所示:

由上面簡單的Demo我們知道了如何使用Java的SPI機制來實現擴展點載入,下面推薦一篇文章JAVA拾遺–關於SPI機制,通過這篇文章,相信大家對Java的SPI會有一個比較深刻的理解,特別是JDBC載入驅動這方面。

4 Java的SPI機制的源碼解讀

通過前面擴展Developer介面的簡單Demo,我們看到Java的SPI機制實現跟ServiceLoader這個類有關,那麼我們先來看下ServiceLoader的類結構程式碼:

// ServiceLoader實現了【Iterable】介面  public final class ServiceLoader<S>      implements Iterable<S>{      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;      // Cached providers, in instantiation order      private LinkedHashMap<String,S> providers = new LinkedHashMap<>();      // The current lazy-lookup iterator      private LazyIterator lookupIterator;      // 構造方法      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();      }        // ...暫時省略相關程式碼        // ServiceLoader的內部類LazyIterator,實現了【Iterator】介面      // Private inner class implementing fully-lazy provider lookup      private class LazyIterator          implements Iterator<S>{          Class<S> service;          ClassLoader loader;          Enumeration<URL> configs = null;          Iterator<String> pending = null;          String nextName = null;            private LazyIterator(Class<S> service, ClassLoader loader) {              this.service = service;              this.loader = loader;          }          // 覆寫Iterator介面的hasNext方法          public boolean hasNext() {              // ...暫時省略相關程式碼          }          // 覆寫Iterator介面的next方法          public S next() {              // ...暫時省略相關程式碼          }          // 覆寫Iterator介面的remove方法          public void remove() {              // ...暫時省略相關程式碼          }        }        // 覆寫Iterable介面的iterator方法,返回一個迭代器      public Iterator<S> iterator() {          // ...暫時省略相關程式碼      }        // ...暫時省略相關程式碼    }  

可以看到,ServiceLoader實現了Iterable介面,覆寫其iterator方法能產生一個迭代器;同時ServiceLoader有一個內部類LazyIterator,而LazyIterator又實現了Iterator介面,說明LazyIterator是一個迭代器。

4.1 ServiceLoader.load方法,為載入服務提供者實現類做前期準備

那麼我們現在開始探究Java的SPI機制的源碼,
先來看JdkSPITest的第一句程式碼ServiceLoader<Developer> serviceLoader = ServiceLoader.load(Developer.class);中的ServiceLoader.load(Developer.class);的源碼:

// ServiceLoader.java    public static <S> ServiceLoader<S> load(Class<S> service) {      //獲取當前執行緒上下文類載入器      ClassLoader cl = Thread.currentThread().getContextClassLoader();      // 將service介面類和執行緒上下文類載入器作為參數傳入,繼續調用load方法      return ServiceLoader.load(service, cl);  }  

我們再來看下ServiceLoader.load(service, cl);方法:

// ServiceLoader.java    public static <S> ServiceLoader<S> load(Class<S> service,                                          ClassLoader loader)  {      // 將service介面類和執行緒上下文類載入器作為構造參數,新建了一個ServiceLoader對象      return new ServiceLoader<>(service, loader);  }  

繼續看new ServiceLoader<>(service, loader);是如何構建的?

// ServiceLoader.java    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();  }  

可以看到在構建ServiceLoader對象時除了給其成員屬性賦值外,還調用了reload方法:

// ServiceLoader.java    public void reload() {      providers.clear();      lookupIterator = new LazyIterator(service, loader);  }  

可以看到在reload方法中又新建了一個LazyIterator對象,然後賦值給lookupIterator

// ServiceLoader$LazyIterator.java    private LazyIterator(Class<S> service, ClassLoader loader) {      this.service = service;      this.loader = loader;  }  

可以看到在構建LazyIterator對象時,也只是給其成員變數serviceloader屬性賦值呀,我們一路源碼跟下來,也沒有看到去META-INF/services文件夾載入Developer介面的實現類!這就奇怪了,我們都被ServiceLoaderload方法名騙了。

還記得分析前面的程式碼時新建了一個LazyIterator對象嗎?Lazy顧名思義是的意思,Iterator就是迭代的意思。我們此時猜測那麼LazyIterator對象的作用應該就是在迭代的時候再去載入Developer介面的實現類了。

4.2 ServiceLoader.iterator方法,實現服務提供者實現類的懶載入

我們現在再來看JdkSPITest的第二句程式碼serviceLoader.forEach(Developer::sayHi);,執行這句程式碼後最終會調用serviceLoaderiterator方法:

// serviceLoader.java    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;              // 調用lookupIterator即LazyIterator的hasNext方法              // 可以看到是委託給LazyIterator的hasNext方法來實現              return lookupIterator.hasNext();          }            public S next() {              if (knownProviders.hasNext())                  return knownProviders.next().getValue();              // 調用lookupIterator即LazyIterator的next方法              // 可以看到是委託給LazyIterator的next方法來實現              return lookupIterator.next();          }            public void remove() {              throw new UnsupportedOperationException();          }        };  }  

可以看到調用serviceLoaderiterator方法會返回一個匿名的迭代器對象,而這個匿名迭代器對象其實相當於一個門面類,其覆寫的hasNextnext方法又分別委託LazyIteratorhasNextnext方法來實現了。

我們繼續調試,發現接下來會進入LazyIteratorhasNext方法:

// serviceLoader$LazyIterator.java    public boolean hasNext() {      if (acc == null) {          // 調用hasNextService方法          return hasNextService();      } else {          PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {              public Boolean run() { return hasNextService(); }          };          return AccessController.doPrivileged(action, acc);      }  }  

繼續跟進hasNextService方法:

// serviceLoader$LazyIterator.java    private boolean hasNextService() {      if (nextName != null) {          return true;      }      if (configs == null) {          try {              // PREFIX = "META-INF/services/"              // service.getName()即介面的全限定名              // 還記得前面的程式碼構建LazyIterator對象時已經給其成員屬性service賦值嗎              String fullName = PREFIX + service.getName();              // 載入META-INF/services/目錄下的介面文件中的服務提供者類              if (loader == null)                  configs = ClassLoader.getSystemResources(fullName);              else                  // 還記得前面的程式碼構建LazyIterator對象時已經給其成員屬性loader賦值嗎                  configs = loader.getResources(fullName);          } catch (IOException x) {              fail(service, "Error locating configuration files", x);          }      }      while ((pending == null) || !pending.hasNext()) {          if (!configs.hasMoreElements()) {              return false;          }          // 返回META-INF/services/目錄下的介面文件中的服務提供者類並賦值給pending屬性          pending = parse(service, configs.nextElement());      }      // 然後取出一個全限定名賦值給LazyIterator的成員變數nextName      nextName = pending.next();      return true;  }  

可以看到在執行LazyIteratorhasNextService方法時最終將去META-INF/services/目錄下載入介面文件的內容即載入服務提供者實現類的全限定名,然後取出一個服務提供者實現類的全限定名賦值給LazyIterator的成員變數nextName。到了這裡,我們就明白了LazyIterator的作用真的是懶載入,在用到的時候才會去載入。

思考:為何這裡要用懶載入呢?懶載入的思想是怎樣的呢?懶載入有啥好處呢?你還能舉出其他懶載入的案例嗎?

同樣,執行完LazyIteratorhasNext方法後,會繼續執行LazyIteratornext方法:

// serviceLoader$LazyIterator.java    public S next() {      if (acc == null) {          // 調用nextService方法          return nextService();      } else {          PrivilegedAction<S> action = new PrivilegedAction<S>() {              public S run() { return nextService(); }          };          return AccessController.doPrivileged(action, acc);      }  }  

我們繼續跟進nextService方法:

// serviceLoader$LazyIterator.java    private S nextService() {      if (!hasNextService())          throw new NoSuchElementException();      // 還記得在hasNextService方法中為nextName賦值過服務提供者實現類的全限定名嗎      String cn = nextName;      nextName = null;      Class<?> c = null;      try {          // 【1】去classpath中根據傳入的類載入器和服務提供者實現類的全限定名去載入服務提供者實現類          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 {          // 【2】實例化剛才載入的服務提供者實現類,並進行轉換          S p = service.cast(c.newInstance());          // 【3】最終將實例化後的服務提供者實現類放進providers集合          providers.put(cn, p);          return p;      } catch (Throwable x) {          fail(service,               "Provider " + cn + " could not be instantiated",               x);      }      throw new Error();          // This cannot happen  }  

可以看到LazyIteratornextService方法最終將實例化之前載入的服務提供者實現類,並放進providers集合中,隨後再調用服務提供者實現類的方法(比如這裡指JavaDevelopersayHi方法)。注意,這裡是載入一個服務提供者實現類後,若main函數中有調用該服務提供者實現類的方法的話,緊接著會調用其方法;然後繼續實例化下一個服務提供者類。

設計模式:可以看到,Java的SPI機制實現程式碼中應用了迭代器模式,迭代器模式屏蔽了各種存儲對象的內部結構差異,提供一個統一的視圖來遍歷各個存儲對象(存儲對象可以為集合,數組等)。java.util.Iterator也是迭代器模式的實現:同時Java的各個集合類一般實現了Iterable介面,實現了其iterator方法從而獲得Iterator介面的實現類對象(一般為集合內部類),然後再利用Iterator對象的實現類的hasNextnext方法來遍歷集合元素。

5 JDBC驅動載入源碼解讀

前面分析了Java的SPI機制的源碼實現,現在我們再來看下Java的SPI機制的實際案例的應用。

我們都知道,JDBC驅動載入是Java的SPI機制的典型應用案例。JDBC主要提供了一套介面規範,而這套規範的api在java的核心庫(rt.jar)中實現,而不同的資料庫廠商只要編寫符合這套JDBC介面規範的驅動程式碼,那麼就可以用Java語言來連接資料庫了。

java的核心庫(rt.jar)中跟JDBC驅動載入的最核心的介面和類分別是java.sql.Driver介面和java.sql.DriverManager類,其中java.sql.Driver是各個資料庫廠商的驅動類要實現的介面,而DriverManager是用來管理資料庫的驅動類的,值得注意的是DriverManager這個類有一個registeredDrivers集合屬性,用來存儲資料庫的驅動類。

// DriverManager.java    // List of registered JDBC drivers  private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();  

這裡以載入Mysql驅動為例來分析JDBC驅動載入的源碼。

我們的項目引入mysql-connector-java依賴(這裡的版本是5.1.47)後,那麼Mysql的驅動實現類文件如下圖所示:


可以看到Mysql的驅動包中有兩個Driver驅動類,分別是com.mysql.jdbc.Drivercom.mysql.fabric.jdbc.FabricMySQLDriver,默認情況下一般我們只用到前者。

5.1 利用Java的SPI載入Mysql的驅動類

那麼接下來我們就來探究下JDBC驅動載入的程式碼是如何實現的。

先來看一下一個簡單的JDBC的測試程式碼:

// JdbcTest.java    public class JdbcTest {      public static void main(String[] args) {          Connection connection = null;          Statement statement = null;          ResultSet rs = null;            try {              // 注意:在JDBC 4.0規範中,這裡可以不用再像以前那樣編寫顯式載入資料庫的程式碼了              // Class.forName("com.mysql.jdbc.Driver");              // 獲取資料庫連接,注意【這裡將會載入mysql的驅動包】              /***************【主線,切入點】****************/              connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc", "root", "123456");              // 創建Statement語句              statement = connection.createStatement();              // 執行查詢語句              rs = statement.executeQuery("select * from user");              // 遍歷查詢結果集              while(rs.next()){                  String name = rs.getString("name");                  System.out.println(name);              }          } catch(Exception e) {              e.printStackTrace();          } finally {              // ...省略釋放資源的程式碼          }      }  }  

JdbcTestmain函數調用DriverManagergetConnection方法時,此時必然會先執行DriverManager類的靜態程式碼塊的程式碼,然後再執行getConnection方法,那麼先來看下DriverManager的靜態程式碼塊:

// DriverManager.java    static {      // 載入驅動實現類      loadInitialDrivers();      println("JDBC DriverManager initialized");  }  

繼續跟進loadInitialDrivers的程式碼:

// DriverManager.java    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;      }      AccessController.doPrivileged(new PrivilegedAction<Void>() {          public Void run() {              // 來到這裡,是不是感覺似曾相識,對,沒錯,我們在前面的JdkSPITest程式碼中執行過下面的兩句程式碼              // 這句程式碼前面已經分析過,這裡不會真正載入服務提供者實現類              // 而是實例化一個ServiceLoader對象且實例化一個LazyIterator對象用於懶載入              ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);              // 調用ServiceLoader的iterator方法,在迭代的同時,也會去載入並實例化META-INF/services/java.sql.Driver文件              // 的com.mysql.jdbc.Driver和com.mysql.fabric.jdbc.FabricMySQLDriver兩個驅動類              /****************【主線,重點關注】**********************/              Iterator<Driver> driversIterator = loadedDrivers.iterator();              try{                  while(driversIterator.hasNext()) {                      driversIterator.next();                  }              } catch(Throwable t) {              // Do nothing              }              return null;          }      });        println("DriverManager.initialize: jdbc.drivers = " + drivers);        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);              Class.forName(aDriver, true,                      ClassLoader.getSystemClassLoader());          } catch (Exception ex) {              println("DriverManager.Initialize: load failed: " + ex);          }      }  }  

在上面的程式碼中,我們可以看到Mysql的驅動類載入主要是利用Java的SPI機制實現的,即利用ServiceLoader來實現載入並實例化Mysql的驅動類。

5.2 註冊Mysql的驅動類

那麼,上面的程式碼只是Mysql驅動類的載入和實例化,那麼,驅動類又是如何被註冊進DriverManagerregisteredDrivers集合的呢?

這時,我們注意到com.mysql.jdbc.Driver類裡面也有個靜態程式碼塊,即實例化該類時肯定會觸發該靜態程式碼塊程式碼的執行,那麼我們直接看下這個靜態程式碼塊做了什麼事情:

// com.mysql.jdbc.Driver.java    // Register ourselves with the DriverManager  static {      try {          // 將自己註冊進DriverManager類的registeredDrivers集合          java.sql.DriverManager.registerDriver(new Driver());      } catch (SQLException E) {          throw new RuntimeException("Can't register driver!");      }  }  

可以看到,原來就是Mysql驅動類com.mysql.jdbc.Driver在實例化的時候,利用執行其靜態程式碼塊的時機時將自己註冊進DriverManagerregisteredDrivers集合中。

好,繼續跟進DriverManagerregisterDriver方法:

// DriverManager.java    public static synchronized void registerDriver(java.sql.Driver driver)      throws SQLException {      // 繼續調用registerDriver方法      registerDriver(driver, null);  }    public static synchronized void registerDriver(java.sql.Driver driver,          DriverAction da)      throws SQLException {        /* Register the driver if it has not already been added to our list */      if(driver != null) {          // 將driver驅動類實例註冊進registeredDrivers集合          registeredDrivers.addIfAbsent(new DriverInfo(driver, da));      } else {          // This is for compatibility with the original DriverManager          throw new NullPointerException();      }      println("registerDriver: " + driver);  }  

分析到了這裡,我們就明白了Java的SPI機制是如何載入Mysql的驅動類的並如何將Mysql的驅動類註冊進DriverManagerregisteredDrivers集合中的。

5.3 使用之前註冊的Mysql驅動類連接資料庫

既然Mysql的驅動類已經被註冊進來了,那麼何時會被用到呢?

我們要連接Mysql資料庫,自然需要用到Mysql的驅動類,對吧。此時我們回到JDBC的測試程式碼JdbcTest類的connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc", "root", "123456");這句程式碼中,看一下getConnection的源碼:

// DriverManager.java    @CallerSensitive  public static Connection getConnection(String url,      String user, String password) throws SQLException {      java.util.Properties info = new java.util.Properties();        if (user != null) {          info.put("user", user);      }      if (password != null) {          info.put("password", password);      }      // 繼續調用getConnection方法來連接資料庫      return (getConnection(url, info, Reflection.getCallerClass()));  }  

繼續跟進getConnection方法:

// DriverManager.java    private static Connection getConnection(          String url, java.util.Properties info, Class<?> caller) throws SQLException {            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;          // 遍歷registeredDrivers集合,注意之前載入的Mysql驅動類實例被註冊進這個集合          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());                      // 利用Mysql驅動類來連接資料庫                      /*************【主線,重點關注】*****************/                      Connection con = aDriver.driver.connect(url, info);                      // 只要連接上,那麼載入的其餘驅動類比如FabricMySQLDriver將會忽略,因為下面直接返回了                      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");      }  

可以看到,DriverManagergetConnection方法會從registeredDrivers集合中拿出剛才載入的Mysql驅動類來連接資料庫。

好了,到了這裡,JDBC驅動載入的源碼就基本分析完了。

6 執行緒上下文類載入器

前面基本分析完了JDBC驅動載入的源碼,但是還有一個很重要的知識點還沒講解,那就是破壞類載入機制的雙親委派模型的執行緒上下文類載入器

我們都知道,JDBC規範的相關類(比如前面的java.sql.Driverjava.sql.DriverManager)都是在Jdk的rt.jar包下,意味著這些類將由啟動類載入器(BootstrapClassLoader)載入;而Mysql的驅動類由外部資料庫廠商實現,當驅動類被引進項目時也是位於項目的classpath中,此時啟動類載入器肯定是不可能載入這些驅動類的呀,此時該怎麼辦?

由於類載入機制的雙親委派模型在這方面的缺陷,因此只能打破雙親委派模型了。因為項目classpath中的類是由應用程式類載入器(AppClassLoader)來載入,所以我們可否"逆向"讓啟動類載入器委託應用程式類載入器去載入這些外部資料庫廠商的驅動類呢?如果可以,我們怎樣才能做到讓啟動類載入器委託應用程式類載入器去載入
classpath中的類呢?

答案肯定是可以的,我們可以將應用程式類載入器設置進執行緒裡面,即執行緒裡面新定義一個類載入器的屬性contextClassLoader,然後在某個時機將應用程式類載入器設置進執行緒的contextClassLoader這個屬性裡面,如果沒有設置的話,那麼默認就是應用程式類載入器。然後啟動類載入器去載入java.sql.Driverjava.sql.DriverManager等類時,同時也會從當前執行緒中取出contextClassLoader即應用程式類載入器去classpath中載入外部廠商提供的JDBC驅動類。因此,通過破壞類載入機制的雙親委派模型,利用執行緒上下文類載入器完美的解決了該問題。

此時我們再回過頭來看下在載入Mysql驅動時是什麼時候獲取的執行緒上下文類載入器呢?

答案就是在DriverManagerloadInitialDrivers方法調用了ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);這句程式碼,而取出執行緒上下文類載入器就是在ServiceLoaderload方法中取出:

  public static <S> ServiceLoader<S> load(Class<S> service) {      // 取出執行緒上下文類載入器取出的是contextClassLoader,而contextClassLoader裝的應用程式類載入器      ClassLoader cl = Thread.currentThread().getContextClassLoader();      // 把剛才取出的執行緒上下文類載入器作為參數傳入,用於後去載入classpath中的外部廠商提供的驅動類      return ServiceLoader.load(service, cl);  }  

因此,到了這裡,我們就明白了執行緒上下文類載入器在載入JDBC驅動包中充當的作用了。此外,我們應該知道,Java的絕大部分涉及SPI的載入都是利用執行緒上下文類載入器來完成的,比如JNDI,JCE,JBI等。

擴展:打破類載入機制的雙親委派模型的還有程式碼的熱部署等,另外,Tomcat的類載入機制也值得一讀。

註:若有些小夥伴對類載入機制的雙親委派模型不清楚的話,推薦完全理解雙親委派模型與自定義 ClassLoader這篇文了解下。

7 擴展:Dubbo的SPI機制

前面也講到Dubbo框架身上處處是SPI機制的應用,可以說處處都是擴展點,真的是把SPI機制應用的淋漓盡致。但是Dubbo沒有採用默認的Java的SPI機制,而是自己實現了一套SPI機制。

那麼,Dubbo為什麼沒有採用Java的SPI機制呢?

原因主要有兩個:

  1. Java的SPI機制會一次性實例化擴展點所有實現,如果有擴展實現初始化很耗時,但如果沒用上也載入,會很浪費資源;
  2. Java的SPI機制沒有Ioc和AOP的支援,因此Dubbo用了自己的SPI機制:增加了對擴展點IoC和AOP的支援,一個擴展點可以直接setter注入其它擴展點。

由於以上原因,Dubbo自定義了一套SPI機制,用於載入自己的擴展點。關於Dubbo的SPI機制這裡不再詳述,感興趣的小夥伴們可以去Dubbo官網看看是如何擴展Dubbo的SPI的?還有其官網也有Duboo的SPI的源碼分析文章。

8 小結

好了,Java的SPI機制就解讀到這裡了,先將前面的知識點再總結下:

  1. Java的SPI機制的使用;
  2. Java的SPI機制的原理;
  3. JDBC驅動的載入原理;
  4. 執行緒上下文類載入器在JDBC驅動載入中的作用;
  5. 簡述了Duboo的SPI機制。

原創不易,幫忙點個讚唄!

由於筆者水平有限,若文中有錯誤還請指出,謝謝。

參考

1,http://dubbo.apache.org/zh-cn/docs/source_code_guide/dubbo-spi.html

2,《深入理解Java虛擬機》