per 提供者 num asn 自動類型轉換 value imp 中大 util

深入理解Java中的spi機制

SPI全名為Service Provider Interface是JDK內置的一種服務提供發現機制,是Java提供的一套用來被第三方實現或者擴展的API,它可以用來啟用框架擴展和替換組件。

JAVA SPI = 基於接口的編程+策略模式+配置文件 的動態加載機制

Java SPI的具體約定如下:

當服務的提供者,提供了服務接口的一種實現之後,在jar包的META-INF/services/目錄裏同時創建一個以服務接口命名的文件。該文件裏就是實現該服務接口的具體實現類。

而當外部程序裝配這個模塊的時候,就能通過該jarMETA-INF/services/裏的配置文件找到具體的實現類名,並裝載實例化,完成模塊的註入。

根據SPI的規範我們的服務實現類必須有一個無參構造方法

為什麽一定要在classes中的META-INF/services下呢?

JDK提供服務實現查找的一個工具類:java.util.ServiceLoader

在這個類裏面已經寫死

// 默認會去這裏尋找相關信息
private static final String PREFIX = "META-INF/services/";

常見的使用場景:

  • JDBC加載不同類型的數據庫驅動
  • 日誌門面接口實現類加載,SLF4J加載不同提供商的日誌實現類
  • Spring中大量使用了SPI,
    • servlet3.0規範
    • ServletContainerInitializer的實現
    • 自動類型轉換Type Conversion SPI(Converter SPI、Formatter SPI)
  • Dubbo裏面有很多個組件,每個組件在框架中都是以接口的形成抽象出來!具體的實現又分很多種,在程序執行時根據用戶的配置來按需取接口的實現

簡單的spi實例

整體包結構如下

└─main
    ├─java
    │  └─com
    │      └─xinchen
    │          └─spi
    │              └─App.java
    │              └─IService.java
    │              └─ServiceImplA.java
    │              └─ServiceImplB.java            
    └─resources
        └─META-INF
            └─services
                └─com.xinchen.spi.IService

SPI接口

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

具體實現類

public class ServiceImplA implements IService {
    @Override
    public void say(String word) {
        System.out.println(this.getClass().toString() + " say: " + word);
    }
}

public class ServiceImplB implements IService {
    @Override
    public void say(String word) {
        System.out.println(this.getClass().toString() + " say: " + word);
    }
}

/resource/META-INF/services/com.xinchen.spi.IService

com.xinchen.spi.ServiceImplA
com.xinchen.spi.ServiceImplB

Client類

public class App {
    static ServiceLoader<IService> services = ServiceLoader.load(IService.class);

    public static void main(String[] args) {
        for (IService service:services){
            service.say("Hello World!");
        }
    }
}

//    結果:
//    class com.xinchen.spi.ServiceImplA say: Hello World!
//    class com.xinchen.spi.ServiceImplB say: Hello World!

源碼解析

java.util.ServiceLoader中的Fied區域

    // 加載具體實現類信息的前綴
    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;

ServiceLoader.load(IService.class)進入源碼中

    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 獲取當前線程上下文的類加載器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

ServiceLoader.load(service, cl)

    public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
        // 返回ServiceLoader的實例
        return new ServiceLoader<>(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();
    }

    public void reload() {
        // 清空已經緩存的加載的接口實現類
        providers.clear();
        // 創建新的延遲加載叠代器
        lookupIterator = new LazyIterator(service, loader);
    }   
    
    private LazyIterator(Class<S> service, ClassLoader loader) {
        // 指定this類中的 需要加載的接口service和類加載器loader
        this.service = service;
        this.loader = loader;
    }         

當我們通過叠代器獲取對象實例的時候,首先在成員變量providers中查找是否有緩存的實例對象

如果存在則直接返回,否則則調用lookupIterator延遲加載叠代器進行加載

叠代器判斷的代碼如下

public Iterator<S> iterator() {
        // 返回叠代器
        return new Iterator<S>() {
            // 查詢緩存中是否存在實例對象
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();

            public boolean hasNext() {
                // 如果緩存中已經存在返回true
                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();
            }

        };
    }

LazyIterator的類加載

        // 判斷是否擁有下一個實例
        private boolean hasNextService() {
            // 如果擁有直接返回true
            if (nextName != null) {
                return true;
            }

            // 具體實現類的全名 ,Enumeration<URL> config
            if (configs == null) {
                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);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                // 轉換config中的元素,或者具體實現類的真實包結構
                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 {
                // 加載類對象
                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 {
                // 通過c.newInstance()實例化
                S p = service.cast(c.newInstance());
                // 將實現類加入緩存
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

總結

優點

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

缺點

  • 多個並發多線程使用ServiceLoader類的實例是不安全的

  • 雖然ServiceLoader也算是使用的延遲加載,但是基本只能通過遍歷全部獲取,也就是接口的實現類全部加載並實例化一遍。

參考

The Java? Tutorials

聊聊Dubbo(五):核心源碼-SPI擴展

深入理解Java SPI機制

【Java】深入理解Java中的spi機制