JDK/Dubbo/Spring три механизма SPI, что лучше?

Java

Ставь лайк и потом смотри, вырабатывай полезную привычку

Полное название SPI — интерфейс поставщика услуг, представляющий собой механизм обнаружения услуг. Суть SPI заключается в настройке полного имени класса реализации интерфейса в файле, а файл конфигурации считывается загрузчиком службы для загрузки класса реализации. Это позволяет вам динамически заменять класс реализации для интерфейса во время выполнения. Благодаря этой функции мы можем легко предоставлять расширенные функции для наших программ через механизм SPI.

Эта статья в основном посвящена функциям и использованию и не включает анализ исходного кода (исходный код очень прост, я думаю, вы поймете его с первого взгляда).

Для чего нужен СПИ?

Например, сейчас мы разработали новую структуру ведения журнала:super-logger. По умолчанию в качестве конфигурационного файла для нашего журнала используется XML-файл, и разработан интерфейс для разбора конфигурационного файла:

package com.github.kongwu.spisamples;

public interface SuperLoggerConfiguration {
	void configure(String configFile);
}

Затем идет реализация XML по умолчанию:

package com.github.kongwu.spisamples;

public class XMLConfiguration implements SuperLoggerConfiguration{
	public void configure(String configFile){
    	......
    }
}

Затем, когда мы инициализируем и анализируем конфигурацию, нам нужно только вызвать эту XMLConfiguration для анализа файла конфигурации XML.

package com.github.kongwu.spisamples;

public class LoggerFactory {
	static {
        SuperLoggerConfiguration configuration = new XMLConfiguration();
		configuration.configure(configFile);
    }
    
    public static getLogger(Class clazz){
    	......
    }
}

Это завершает базовую модель, которая выглядит хорошо. Однако расширяемость не очень хорошая, потому что, если я хочу настроить/расширить/переписать функцию парсинга, мне придется переопределить код записи, а LoggerFactory придется переписать, что недостаточно гибко и слишком навязчиво.

Например, если пользователь/пользователь хочет добавить файл yml в качестве файла конфигурации журнала, ему нужно только создать новую конфигурацию YAMLConfiguration для реализации SuperLoggerConfiguration. Но... как его внедрить, как использовать только что созданную YAMLConfiguration в LoggerFactory? Возможно ли, что даже LoggerFactory был переписан?

Если используется механизм SPI, это очень просто, и функция расширения этой записи может быть легко выполнена.

Давайте сначала посмотрим, как использовать механизм SPI JDK для решения вышеуказанной проблемы масштабируемости.

JDK SPI

JDK предоставляет функцию SPI, а основным классом является java.util.ServiceLoader. Его функция заключается в получении нескольких файлов реализации конфигурации в разделе «META-INF/services/» через имя класса.

Чтобы решить вышеупомянутую проблему расширения, теперь мы находимся вMETA-INF/services/создать следующийcom.github.kongwu.spisamples.SuperLoggerConfigurationфайл (без суффикса). В файле только одна строка кода, это наша установка по умолчанию.com.github.kongwu.spisamples.XMLConfiguration(Обратите внимание, что несколько реализаций также могут быть записаны в один файл, разделенные символами возврата каретки)

META-INF/services/com.github.kongwu.spisamples.SuperLoggerConfiguration:

com.github.kongwu.spisamples.XMLConfiguration

Затем получите класс реализации, настроенный нашим механизмом SPI через ServiceLoader:

ServiceLoader<SuperLoggerConfiguration> serviceLoader = ServiceLoader.load(SuperLoggerConfiguration.class);
Iterator<SuperLoggerConfiguration> iterator = serviceLoader.iterator();
SuperLoggerConfiguration configuration;

while(iterator.hasNext()) {
    //加载并初始化实现类
	configuration = iterator.next();
}

//对最后一个configuration类调用configure方法
configuration.configure(configFile);

Наконец, метод инициализации конфигурации в настройке LoggerFactory — текущий метод SPI:

package com.github.kongwu.spisamples;

public class LoggerFactory {
	static {
        ServiceLoader<SuperLoggerConfiguration> serviceLoader = ServiceLoader.load(SuperLoggerConfiguration.class);
        Iterator<SuperLoggerConfiguration> iterator = serviceLoader.iterator();
        SuperLoggerConfiguration configuration;

        while(iterator.hasNext()) {
            configuration = iterator.next();//加载并初始化实现类
        }
        configuration.configure(configFile);
    }
    
    public static getLogger(Class clazz){
    	......
    }
}

Подождите, зачем использовать итератор вместо метода вроде get, который получает только экземпляр?

Только представьте, если это фиксированный метод get, то get получает фиксированный экземпляр, в чем смысл SPI?

Целью SPI является повышение масштабируемости. Извлеките фиксированную конфигурацию и настройте ее с помощью механизма SPI. В этом случае обычно используется конфигурация по умолчанию, а затем через файл SPI настраиваются разные реализации, поэтому возникает проблема нескольких реализаций интерфейса. Если найдено несколько реализаций, какая реализация используется в качестве конечного экземпляра?

Итак, здесь итератор используется для получения всей конфигурации класса реализации. только что в нашемsuper-loggerВ пакет добавлена ​​реализация SuperLoggerConfiguration по умолчанию.

Для поддержки конфигурации YAML теперь добавьте конфигурацию SPI YAMLConfiguration в код потребителя/пользователя:

META-INF/services/com.github.kongwu.spisamples.SuperLoggerConfiguration:

com.github.kongwu.spisamples.ext.YAMLConfiguration

На этом этапе с помощью метода итератора вы получите XMLConfiguration по умолчанию, а YAMLConfiguration мы расширяем два класса реализации конфигурации.

В загруженном выше коде мы проходим итератор до конца, мы** используем последнюю конфигурацию реализации в качестве конечного экземпляра.

Подожди? последний? Как считать последнюю?

Должна ли эта YAMLConfiguration, настроенная потребителем/пользователем, быть последней?

Это не обязательно верно. Это зависит от конфигурации ClassPath в нашей среде выполнения. Банка, загруженная впереди, естественно, находится впереди, а последняя банка, естественно, сзади. такЕсли пользовательский пакет находится позже в порядке ClassPath, чем пакет супер-логгера, он будет в последней позиции; если пользовательский пакет находится впереди, так называемый последний пакет по-прежнему является XMLConfiguration по умолчанию.

Например, если сценарий запуска нашей программы:

java -cp super-logger.jar:a.jar:b.jar:main.jar example.Main

SPI XMLConfiguration по умолчанию настраивается вsuper-logger.jar, расширенный файл конфигурации SPI YAMLConfiguration находится вmain.jar, то последним элементом, полученным итератором, должен быть YAMLConfiguration.

Но что, если порядок путей к классам обратный? main.jar идет первым, super-logger.jar идет после

java -cp main.jar:super-logger.jar:a.jar:b.jar example.Main

Таким образом, последний элемент, полученный итератором, снова становится XMLConfiguration по умолчанию, использовать JDK SPI для нас бессмысленно, а первый полученный — это XMLConfiguration по умолчанию.

Поскольку этот порядок загрузки (путь к классам) задается пользователем, независимо от того, загружаем ли мы первый или последний, возможно, что пользовательская конфигурация не может быть загружена.

Следовательно, это тоже недостаток механизма JDK SPI.Невозможно подтвердить, какая реализация загружена, и невозможно загрузить указанную реализацию.Один только порядок ClassPath является очень неточным методом.

Dubbo SPI

Dubbo загружает все компоненты через механизм SPI. Однако Dubbo не использует встроенный в Java механизм SPI, а совершенствует его для лучшего удовлетворения потребностей. В Dubbo SPI — очень важный модуль. На основе SPI мы можем легко расширить Dubbo. Если вы хотите изучить исходный код Dubbo, вы должны понимать механизм SPI. Далее, давайте сначала разберемся с использованием Java SPI и Dubbo SPI, а затем проанализируем исходный код Dubbo SPI.

В Dubbo реализован новый механизм SPI, более мощный и сложный. Соответствующая логика инкапсулирована в классе ExtensionLoader, через него мы можем загрузить указанный класс реализации. Файлы конфигурации, необходимые для Dubbo SPI, должны быть помещены в путь META-INF/dubbo.Содержание конфигурации выглядит следующим образом (следующая демонстрация взята из официальной документации dubbo).

optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee

В отличие от конфигурации класса реализации Java SPI, Dubbo SPI настраивается парами ключ-значение, поэтому мы можем загружать указанный класс реализации по запросу. Кроме того, при его использовании необходимо отметить аннотацию @SPI на интерфейсе. Давайте продемонстрируем использование Dubbo SPI:

@SPI
public interface Robot {
    void sayHello();
}

public class OptimusPrime implements Robot {
    
    @Override
    public void sayHello() {
        System.out.println("Hello, I am Optimus Prime.");
    }
}

public class Bumblebee implements Robot {

    @Override
    public void sayHello() {
        System.out.println("Hello, I am Bumblebee.");
    }
}


public class DubboSPITest {

    @Test
    public void sayHello() throws Exception {
        ExtensionLoader<Robot> extensionLoader = 
            ExtensionLoader.getExtensionLoader(Robot.class);
        Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
        optimusPrime.sayHello();
        Robot bumblebee = extensionLoader.getExtension("bumblebee");
        bumblebee.sayHello();
    }
}

Самая большая разница между Dubbo SPI и JDK SPI заключается в том, что он поддерживает «псевдоним»., фиксированную точку расширения можно получить через псевдоним точки расширения. Как и в приведенном выше примере, я могу получить реализацию с псевдонимом «optimusPrime» среди нескольких реализаций SPI робота, а также я могу получить реализацию с псевдонимом «шмель», что очень полезно!

С помощью атрибута value аннотации @SPI также может быть задана реализация «псевдоним». Например, в Dubbo по умолчанию используется частный протокол Dubbo:dubbo protocol - dubbo://** Взглянем на интерфейс протокола в Dubbo:

@SPI("dubbo")
public interface Protocol {
	......
}

В интерфейсе протокола добавляется аннотация @SPI, и значением аннотации является Dubbo.Когда реализация получена через SPI, будет получена реализация с псевдонимом dubbo в конфигурации протокола SPI.com.alibaba.dubbo.rpc.ProtocolФайлы следующие:

filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol


dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol


injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
memcached=com.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol
redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol
rest=com.alibaba.dubbo.rpc.protocol.rest.RestProtocol
registry=com.alibaba.dubbo.registry.integration.RegistryProtocol
qos=com.alibaba.dubbo.qos.protocol.QosProtocolWrapper

Затем вам нужно только передать getDefaultExtension, вы можете получить расширение, соответствующее значению в аннотации @SPI.

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension();
//protocol: DubboProtocol

Есть еще адаптивный механизм, хоть и очень гибкий, но... использование не очень "изящное", так что не буду его здесь представлять

В SPI Dubbo также есть «приоритет загрузки», который сначала загружает встроенный (внутренний), затем загружает внешний (внешний) и загружает в порядке приоритета,Пропустить, если встречаются дубликаты, не загружается.

Поэтому, если вы хотите полагаться на порядок загрузки classpath для переопределения встроенных расширений, это также очень неразумный подход, причина та же, что и выше — порядок загрузки не является строгим.

Spring SPI

Файл конфигурации Spring SPI является фиксированным файлом -META-INF/spring.factories, похожий по функциям на JDK, каждый интерфейс может иметь несколько реализаций расширений, что очень просто в использовании:

//获取所有factories文件中配置的LoggingSystemFactory
List<LoggingSystemFactory>> factories = 
    SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader);

Ниже приведена конфигурация spring.factories в Spring Boot.

# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

# ConfigData Location Resolvers
org.springframework.boot.context.config.ConfigDataLocationResolver=\
org.springframework.boot.context.config.ConfigTreeConfigDataLocationResolver,\
org.springframework.boot.context.config.StandardConfigDataLocationResolver

......

В Spring SPI все конфигурации помещаются в фиксированный файл, что избавляет от необходимости настраивать множество файлов. Что касается расширенной настройки нескольких интерфейсов, лучше ли использовать один файл или один файл для каждого, это вопрос мнения (лично нравится Spring, который чист и аккуратен).

Хотя Spring SPI относится к spring-framework (core), в настоящее время он в основном используется в весенней загрузке...

Как и два предыдущих механизма SPI, Spring также поддерживает существование нескольких файлов spring.factories в ClassPath.При загрузке эти файлы spring.factories будут загружены в порядке пути к классам и добавлены в ArrayList. Поскольку нет псевдонима, нет понятия дедупликации, и вы можете добавить столько, сколько хотите.

Однако, поскольку SPI Spring в основном используется в Spring Boot, ClassLoader в Spring Boot будет предпочтительно загружать файлы в проекте, а не полагаться на файлы в пакете. Поэтому, если вы определите файл spring.factories в своем проекте, файл в вашем проекте будет загружен первым, а в полученных фабриках класс реализации, настроенный в spring.factories в проекте, также будет ранжирован первым.

Если мы хотим расширить интерфейс, нам нужно только создать новый в вашем проекте (весенняя загрузка)META-INF/spring.factoriesдокумент,Добавляйте только ту конфигурацию, которую хотите, не копируйте файл spring.factories Spring Boot полностью и не изменяйте его.** Например, я просто хочу добавить новую реализацию LoggingSystemFactory, тогда мне просто нужно создать новуюMETA-INF/spring.factoriesфайл вместо полной копии+изменения:

org.springframework.boot.logging.LoggingSystemFactory=\
com.example.log4j2demo.Log4J2LoggingSystem.Factory

В сравнении

JDK SPI DUBBO SPI Spring SPI
файловый метод Отдельный файл для каждой точки расширения Отдельный файл для каждой точки расширения Все точки расширения в одном файле
получить фиксированную реализацию Не поддерживается, только привести все реализации в порядок Есть понятие "псевдоним", фиксированную реализацию точки расширения можно получить через имя, и очень удобно сотрудничать с аннотацией Dubbo SPI Не поддерживается, только привести все реализации в порядок. Однако, поскольку Spring Boot ClassLoader будет предпочтительно загружать файлы в пользовательском коде, он может гарантировать, что определяемый пользователем файл spring.factoires находится на первом месте, а пользовательское расширение может быть получено путем получения первой фабрики.
разное без Поддерживает внедрение зависимостей внутри Dubbo, различает встроенный SPI Dubbo и внешний SPI через каталоги и сначала загружает внутреннюю часть, чтобы обеспечить наивысший внутренний приоритет. без
Комплектность документа Статьи и сторонняя информация достаточно богаты Документы и сторонняя информация достаточно богаты Документация недостаточно богата, но очень проста в использовании из-за небольшого количества функций.
поддержка IDE без без IDEA отлично поддерживается, с подсказками синтаксиса

Если сравнивать три механизма SPI, то встроенный механизм JDK самый слабый, но поскольку он встроен в JDK, то все равно есть определенные сценарии применения, ведь никаких дополнительных зависимостей не требуется, у Dubbo больше всего функций, но Механизм немного сложен, и его можно использовать только в связке с Dubbo, и нельзя рассматривать как самостоятельный модуль, функции Spring почти такие же, как у JDK, самое большое отличие в том, что все точки расширения написаны на файл spring.factories, что тоже является улучшением, а IDEA прекрасно поддерживает синтаксические подсказки.

Уважаемые наблюдатели, что вы думаете о трех механизмах SPI JDK/Dubbo/Spring, какой из них лучше? Добро пожаловать, чтобы оставить сообщение в области комментариев

Ссылаться на

Оригинальность непростая, несанкционированное воспроизведение запрещено. Если моя статья полезна для вас, пожалуйста, поставьте лайк/добавьте в избранное/подпишитесь, чтобы поддержать и поддержать ее ❤❤❤❤❤❤