系列目录

spi 01-spi 是什么?入门使用

spi 02-spi 的实战解决 slf4j 包冲突问题

spi 03-spi jdk 实现源码解析

spi 04-spi dubbo 实现源码解析

spi 05-dubbo adaptive extension 自适应拓展

spi 06-自己从零手写实现 SPI 框架

spi 07-自动生成 SPI 配置文件实现方式

回顾

学习了 java 的 SPI 和 dubbo 的 SPI 实现之后,希望实现一个属于自己的 SPI 框架。

希望具有如下特性:

(1)类似 dubbo 的真正的惰性加载,而不是遍历一堆一需要的实例

(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,直接一个注解,帮我们省略掉文件创建的过程呢?

当然是可以的,下一节,我们就来实现一个这样的功能。