Зачем изучать SPI ShardingSphere?
Возможно, вы уже знакомы с Java и механизмом SPI (Service Provider Interface) в Dubbo, поэтому вы можете задаться вопросом: “Зачем мне изучать механизм SPI в ShardingSphere?”. Причины довольно просты:
- Исходный код ShardingSphere проще и легче адаптируется.
- Выполнение механизма SPI ShardingSphere довольно плавное, и для повседневных операций требуется меньше кода. В отличие от механизма SPI Dubbo и его дополнительных возможностей, связанных с IoC, механизм ShardingSphere сохраняет только фундаментальную структуру, что делает его простым в использовании.
Понимание SPI ShardingSphere
Мы также должны упомянуть о некоторых недостатках механизма SPI в Java:
- Экземпляры класса ServiceLoader с несколькими параллельными потоками использовать небезопасно.
- Каждый раз, когда вы получаете элемент, вам нужно перебрать все элементы, и вы не можете загружать их по требованию.
- Когда класс реализации не удается загрузить, выдается исключение без указания реальной причины, что затрудняет обнаружение ошибки.
- Способ получения класса реализации не является достаточно гибким. Его можно получить только через форму Iterator, а не на основе одного параметра для получения соответствующего класса реализации.
В свете этого давайте посмотрим, как ShardingSphere решает эти проблемы простым способом.
Загрузка класса SPI
Dubbo является прямым переписыванием собственного SPI, включая имя SPI-файла и способ его конфигурирования, что резко отличается от JDK. Давайте кратко сравним различия между их использованием:
Java SPI
Добавить класс реализации интерфейса в папку META-INF/services
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee
Dubbo SPI
Добавьте класс реализации интерфейса в папку META-INF/services
, настройте с помощью key
, value
, как показано в следующем примере:
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee
Теперь мы видим, что Java SPI Dubbo полностью отличается от SPI JDK.
Как ShardingSphere легко расширяет SPI JDK?
В отличие от концепции реализации Dubbo, ShardingSphere расширяет JDK SPI с меньшим количеством кода.
- Конфигурация точно такая же, как в 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
.
Как вы можете видеть, это точно такая же конфигурация, как и конфигурация родного 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
Метод findRegisteredService
в TypedSPIRegistry
по сути является вызовом метода getSingletonServiceInstancesmethod
из ShardingSphereServiceLoader
.
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);
}
Здесь видно, что расширение класса использует getType
или getTypeAliases
в TypedSPI
для получения соответствия, поэтому каждый SPI должен реализовать интерфейс TypedSPI
.
Теперь рассмотрим метод newServiceInstances
в ShardingSphereServiceLoader
.
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
, зарегистрированных через блок статического кода.
Несмотря на свою краткость, это короткое прохождение в основном представило исходный код SPI ShardingSphere. Мы уверены, что вы уже заметили, что работать с SPI ShardingSphere гораздо легче и проще, чем с SPI механизма Dubbo.
Резюме
И ShardingSphere, и Dubbo SPI удовлетворяют требованию поиска указанного класса реализации по ключу, без необходимости перезагрузки всех классов реализации при каждом использовании, что решает проблему одновременной загрузки. Однако, по сравнению с Dubbo, SPI ShardingSphere более упрощен и прост в использовании.
Вы можете обратиться к реализации ShardingSphere позже при написании собственных расширений SPI, поскольку она проще в реализации и элегантна в работе. Вы можете написать расширяемый парсер конфигурационных файлов на основе SPI, чтобы мы могли понять, на что способен SPI, а также сценарии его применения.
Ссылки на проект Apache ShardingSphere:
ShardingSphere Github
ShardingSphere Twitter
ShardingSphere Slack
Руководство для контрибьюторов