学习 ShardingSphere 中 spi 应用的优雅之处

SphereEx
企业官方账号
2022-06-06 16:26:06

为什么要学习 ShardingSphere spi

 

之前我们都简单研究过 java spi 和 dubbo 的 spi 机制。那么可能有小伙伴会问既然我们都知道了 duboo 的 spi 机制为什么还要研究 ShardingSphere 的 spi 机制呢?其实原因很简单:

1. ShardingSphere 源码更简单,更容易我们学;

2. ShardingSphere 中的 spi 机制实现的也比较优雅,核心代码很少,更贴合我们平时项目使用,仅仅只有 spi 的封装,更简洁,不像 dubbo 中可能还增加了 ioc 相关的功能等。

ShardingSphere spi

我们这里还是简单说一下 java spi 机制的一些缺点:

1. 多个并发多线程使用 ServiceLoader 类的实例是不安全的

2. 每次获取元素需要遍历全部元素,不能按需加载。

3. 加载不到实现类时抛出并不是真正原因的异常,错误很难定位

4. 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类

基于这些问题我们来看看 ShardingSphere 是如何简洁地解决这些问题的。

加载 spi 类

dubbo 对于自己的 spi 直接是重写了,用法和 jdk 可以说是完全不一样,包括 spi 的文件名,以及文件配置方式,我们这里还是简单对比下 dubbo 和 java spi 的使用区别。

java spi

在文件夹 META-INF/services 下添加接口的实现类

org.apache.dubbo.OptimusPrime
org.apache.dubbo.Bumblebee

dubbo spi

在文件夹 META-INF/services 下添加接口的实现类以 key,value 的方式配置,类似如下:

optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee

可以看到 dubbo 的 spi 基本和原先的 jdk spi 完全不一致了。

ShardingSphere 是如何更简洁地扩展 jdk spi

与 dubbo 实现理念不同的是 ShardingSphere 用了更少的代码在 jdk spi 上做了扩展。

1. 首先是配置方式完全和 java spi 一致

我们以 DialectTableMetaDataLoader 接口的实现类为例

  • DialectTableMetaDataLoader.class

public interface DialectTableMetaDataLoader extends StatelessTypedSPI {

    /**
     * Load table meta data.
     *
     * @param dataSource data source
     * @param tables tables
     * @return table meta data map
     * @throws SQLException SQL exception
     */
    Map<String, TableMetaData> load(DataSource dataSource, Collection<String> tables) throws SQLException;
}
  • TypedSPI.class

public interface TypedSPI {

    /**
     * Get type.
     * 
     * @return type
     */
    String getType();

    /**
     * Get type aliases.
     *
     * @return type aliases
     */
    default Collection<String> getTypeAliases() {
        return Collections.emptyList();
    }
}

StatelessTypedSPI 接口继承于 TypedSPI,多接口继承用于满足接口职责单一原则,其中 TypedSPI 就是子类需要指定自己 spi 中的 Map 中的 key

这里我们完全无需关心 DialectTableMetaDataLoader 接口定义的是什么方法,我们重点是要关心子类的如何通过 spi 加载的。这里如果是 java spi,我们需要如何加载子类呢?很简单,在 META-INF/services 中通过全类名定义就行了。

4f7c7e0de39b61a369bee106d2d25ab1.png

可以看到完全和 java 原生的 spi 配置方式一致。那么是如何解决原生 java spi 的缺点的呢?

工厂设计模式的使用

在对于每一个接口需要通过 spi 扩展并创建的时候,一般会有一个类似的 xxDataLoaderFactory 来创建获取指定的 spi 扩展类。

DialectTableMetaDataLoaderFactory

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class DialectTableMetaDataLoaderFactory {

    static {
        ShardingSphereServiceLoader.register(DialectTableMetaDataLoader.class);
    }

    /**
     * Create new instance of dialect table meta data loader.
     * 
     * @param databaseType database type
     * @return new instance of dialect table meta data loader
     */
    public static Optional<DialectTableMetaDataLoader> newInstance(final DatabaseType databaseType) {
        return TypedSPIRegistry.findRegisteredService(DialectTableMetaDataLoader.class, databaseType.getName());
    }
}

这里可以看到这里使用了静态代码块,在类加载的时候就通过方法 ShardingSphereServiceLoader.register 注册了 DialectTableMetaDataLoader 的所有实现类,我们需要获取我们的指定的spi 扩展类就通过方法 TypedSPIRegistry.findRegisteredService 去获取。

TypedSPIRegistry.findRegisteredService(final Class<T> spiClass, final String type)

所以我们核心就看看 ShardingSphereServiceLoader.register`` 和 ypedSPIRegistry.findRegisteredService` 方法即可。

ShardingSphereServiceLoader

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ShardingSphereServiceLoader {

    private static final Map<Class<?>, Collection<Object>> SERVICES = new ConcurrentHashMap<>();

    /**
     * Register service.
     *
     * @param serviceInterface service interface
     */
    public static void register(final Class<?> serviceInterface) {
        if (!SERVICES.containsKey(serviceInterface)) {
            SERVICES.put(serviceInterface, load(serviceInterface));
        }
    }

    private static <T> Collection<Object> load(final Class<T> serviceInterface) {
        Collection<Object> result = new LinkedList<>();
        for (T each : ServiceLoader.load(serviceInterface)) {
            result.add(each);
        }
        return result;
    }

    /**
     * Get singleton service instances.
     *
     * @param service service class
     * @param <T> type of service
     * @return service instances
     */
    @SuppressWarnings("unchecked")
    public static <T> Collection<T> getSingletonServiceInstances(final Class<T> service) {
        return (Collection<T>) SERVICES.getOrDefault(service, Collections.emptyList());
    }

    /**
     * New service instances.
     *
     * @param service service class
     * @param <T> type of service
     * @return service instances
     */
    @SuppressWarnings("unchecked")
    public static <T> Collection<T> newServiceInstances(final Class<T> service) {
        if (!SERVICES.containsKey(service)) {
            return Collections.emptyList();
        }
        Collection<Object> services = SERVICES.get(service);
        if (services.isEmpty()) {
            return Collections.emptyList();
        }
        Collection<T> result = new ArrayList<>(services.size());
        for (Object each : services) {
            result.add((T) newServiceInstance(each.getClass()));
        }
        return result;
    }

    private static Object newServiceInstance(final Class<?> clazz) {
        try {
            return clazz.getDeclaredConstructor().newInstance();
        } catch (final ReflectiveOperationException ex) {
            throw new ServiceLoaderInstantiationException(clazz, ex);
        }
    }
}

可以看到所有的 spi 类都是放在 SERVICES 这个属性中

private static final Map<Class<?>, Collection<Object>> SERVICES = new ConcurrentHashMap<>();

而注册也是很简单,直接调用 java 默认的 spi api:

public static void register(final Class<?> serviceInterface) {
        if (!SERVICES.containsKey(serviceInterface)) {
            SERVICES.put(serviceInterface, load(serviceInterface));
        }
    }
private static <T> Collection<Object> load(final Class<T> serviceInterface) {
        Collection<Object> result = new LinkedList<>();
        for (T each : ServiceLoader.load(serviceInterface)) {
            result.add(each);
        }
        return result;
    }

TypedSPIRegistry

TypedSPIRegistry 中的 findRegisteredService 方法本质上其实也是调用的 ShardingSphereServiceLoader 的 getSingletonServiceInstances 方法。

  • TypedSPIRegistry

public static <T extends StatelessTypedSPI> Optional<T> findRegisteredService(final Class<T> spiClass, final String type) {
        for (T each : ShardingSphereServiceLoader.getSingletonServiceInstances(spiClass)) {
            if (matchesType(type, each)) {
                return Optional.of(each);
            }
        }
        return Optional.empty();
    }

private static boolean matchesType(final String type, final TypedSPI typedSPI) {
        return typedSPI.getType().equalsIgnoreCase(type) || typedSPI.getTypeAliases().contains(type);
    }

这里可以看到通过扩展类也就是通过 TypedSPI 中的 getType 或 getTypeAliases 去匹配,这就是为什么每个 spi 需要去实现 TypedSPI 接口。

我们这里再来看看 ShardingSphereServiceLoader 中的 newServiceInstances 方法:

public static <T> Collection<T> newServiceInstances(final Class<T> service) {
        if (!SERVICES.containsKey(service)) {
            return Collections.emptyList();
        }
        Collection<Object> services = SERVICES.get(service);
        if (services.isEmpty()) {
            return Collections.emptyList();
        }
        Collection<T> result = new ArrayList<>(services.size());
        for (Object each : services) {
            result.add((T) newServiceInstance(each.getClass()));
        }
        return result;
    }

可以看到也是非常简单的,直接在直接通过静态代码块注册的 SERVICES 中找到接口的所有实现类返回。

到这里 ShardingSphere 的 spi 源码基本就分析清晰了,是不是比 dubbo 的 spi 实现的更简单,更容易使用。

总结

ShardingSphere 的 spi 相比 dubbo 的 spi 功能上都是满足通过 key 去寻找指定实现类,不用每次使用都重新加载所有实现类,也解决了并发加载问题。但是相比 dubbo,ShardingSphere spi 实现的更简洁,更容易使用。

后续我们在自己编写需要有 spi 扩展的时候完全可以参考 ShardingSphere 这一套实现方式,因为实现的比较简单,但是也很好用。后续我们有机会可以基于 spi 写一个可扩展的配置文件解析器,让大家明白 spi 的强大与实际应用场景。

欢迎更多使用 ShardingSphere 的开发者们与我们分享使用经验。

...全文
288 回复 打赏 收藏 转发到动态 举报
AI 作业
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

18

社区成员

发帖
与我相关
我的任务
社区描述
这里是关于 SphereEx 和 Apache ShardingSphere 技术交流的专属空间,关于它们的一切都可以在这里找到。
数据库 企业社区
社区管理员
  • SphereEx
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

有任何技术问题,欢迎添加社区小助手微信(ss_assistant_1)加入交流群,与众多 ShardingSphere 爱好者一同交流

试试用AI创作助手写篇文章吧