理解 Apache ShardingSphere 的 SPI,以及为什么它比 Dubbo 的简单
[](https://res.cloudinary.com/practicaldev/image/fetch/s--Wq9eRXge--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/uploads/articles/phd62v4hi28k41td0gws.p
[](https://res.cloudinary.com/practicaldev/image/fetch/s--Wq9eRXge--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/uploads/articles/phd62v4hi28k41td0gws.png)
为什么要学习ShardingSphere的 SPI?
您可能已经熟悉Java和Dubbo的 SPI(Service Provider Interface)机制,那么您可能会疑惑“为什么要学习ShardingSphere的SPI机制?”原因很简单:
- ShardingSphere的源码更简单,更容易适配。
2、ShardingSphere的SPI机制执行相当流畅,日常操作所需的代码较少。与 Dubbo 的 SPI 机制及其与IoC相关的附加特性不同,ShardingSphere 中的 SPI 机制只保留了基本结构,使用起来毫不费力。
了解 ShardingSphere 的 SPI
我们还不得不提到Java SPI机制中发现的一些缺点:
1.ServiceLoader类的实例有多个并发线程使用不安全。
2.每次获取一个元素,都需要遍历所有元素,不能按需加载。
- 实现类加载失败时,提示异常,不说明真实原因,导致错误难以定位。
4、获取实现类的方式不够灵活。只能通过Iterator形式获取,不能根据一个参数获取对应的实现类。
有鉴于此,让我们看看ShardingSphere是如何用简单的方式解决这些问题的。
加载SPI类
Dubbo 是直接重写了自己的 SPI,包括 SPI 文件名和文件配置方式,与 JDK形成鲜明对比。让我们简单比较一下这两种用法的区别:
Java SPI
在META-INF/services
文件夹下添加接口实现类
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee
进入全屏模式 退出全屏模式
达博SPI
将接口的实现类添加到文件夹META-INF/services
中,通过key
、value
进行配置,如下例:
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee
进入全屏模式 退出全屏模式
现在我们可以看到 Dubbo 的 Java 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;
}
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
是Map
的键,其中子类需要指定自己的 SPI。
这里不需要关心DialectTableMetaDataLoader
接口定义了哪些方法,只需要关注SPI如何加载子类即可。如果是 Java SPI,要加载子类,只需在META-INF/services
中使用完整的类名来定义它。
[](https://res.cloudinary.com/practicaldev/image/fetch/s--q0h6UMuK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/uploads/articles/9nzzkyhn91dpzvbkapxy.png)
如您所见,它与本机 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());
}
}
进入全屏模式 退出全屏模式
这里可以看到使用了一个静态块,在类加载过程中,所有的DialectTableMetaDataLoader
个实现类都通过ShardingSphereServiceLoader.register
注册了。通过使用TypedSPIRegistry.findRegisteredService
,我们可以得到我们指定的spi扩展类。
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.
*
* eparam 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<>();
进入全屏模式 退出全屏模式
And registering is pretty simple too, just use the SPI api embedded in java.
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
的getSingletonServiceInstancesmethod
的调用。
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 源代码。相信您已经注意到,使用 ShardingSphere 的 SPI 比使用 Dubbo 的 SPI 机制更容易、更简单。
总结
ShardingSphere 和 Dubbo 的 SPI 都满足按键查找指定实现类的需求,无需每次使用都重新加载所有实现类,解决并发加载问题。但是相比 Dubbo,ShardingSphere SPI 更加精简,使用更加方便。
您可以稍后在编写自己的 SPI 扩展时参考 ShardingSphere 的实现,因为它实现起来更简单,使用起来也很优雅。您可以编写一个基于SPI的可扩展配置文件解析器,以便我们了解SPI的能力以及它的应用场景。
Apache ShardingSphere 项目链接:
ShardingSphere Github
ShardingSphere 推特
ShardingSphere Slack
贡献者指南
更多推荐
所有评论(0)