1. 程式人生 > 程式設計 >詳解java實踐SPI機制及淺析原始碼

詳解java實踐SPI機制及淺析原始碼

1.概念

正式步入今天的核心內容之前,溪源先給大家介紹一下關於SPI機制的相關概念,最後會提供實踐原始碼。

SPI即Service Provider Interface,屬於JDK內建的一種動態的服務提供發現機制,可以理解為執行時動態載入介面的實現類。更甚至,大家可以將SPI機制與設計模式中的策略模式建立聯絡。

SPI機制:

詳解java實踐SPI機制及淺析原始碼

從上圖中理解SPI機制:標準化介面+策略模式+配置檔案;

SPI機制核心思想:系統設計的各個抽象,往往有很多不同的實現方案,在面向的物件的設計裡,一般推薦模組之間基於介面程式設計,模組之間不對實現類進行硬編碼。一旦程式碼裡涉及具體的實現類,就違反了可拔插的原則,如果需要替換一種實現,就需要修改程式碼。為了實現在模組裝配的時候能不在程式裡動態指明,這就需要一種服務發現機制

使用場景:

  • 1.資料庫驅動載入:面對不同廠商的資料庫,JDBC需要載入不同型別的資料庫驅動;
  • 2.日誌介面實現:SLF4J載入不同日誌實現類;
  • 3.溪源在實際開發中也使用了SPI機制:面對不同儀器平臺的結果檔案上傳需要解析具體的結果,檔案不同,解析邏輯不同,因此採用SPI機制能夠解耦和降低維護成本;

SPI機制使用約定:

從上面的圖中,我們可以清晰的知道SPI的三部分:介面+實現類+配置檔案;因此,專案中若要利用SPI機制,則需要遵循以下約定:

  • 當服務提供者提供了介面的一種具體實現後,在jar包的META-INF/services目錄下建立一個以“介面全限定名”為命名的檔案,內容為實現類的全限定名。
  • 主程式通過java.util.ServiceLoder動態裝載實現模組,它通過掃描META-INF/services目錄下的配置檔案找到實現類的全限定名,把類載入到JVM;

注意:除SPI,我還發布了最新Java架構專案實戰教程+大廠面試題庫, 點選此處免費獲取,小白勿進!

2.實踐

整體包結構如圖:

詳解java實踐SPI機制及淺析原始碼

新建標準化介面:

public interface SayService {
  void say(String word);
}

建立兩個實現類

@Service
public class ASayServiceImpl implements SayService {
  @Override
  public void say(String word) {
    System.out.println(word + " A say: I am a boy");
  }
}


@Service
public class BSayServiceImpl implements SayService {
  @Override
  public void say(String word) {
    System.out.println(word + " B say: I am a girl");
  }
}

新建META-INF/services目錄和配置檔案(以介面全限定名)

配置檔案內容為實現類全限定名

com.qxy.spi.impl.ASayServiceImpl
com.qxy.spi.impl.BSayServiceImpl

單測

@SpringBootTest
@RunWith(SpringRunner.class)
public class SpiTest {

  static ServiceLoader<SayService> services = ServiceLoader.load(SayService.class);

  @Test
  public void test1() {
    for (SayService sayService : services) {
      sayService.say("Hello");
    }
  }

}

結果

Hello A say: I am a boy
Hello B say: I am a girl

3.原始碼

原始碼主要載入流程如下:

應用程式呼叫ServiceLoader.load方法 ServiceLoader.load方法內先建立一個新的ServiceLoader,並例項化該類中的成員變數;

  • loader(ClassLoader型別,類載入器)
  • acc(AccessControlContext型別,訪問控制器)
  • providers(LinkedHashMap<String,S>型別,用於快取載入成功的類)
  • lookupIterator(實現迭代器功能)

應用程式通過迭代器介面獲取物件例項 ServiceLoader先判斷成員變數providers物件中(LinkedHashMap<String,S>型別)是否有快取例項物件,如果有快取,直接返回。如果沒有快取,執行類的裝載。

  • 讀取META-INF/services/下的配置檔案,獲得所有能被例項化的類的名稱,值得注意的是,ServiceLoader可以跨越jar包獲取META-INF下的配置檔案;
  • 通過反射方法Class.forName()載入類物件,並用instance()方法將類例項化。
  • 把例項化後的類快取到providers物件中,(LinkedHashMap<String,S>型別) 然後返回例項物件。
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;

  // 建立ServiceLoader時採用的訪問控制上下文
  // The access control context taken when the ServiceLoader is created
  private final AccessControlContext acc;

  // 用於快取已經載入的介面實現類,其中key為實現類的完整類名
  // Cached providers,in instantiation order
  private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

  // 用於延遲載入介面的實現類
  // The current lazy-lookup iterator
  private LazyIterator lookupIterator;

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

  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();
  }

  private static void fail(Class<?> service,String msg,Throwable cause)
    throws ServiceConfigurationError
  {
    throw new ServiceConfigurationError(service.getName() + ": " + msg,cause);
  }

  private static void fail(Class<?> service,String msg)
    throws ServiceConfigurationError
  {
    throw new ServiceConfigurationError(service.getName() + ": " + msg);
  }

  private static void fail(Class<?> service,URL u,int line,String msg)
    throws ServiceConfigurationError
  {
    fail(service,u + ":" + line + ": " + msg);
  }

  // Parse a single line from the given configuration file,adding the name
  // on the line to the names list.
  //具體解析資原始檔中的每一行內容
  private int parseLine(Class<?> service,BufferedReader r,int lc,List<String> names)
    throws IOException,ServiceConfigurationError
  {
    String ln = r.readLine();
    if (ln == null) {
    	//-1表示解析完成
      return -1;
    }
    // 如果存在'#'字元,擷取第一個'#'字串之前的內容,'#'字元之後的屬於註釋內容
    int ci = ln.indexOf('#');
    if (ci >= 0) ln = ln.substring(0,ci);
    ln = ln.trim();
    int n = ln.length();
    if (n != 0) {
    	//不合法的標識:' '、'\t'
      if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
        fail(service,u,lc,"Illegal configuration-file syntax");
      int cp = ln.codePointAt(0);
      //判斷第一個 char 是否一個合法的 Java 起始識別符號
      if (!Character.isJavaIdentifierStart(cp))
        fail(service,"Illegal provider-class name: " + ln);
      	//判斷所有其他字串是否屬於合法的Java識別符號
      for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
        cp = ln.codePointAt(i);
        if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
          fail(service,"Illegal provider-class name: " + ln);
      }
      //不存在則快取
      if (!providers.containsKey(ln) && !names.contains(ln))
        names.add(ln);
    }
    return lc + 1;
  }

  
  private Iterator<String> parse(Class<?> service,URL u)
    throws ServiceConfigurationError
  {
    InputStream in = null;
    BufferedReader r = null;
    ArrayList<String> names = new ArrayList<>();
    try {
      in = u.openStream();
      r = new BufferedReader(new InputStreamReader(in,"utf-8"));
      int lc = 1;
      while ((lc = parseLine(service,r,names)) >= 0);
    } catch (IOException x) {
      fail(service,"Error reading configuration file",x);
    } finally {
      try {
        if (r != null) r.close();
        if (in != null) in.close();
      } catch (IOException y) {
        fail(service,"Error closing configuration file",y);
      }
    }
    return names.iterator();
  }

  // Private inner class implementing fully-lazy provider lookup
  //
  private class LazyIterator
    implements Iterator<S>
  {

    Class<S> service;
    ClassLoader loader;
    // 載入資源的URL集合
	  Enumeration<URL> configs = null; 
	  // 需載入的實現類的全限定類名的集合
	  Iterator<String> pending = null;
	  // 下一個需要載入的實現類的全限定類名
	  String nextName = null;

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

    private boolean hasNextService() {
      if (nextName != null) {
        return true;
      }
      if (configs == null) {
        try {
        // 資源名稱,META-INF/services + 全限定名
          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);
        }
      }
      // 從資源中解析出需要載入的所有實現類的全限定名
      while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
          return false;
        }
        pending = parse(service,configs.nextElement());
      }
      //下一個需要載入的實現類全限定名
      nextName = pending.next();
      return true;
    }

    private S nextService() {
      if (!hasNextService())
        throw new NoSuchElementException();
      String cn = nextName;
      nextName = null;
      Class<?> c = null;
      try {
      //反射構造Class例項
        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());
         // 例項完成,新增快取,Key:實現類全限定類名,Value:實現類例項
        providers.put(cn,p);
        return p;
      } catch (Throwable x) {
        fail(service,"Provider " + cn + " could not be instantiated",x);
      }
      throw new Error();     // This cannot happen
    }

    public boolean hasNext() {
      if (acc == null) {
        return hasNextService();
      } else {
        PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
          public Boolean run() { return hasNextService(); }
        };
        return AccessController.doPrivileged(action,acc);
      }
    }

    public S next() {
      if (acc == null) {
        return nextService();
      } else {
        PrivilegedAction<S> action = new PrivilegedAction<S>() {
          public S run() { return nextService(); }
        };
        return AccessController.doPrivileged(action,acc);
      }
    }

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

  }

  
  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();
      }

    };
  }

  
  public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader)
  {
  // 返回ServiceLoader的例項
    return new ServiceLoader<>(service,loader);
  }

  
  public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
  
    ClassLoader cl = ClassLoader.getSystemClassLoader();
    ClassLoader prev = null;
    while (cl != null) {
      prev = cl;
      cl = cl.getParent();
    }
    return ServiceLoader.load(service,prev);
  }

  
  public String toString() {
    return "java.util.ServiceLoader[" + service.getName() + "]";
  }

}

4.總結

SPI機制在實際開發中使用得場景也有很多。特別是統一標準的不同廠商實現,溪源也正是利用SPI機制(但略做改進,避免過多載入資源浪費)實現不同技術平臺的結果檔案解析需求。

優點

使用Java SPI機制的優勢是實現解耦,使得第三方服務模組的裝配控制的邏輯與呼叫者的業務程式碼分離,而不是耦合在一起。應用程式可以根據實際業務情況啟用框架擴充套件或替換框架元件。

缺點

雖然ServiceLoader也算是使用的延遲載入,但是基本只能通過遍歷全部獲取,也就是介面的實現類全部載入並例項化一遍。如果你並不想用某些實現類,它也被載入並例項化了,這就造成了浪費。

原始碼傳送門:SPI Service

到此這篇關於詳解java實踐SPI機制及淺析原始碼的文章就介紹到這了,更多相關java SPI機制內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!