双11狂欢大促

盲盒赢 定制羽绒服

开启惊喜盲盒,抽取你的专属好礼

— 惊喜奖品抢先看 —

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

SphereEx SphereEx 墨衍会员 · AI 创作全网分发 墨衍智能分发 通过墨衍一键同步至各平台 可同步平台 CSDN微信公众号微博知乎掘金百家号博客园抖音小红书 你的文章也可以这样分发 墨衍会员 ¥399起/年,写一次发全网 我也要 + 加入社区

为什么要学习 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 的开发者们与我们分享使用经验。

...全文

相关推荐

awesome-system-design-resources(Java)

「awesome-system-design-resources(Java)」是一套Java项目完整源码,涵盖精选资源合集、后端、分布式系统等核心内容。代码结构清晰、注释完整,包含全部源代码文件,下载解压即可运行使用,适合学习参考、课程设计、毕业设计与二次开发。

phpgolife

Java 最常见200 面试必备

不错的资源,需要的朋友下载看看

啊申

ShardingSphere数据脱敏实战:透明加密与模糊查询解决方案

数据脱敏是数据安全领域的基础技术,旨在保护敏感信息在存储、传输和使用过程中的隐私性。其核心原理是通过加密、替换或屏蔽等手段,将原始数据转换为无法直接识别的形式,同时确保在授权场景下可恢复或安全使用。这项技术的核心价值在于平衡数据安全与业务可用性,帮助企业在满足GDPR等合规要求的同时,保障业务连续性和用户体验。在应用层面,数据脱敏广泛应用于金融、政务、医疗等行业的用户隐私保护、测试数据安全共享等场景。本文聚焦于Apache ShardingSphere这一主流开源框架,深入解析其如何通过透明加解密机制,实现

weixin_30417487

微内核和宏内核

微内核是一种典型的架构模式 ,区别于普通的设计模式,架构模式是一种高层模式,用于描述系统级的结构组成、相互关系及相关约束。微内核架构在开源框架中的应用非常广泛,比如常见的 ShardingSphere 还有Dubbo都实现了自己的微内核架构。那么,在介绍什么是微内核架构之前,我们有必要先阐述这些开源框架会使用微内核架构的原因。

薛定谔的猫1982

【信息科学与工程学】计算机科学与自动化——第三百零四篇 高性能系统开发指南01

高并发:限流、熔断、降级高性能:红包拆分、多级缓存、大Key/热Key拆分、Pipeline、线程池、RocketMQ零丢失、幂等、分布式锁、集群缩容、分批入库海量数据处理:日志分表分片、跨表查询、冷热分层、大数据引擎服务器选型系统安全:网关安全、服务器安全、云存储安全、风控、监控、测试数据一致性:缓存与数据库一致性、大厂方案、蓝绿回滚、流量一致性、流程一致性项目与团队管理:流程问题、管理问题(风险、成本、人员、文档、培训)稳定性建设:上线三板斧、线上五步闭环、混沌工程、容量规划。

flyair_China
评论
收藏 已收藏
分享
返回顶部
SphereEx
SphereEx 企业官方账号 企业官方账号
SphereEx 18 成员 19 内容