java SPI 06-本身從零手寫實現 SPI 框架

系列目錄

spi 01-spi 是什麼?入門使用java

spi 02-spi 的實戰解決 slf4j 包衝突問題git

spi 03-spi jdk 實現源碼解析github

spi 04-spi dubbo 實現源碼解析spring

spi 05-dubbo adaptive extension 自適應拓展api

spi 06-本身從零手寫實現 SPI 框架緩存

spi 07-自動生成 SPI 配置文件實現方式安全

回顧

學習了 java 的 SPI 和 dubbo 的 SPI 實現以後,但願實現一個屬於本身的 SPI 框架。併發

但願具備以下特性:框架

(1)相似 dubbo 的真正的惰性加載,而不是遍歷一堆一須要的實例ide

(2)併發安全考慮

(3)支持基於名稱獲取實現類,後期能夠添加更多的特性支持。相似 spring 的 IOC

(4)儘量的簡化實現

使用演示

類實現

  • Say.java
@SPI
public interface Say {

    void say();

}
  • SayBad.java
public class SayBad implements Say {

    @Override
    public void say() {
        System.out.println("bad");
    }

}
  • SayGood.java
public class SayGood implements Say {

    @Override
    public void say() {
        System.out.println("good");
    }

}

文件配置

META-INF/services/ 文件夾下定義文件 com.github.houbb.spi.bs.spi.Say

內容以下:

good=com.github.houbb.spi.bs.spi.impl.SayGood
bad=com.github.houbb.spi.bs.spi.impl.SayBad

測試案例

ExtensionLoader<Say> loader = SpiBs.load(Say.class);

Say good = loader.getExtension("good");
Say bad = loader.getExtension("bad");

good.say();
bad.say();

日誌輸出:

good
bad

總體目錄

├─annotation
│      SPI.java
│
├─api
│  │  IExtensionLoader.java
│  │
│  └─impl
│          ExtensionLoader.java
│
├─bs
│      SpiBs.java
│
├─constant
│      SpiConst.java
│
└─exception
        SpiException.java

源碼分析

annotation 註解

@SPI 相似於 dubbo 的註解,標識一個接口爲 SPI 接口。

這樣嚴格控制,便於後期拓展和管理,下降代碼複雜度。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface SPI {
}

bs 引導類

  • SpiBs.java

核心引導類

public final class SpiBs {

    private SpiBs(){}

    /**
     * 拓展加載類 map
     * @since 0.0.1
     */
    private static final Map<Class, ExtensionLoader> EX_LOADER_MAP = new ConcurrentHashMap<>();

    /**
     * 加載實例
     * @param clazz 類
     * @param <T> 泛型
     * @return 結果
     * @since 0.0.1
     */
    @SuppressWarnings("unchecked")
    public static <T> ExtensionLoader<T> load(Class<T> clazz) {
        ArgUtil.notNull(clazz, "clazz");

        ExtensionLoader extensionLoader = EX_LOADER_MAP.get(clazz);
        if(EX_LOADER_MAP.get(clazz) != null) {
            return extensionLoader;
        }

        // DLC
        synchronized (EX_LOADER_MAP) {
            extensionLoader = EX_LOADER_MAP.get(clazz);
            if(extensionLoader == null) {
                extensionLoader = new ExtensionLoader(clazz);
            }
        }

        return extensionLoader;
    }

}

api 核心實現

  • ExtensionLoader.java
import com.github.houbb.heaven.annotation.CommonEager;
import com.github.houbb.heaven.annotation.ThreadSafe;
import com.github.houbb.heaven.response.exception.CommonRuntimeException;
import com.github.houbb.heaven.util.common.ArgUtil;
import com.github.houbb.heaven.util.lang.StringUtil;
import com.github.houbb.heaven.util.util.CollectionUtil;
import com.github.houbb.spi.annotation.SPI;
import com.github.houbb.spi.api.IExtensionLoader;
import com.github.houbb.spi.constant.SpiConst;
import com.github.houbb.spi.exception.SpiException;

import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 默認實現
 * @author binbin.hou
 * @since 0.0.1
 */
@ThreadSafe
public class ExtensionLoader<T> implements IExtensionLoader<T> {

    /**
     * 接口定義
     * @since 0.0.1
     */
    private final Class<T> spiClass;

    /**
     * 類加載器
     * @since 0.0.1
     */
    private final ClassLoader classLoader;

    /**
     * 緩存的對象實例
     * @since 0.0.1
     */
    private final Map<String, T> cachedInstances = new ConcurrentHashMap<>();

    /**
     * 實例別名 map
     * @since 0.0.1
     */
    private final Map<String, String> classAliasMap = new ConcurrentHashMap<>();

    public ExtensionLoader(Class<T> spiClass, ClassLoader classLoader) {
        spiClassCheck(spiClass);
        ArgUtil.notNull(classLoader, "classLoader");

        this.spiClass = spiClass;
        this.classLoader = classLoader;

        // 初始化配置
        this.initSpiConfig();
    }

    public ExtensionLoader(Class<T> spiClass) {
        this(spiClass, Thread.currentThread().getContextClassLoader());
    }

    /**
     * 獲取對應的拓展信息
     *
     * @param alias 別名
     * @return 結果
     * @since 0.0.1
     */
    @Override
    public T getExtension(String alias) {
        ArgUtil.notEmpty(alias, "alias");

        //1. 獲取
        T instance = cachedInstances.get(alias);
        if(instance != null) {
            return instance;
        }

        // DLC
        synchronized (cachedInstances) {
            instance = cachedInstances.get(alias);
            if(instance == null) {
                instance = createInstance(alias);
                cachedInstances.put(alias, instance);
            }
        }

        return instance;
    }

    /**
     * 實例
     * @param name 名稱
     * @return 實例
     * @since 0.0.1
     */
    @SuppressWarnings("unchecked")
    private T createInstance(String name) {
        String className = classAliasMap.get(name);
        if(StringUtil.isEmpty(className)) {
            throw new SpiException("SPI config not found for spi: " + spiClass.getName()
                    +" with alias: " + name);
        }

        try {
            Class clazz = Class.forName(className);
            return (T) clazz.newInstance();
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
            throw new SpiException(e);
        }
    }

    /**
     * 參數校驗
     *
     * 1. 不能爲 null
     * 2. 必須是接口
     * 3. 必須指定 {@link com.github.houbb.spi.annotation.SPI} 註解
     * @param spiClass spi 類
     * @since 0.0.1
     */
    private void spiClassCheck(final Class<T> spiClass) {
        ArgUtil.notNull(spiClass, "spiClass");

        if(!spiClass.isInterface()) {
            throw new SpiException("Spi class is not interface, " + spiClass);
        }
        if(!spiClass.isAnnotationPresent(SPI.class)) {
            throw new SpiException("Spi class is must be annotated with @SPI, " + spiClass);
        }
    }

    /**
     * 初始化配置文件名稱信息
     *
     * 只加載當前類的文件信息
     * @since 0.0.1
     */
    private void initSpiConfig() {
        // 文件的讀取
        String fullName = SpiConst.JDK_DIR+this.spiClass.getName();

        try {
            Enumeration<URL> urlEnumeration = this.classLoader
                    .getResources(fullName);

            // 沒有更多元素
            if(!urlEnumeration.hasMoreElements()) {
                throw new SpiException("SPI config file for class not found: "
                        + spiClass.getName());
            }

            // 獲取第一個元素
            URL url = urlEnumeration.nextElement();
            List<String> allLines = readAllLines(url);

            // 構建 map
            if(CollectionUtil.isEmpty(allLines)) {
                throw new SpiException("SPI config file for class is empty: " + spiClass.getName());
            }

            for(String line : allLines) {
                String[] lines = line.split(SpiConst.SPLITTER);
                classAliasMap.put(lines[0], lines[1]);
            }
        } catch (IOException e) {
            throw new SpiException(e);
        }
    }

    /**
     * 讀取每一行的內容
     * @param url url 信息
     * @return 結果
     * @since 0.0.1
     */
    @CommonEager
    private List<String> readAllLines(final URL url) {
        ArgUtil.notNull(url, "url");

        List<String> resultList = new ArrayList<>();

        try(InputStream is = url.openStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
            // 按行讀取信息
            String line;
            while ((line = br.readLine()) != null) {
                resultList.add(line);
            }
        } catch (IOException e) {
            throw new CommonRuntimeException(e);
        }
        return resultList;
    }

}

其餘

其餘的只是一些常量定義等。

完整代碼見:

spi

進步一思考

這裏只實現了最基本的功能,能夠後續添加更多豐富的特性。

還有一個問題就是,咱們能不能像使用 jdk spi 那樣,去使用 google auto,直接一個註解,幫咱們省略掉文件建立的過程呢?

固然是能夠的,下一節,咱們就來實現一個這樣的功能。

相關文章
相關標籤/搜索