Java的SPI機(jī)制

什么是SPI

SPI全稱Service Provider Interface,是Java提供的一種接口擴(kuò)展機(jī)制。通過該機(jī)制可以將接口的定義與接口的實現(xiàn)分離,實現(xiàn)代碼解耦。

使用方式

SPI的使用方法很簡單,只需要按如下步驟即可:

  1. 定義接口

    package cn.bdqfork.spi;
    
    /**
     * @author bdq
     * @since 2020/3/2
     */
    public interface UserService {
        void sayHello();
    }
    
    
  2. 實現(xiàn)接口

    package cn.bdqfork.spi;
    
    /**
     * @author bdq
     * @since 2020/3/2
     */
    public class UserServiceImpl implements UserService {
        @Override
        public void sayHello() {
            System.out.println("hello");
        }
    }
    
    
  3. 編寫擴(kuò)展文件

    在META-INF/services目錄下創(chuàng)建一個以接口全類名命名的文件,即com.test.service.UserService文件。

    - src
        -main
            -resources
                - META-INF
                    - services
                        - cn.bdqfork.spi.UserService
    

    文件的內(nèi)容為實現(xiàn)類的全類名。

    cn.bdqfork.spi.UserServiceImpl
    
  4. 使用ServiceLoader加載服務(wù)

    package cn.bdqfork.spi;
    
    import java.util.ServiceLoader;
    
    /**
     * @author bdq
     * @since 2020/3/2
     */
    public class Main {
        public static void main(String[] args) {
            ServiceLoader<UserService> userServices = ServiceLoader.load(UserService.class);
            for (UserService userService : userServices) {
                userService.sayHello();
            }
        }
    }
    

SPI的缺點(diǎn)

雖然SPI機(jī)制可以很方便的將接口與其實現(xiàn)分離,但是卻有兩個缺點(diǎn):

  • 在ServiceLoader加載的時候,會一次性將所有的Service都加載到JVM中,包括并不會用到的一些擴(kuò)展。
  • 多線程并發(fā)訪問的時候會有線程問題。

SPI的實現(xiàn)原理

SPI機(jī)制的原理實際上不是很難,整個加載擴(kuò)展服務(wù)的過程如下:

  1. 掃描META-INF/services目錄下的文件。
  2. 讀取文件內(nèi)容,加載服務(wù)實現(xiàn)的Class。
  3. 實例化服務(wù)實現(xiàn)。
  4. 返回服務(wù)實例。

實現(xiàn)一個簡單SPI

了解了SPI的實現(xiàn)原理之后,便可以很簡單的實現(xiàn)一個SPI,下面筆者介紹一下筆者實現(xiàn)的一個SPI,參考了Dubbo的SPI實現(xiàn),但整體原理還是差不多的。

首先定義一個ExtensionLoader類以及相關(guān)屬性:

/**
 * SPI擴(kuò)展
 *
 * @author bdq
 * @since 2019-08-20
 */
public class ExtensionLoader<T> {
    /**
     * 掃描路徑
     */
    private static final String PREFIX = "META-INF/extensions/";
    /**
     * 緩存
     */
    private static final Map<String, ExtensionLoader<?>> CACHES = new ConcurrentHashMap<>();
    /**
     * 擴(kuò)展Class名稱緩存
     */
    private final Map<Class<T>, String> classNames = new ConcurrentHashMap<>();
    /**
     * 擴(kuò)展Class緩存
     */
    private final Map<String, Class<T>> extensionClasses = new ConcurrentHashMap<>();
    /**
     * 擴(kuò)展實例緩存
     */
    private volatile Map<String, T> cacheExtensions;
    /**
     * 默認(rèn)擴(kuò)展名
     */
    private String defaultName;
    /**
     * 擴(kuò)展服務(wù)類型
     */
    private Class<T> type;

    private ExtensionLoader(Class<T> type) {
        this.type = type;
    }
    // ......
}

然后實現(xiàn)加載擴(kuò)展的方法,主要實現(xiàn)了擴(kuò)展實現(xiàn)類的加載,具體代碼如下:

    private void loadExtensionClasses() {
        if (classNames.size() > 0) {
            return;
        }
        try {
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

            // 加載擴(kuò)展文件
            Enumeration<URL> urlEnumeration = classLoader.getResources(PREFIX + type.getName());

            while (urlEnumeration.hasMoreElements()) {

                URL url = urlEnumeration.nextElement();

                if (url.getPath().isEmpty()) {
                    throw new IllegalArgumentException("Extension path " + PREFIX + type.getName() + " don't exsist !");
                }

                // 讀取文件內(nèi)容
                if (url.getProtocol().equals("file") || url.getProtocol().equals("jar")) {

                    URLConnection urlConnection = url.openConnection();
                    Reader reader = new InputStreamReader(urlConnection.getInputStream());
                    BufferedReader bufferedReader = new BufferedReader(reader);

                    // 逐行讀取
                    String line;
                    while ((line = bufferedReader.readLine()) != null) {

                        if (line.equals("")) {
                            continue;
                        }

                        // 過濾注釋
                        if (line.contains("#")) {
                            line = line.substring(0, line.indexOf("#"));
                        }

                        // 解析key=value
                        String[] values = line.split("=");
                        String name = values[0].trim();
                        String impl = values[1].trim();

                        if (extensionClasses.containsKey(name)) {
                            throw new IllegalStateException("Duplicate extension named " + name);
                        }

                        // 加載Class
                        @SuppressWarnings("unchecked")
                        Class<T> clazz = (Class<T>) classLoader.loadClass(impl);

                        // 緩存Class
                        classNames.putIfAbsent(clazz, name);
                        extensionClasses.putIfAbsent(name, clazz);
                    }
                }
            }
        } catch (Exception e) {
            throw new IllegalArgumentException("Fail to get extension class from " + PREFIX + type.getName() + "!", e);
        }
    }

實例化所有的擴(kuò)展,并緩存到Map中。

    /**
     * 獲取所有擴(kuò)展
     *
     * @return Map<String, T>
     */
    public Map<String, T> getExtensions() {
        if (cacheExtensions == null) {
            cacheExtensions = new ConcurrentHashMap<>();
            loadExtensionClasses();

            for (Map.Entry<String, Class<T>> entry : extensionClasses.entrySet()) {
                Class<T> clazz = entry.getValue();
                T instance;
                try {
                    instance = clazz.newInstance();
                } catch (InstantiationException | IllegalAccessException e) {
                    throw new IllegalStateException(e);
                }
                cacheExtensions.putIfAbsent(entry.getKey(), instance);
            }

        }
        return Collections.unmodifiableMap(cacheExtensions);
    }

提供按名獲取擴(kuò)展和獲取默認(rèn)擴(kuò)展的方法。

    /**
     * 根據(jù)extensionName獲取擴(kuò)展實例
     *
     * @param extensionName 擴(kuò)展名稱
     * @return T
     */
    public T getExtension(String extensionName) {
        T extension = getExtensions().get(extensionName);
        if (extension != null) {
            return extension;
        }
        throw new IllegalStateException("No extension named " + extensionName + " for class " + type.getName() + "!");
    }

    /**
     * 根據(jù)extensionName獲取擴(kuò)展實例
     *
     * @return T
     */
    public T getDefaultExtension() {
        T extension = getExtensions().get(defaultName);
        if (extension != null) {
            return extension;
        }
        throw new IllegalStateException("No default extension named " + defaultName + " for class " + type.getName() + "!");
    }

最后提供一個工廠方法,創(chuàng)建ExtensionLoader實例。

    /**
     * 獲取擴(kuò)展接口對應(yīng)的ExtensionLoader
     *
     * @param clazz 擴(kuò)展接口
     * @param <T>   Class類型
     * @return ExtensionLoader<T>
     */
    @SuppressWarnings("unchecked")
    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> clazz) {
        String className = clazz.getName();

        if (!clazz.isInterface()) {
            throw new IllegalArgumentException("Fail to create ExtensionLoader for class " + className
                    + ", class is not Interface !");
        }

        SPI spi = clazz.getAnnotation(SPI.class);

        if (spi == null) {
            throw new IllegalArgumentException("Fail to create ExtensionLoader for class " + className
                    + ", class is not annotated by @SPI !");
        }

        ExtensionLoader<T> extensionLoader = (ExtensionLoader<T>) CACHES.get(className);

        if (extensionLoader == null) {
            CACHES.putIfAbsent(className, new ExtensionLoader<>(clazz));
            extensionLoader = (ExtensionLoader<T>) CACHES.get(className);
            extensionLoader.defaultName = spi.value();
        }

        return extensionLoader;
    }

SPI注解的定義如下:

/**
 * 注解在擴(kuò)展類上,表示可以擴(kuò)展
 *
 * @author bdq
 * @since 2019/9/21
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SPI {
    /**
     * 默認(rèn)擴(kuò)展名
     */
    String value() default "";
}

以上是一個簡單SPI的實現(xiàn)代碼,使用方法也很簡單,跟JDK的使用方法差不多,區(qū)別是需要使用@SPI注解在服務(wù)接口上表明擴(kuò)展需求,且配置文件存儲在META-INF/extensions文件夾下,具體使用方法如下。

  1. 首先定義接口。

    package com.github.bdqfork.core.extension;
    
    /**
     * @author bdq
     * @since 2020/2/22
     */
    @SPI
    public interface IExtensionTest {
    }
    
  2. 實現(xiàn)接口

    package com.github.bdqfork.core.extension;
    
    /**
     * @author bdq
     * @since 2020/2/22
     */
    public class ExtensionTestImpl1 implements IExtensionTest {
    }
    
    package com.github.bdqfork.core.extension;
    
    /**
     * @author bdq
     * @since 2020/2/22
     */
    public class ExtensionTestImpl2 implements IExtensionTest {
    }
    
  3. 編寫配置文件

    在META-INF/services目錄下創(chuàng)建一個以接口全類名命名的文件,即com.github.bdqfork.core.extension.ExtensionLoaderTest文件。

    - src
        -main
            -resources
                - META-INF
                    - extensions
                        - com.github.bdqfork.core.extension.IExtensionTest
    

    文件內(nèi)容如下:

    imp1=com.github.bdqfork.core.extension.ExtensionTestImpl1
    imp2=com.github.bdqfork.core.extension.ExtensionTestImpl2
    

    這里為每一個擴(kuò)展實例提供了名稱。

  4. 通過ExtensionLoader獲取擴(kuò)展實例

    package com.github.bdqfork.core.extension;
    
    import org.junit.jupiter.api.Test;
    
    import static org.junit.jupiter.api.Assertions.*;
    
    class ExtensionLoaderTest {
    
        @Test
        public void getExtension() {
            ExtensionLoader<IExtensionTest> extensionLoader = ExtensionLoader.getExtensionLoader(IExtensionTest.class);
            IExtensionTest iExtensionTest = extensionLoader.getExtension("imp1");
            assert iExtensionTest != null;
        }
    
    }
    

以上就是一個簡單SPI的實現(xiàn),在筆者的ioc容器festival和rpc框架hamal中均使用到了SPI,提供擴(kuò)展服務(wù),感興趣的同學(xué)歡迎查看筆者的項目。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • SPI是什么 SPI的英文名稱是Service Provider Interface,是Java 內(nèi)置的服務(wù)發(fā)現(xiàn)機(jī)...
    GallenZhang閱讀 394評論 0 0
  • 最近在閱讀Dubbo框架源代碼時,經(jīng)??吹紷Spi,查了一下SPI: Service Provider Inter...
    kobe0429閱讀 401評論 0 0
  • SPI的概念 英文全稱為Service Provider Interface 是JDK內(nèi)置的一種服務(wù)提供發(fā)現(xiàn)機(jī)制 ...
    孫先森不可不弘毅閱讀 304評論 0 0
  • 當(dāng)服務(wù)的提供者,提供了服務(wù)接口的一種實現(xiàn)之后,在jar包的META-INF/services/目錄里同時創(chuàng)建一個以...
    男人三餅閱讀 313評論 0 2
  • 1.SPI簡述 SPI(Service Provider Interface),是JDK內(nèi)置的一種服務(wù)提供發(fā)現(xiàn)機(jī)制...
    zhglance閱讀 528評論 0 0

友情鏈接更多精彩內(nèi)容