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

alan_ham
发布于 2022-10-14 16:25
浏览
0收藏

为什么要学习 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 中通过全类名定义就行了。

学习 ShardingSphere 中 spi 应用的优雅之处-鸿蒙开发者社区

可以看到完全和 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官微

分类
已于2022-10-14 16:25:32修改
收藏
回复
举报
回复
    相关推荐