学习 ShardingSphere 中 spi 应用的优雅之处
为什么要学习 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 中通过全类名定义就行了。
可以看到完全和 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官微