Понимание SPI Apache ShardingSphere и почему он проще, чем SPI Dubbo

Зачем изучать SPI ShardingSphere?

Возможно, вы уже знакомы с Java и механизмом SPI (Service Provider Interface) в Dubbo, поэтому вы можете задаться вопросом: “Зачем мне изучать механизм SPI в ShardingSphere?”. Причины довольно просты:

  1. Исходный код ShardingSphere проще и легче адаптируется.
  2. Выполнение механизма SPI ShardingSphere довольно плавное, и для повседневных операций требуется меньше кода. В отличие от механизма SPI Dubbo и его дополнительных возможностей, связанных с IoC, механизм ShardingSphere сохраняет только фундаментальную структуру, что делает его простым в использовании.

Понимание SPI ShardingSphere

Мы также должны упомянуть о некоторых недостатках механизма SPI в Java:

  1. Экземпляры класса ServiceLoader с несколькими параллельными потоками использовать небезопасно.
  2. Каждый раз, когда вы получаете элемент, вам нужно перебрать все элементы, и вы не можете загружать их по требованию.
  3. Когда класс реализации не удается загрузить, выдается исключение без указания реальной причины, что затрудняет обнаружение ошибки.
  4. Способ получения класса реализации не является достаточно гибким. Его можно получить только через форму 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 с меньшим количеством кода.

  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.

Как вы можете видеть, это точно такая же конфигурация, как и конфигурация родного 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

Руководство для контрибьюторов

Оцените статью
Procodings.ru
Добавить комментарий