文章首发于Hollis公众号
作者 陈彩华
文章转载交流请联系 caison@aliyun.com
В этой статье анализируется механизм Java SPI, предоставляемый JDK и обычно используемый в проектах с открытым исходным кодом, в надежде предоставить справку для всех, кто занимается реальной практикой разработки и изучает проекты с открытым исходным кодом.
1 Что такое СПИ
Полное название SPI — интерфейс поставщика услуг. Это набор API-интерфейсов, предоставляемых Java для реализации или расширения третьими сторонами. Его можно использовать для включения расширений фреймворка и замены компонентов.
Общая схема механизма выглядит следующим образом:
Java SPI на самом деле "Интерфейсное программирование + режим стратегии + конфигурационный файл«Механизм динамического нагружения реализован композицией.
Часто существует множество различных схем реализации для каждой абстракции проектирования системы.В объектно-ориентированном проектировании обычно рекомендуется программирование интерфейсов между модулями, а классы реализации не жестко закодированы между модулями. Как только в код вовлекается конкретный класс реализации, нарушается принцип подключаемости: если нужно заменить реализацию, нужно модифицировать код. Для того, чтобы понять, что его нельзя динамически указать в программе при сборке модуля, нужен механизм обнаружения сервисов. Java SPI предоставляет такой механизм: механизм поиска реализации службы для интерфейса. Это чем-то похоже на идею IOC, которая заключается в том, чтобы вынести управление сборкой за пределы программы.Этот механизм особенно важен в модульном дизайне. Итак, основная идея SPI заключается вразъединение.
2 сценария использования
Вкратце, это относится к:Вызывающий объект включает, расширяет или заменяет стратегию реализации фреймворка в соответствии с фактическими потребностями использования.
Более распространенные примеры:
- Интерфейс загрузки на основе базы данных реализует загрузку классов JDBC загружает драйверы для разных типов баз данных
- Интерфейс бревенчатого фасада реализует загрузку классов SLF4J загружает классы реализации журнала от разных поставщиков
- Весна Spring использует множество SPI, таких как: реализация спецификации servlet3.0 для ServletContainerInitializer, автоматическое преобразование типов Type Conversion SPI (Converter SPI, Formatter SPI) и т. д.
- Даббо Dubbo также использует много SPI для расширения инфраструктуры, но он инкапсулирует собственный SPI, предоставляемый Java, что позволяет пользователям расширять реализацию интерфейса Filter.
3 Введение
Чтобы использовать Java SPI, необходимо соблюдать следующие соглашения:
- 1. После того, как поставщик услуг предоставляет конкретную реализацию интерфейса, в каталоге META-INF/services пакета jar создается файл с именем «полное имя интерфейса», а его содержимое представляет собой полное имя класса реализации. ;
- 2. Пакет jar, в котором находится класс реализации интерфейса, помещается в путь к классам основной программы;
- 3. Основная программа динамически загружает модуль реализации через java.util.ServiceLoder, который находит полное имя класса реализации путем сканирования файлов конфигурации в каталоге META-INF/services и загружает класс в JVM;
- 4. Класс реализации SPI должен содержать конструктор без параметров;
образец кода
шаг 1, определите набор интерфейсов (скажем, org.foo.demo.IShout) и запишите одну или несколько реализаций интерфейса (скажем, org.foo.demo.animal.Dog, org.foo.demo.animal. Кот).
public interface IShout {
void shout();
}
public class Cat implements IShout {
@Override
public void shout() {
System.out.println("miao miao");
}
}
public class Dog implements IShout {
@Override
public void shout() {
System.out.println("wang wang");
}
}
Шаг 2, Создайте каталог /META-INF/services в папке src/main/resources/, добавьте файл с именем интерфейса (файл org.foo.demo.IShout), а его содержимое — класс реализации, который необходимо применить (вот org .foo .demo.animal.Dog и org.foo.demo.animal.Cat, по одному классу в строке).
местоположение файла
- src
-main
-resources
- META-INF
- services
- org.foo.demo.IShout
содержание документа
org.foo.demo.animal.Dog
org.foo.demo.animal.Cat
Шаг 3, Используйте ServiceLoader для загрузки реализации, указанной в файле конфигурации.
public class SPIMain {
public static void main(String[] args) {
ServiceLoader<IShout> shouts = ServiceLoader.load(IShout.class);
for (IShout s : shouts) {
s.shout();
}
}
}
Вывод кода:
wang wang
miao miao
4 Принцип анализа
Сначала посмотрите на переменные-члены класса подписи класса ServiceLoader:
public final class ServiceLoader<S> implements Iterable<S>{
private static final String PREFIX = "META-INF/services/";
// 代表被加载的类或者接口
private final Class<S> service;
// 用于定位,加载和实例化providers的类加载器
private final ClassLoader loader;
// 创建ServiceLoader时采用的访问控制上下文
private final AccessControlContext acc;
// 缓存providers,按实例化的顺序排列
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懒查找迭代器
private LazyIterator lookupIterator;
......
}
Ссылаясь на конкретный исходный код конкретного ServiceLoader, объем кода невелик, а всего строк с комментариями 587. После разбора процесс реализации выглядит следующим образом:
-
1 Приложение вызывает метод ServiceLoader.load В методе ServiceLoader.load сначала создайте новый ServiceLoader и создайте экземпляр переменных-членов в классе, в том числе:
- загрузчик (тип ClassLoader, загрузчик классов)
- acc (тип AccessControlContext, контроллер доступа)
- провайдеры (тип LinkedHashMap
, используемый для кэширования успешно загруженных классов) - lookupIterator (реализующий функцию итератора)
-
2 Приложение получает экземпляр объекта через интерфейс итератора ServiceLoader сначала определяет, есть ли кэшированный объект экземпляра в объекте поставщиков переменных-членов (тип LinkedHashMap
), и напрямую возвращает значение, если есть кэш. Если кеша нет, выполнить загрузку класса, которая реализована следующим образом: -
(1) Прочитайте файл конфигурации в META-INF/services/, чтобы получить имена всех классов, которые могут быть созданы.Стоит отметить, что ServiceLoaderФайлы конфигурации в META-INF можно получить через пакеты jar., код реализации конкретной конфигурации загрузки выглядит следующим образом:
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
- (2) Загрузите объект класса с помощью метода отражения Class.forName() и создайте экземпляр класса с помощью метода instance().
- (3) Кэшировать созданный класс в объекте провайдеров (тип LinkedHashMap
) Затем верните объект экземпляра.
5 Резюме
преимущество: Преимущество использования механизма Java SPI заключается в достижении разделения, так что логика управления сборкой стороннего сервисного модуля отделена от бизнес-кода вызывающей стороны, а не связана вместе. Приложения могут включать расширения фреймворка или заменять его компоненты в соответствии с реальными бизнес-ситуациями.
недостаток:
- Хотя ServiceLoader можно рассматривать как ленивую загрузку, его можно получить, только пройдя их все, то есть все классы реализации интерфейса загружаются и создаются один раз. Если вы не хотите использовать какой-либо класс реализации, он также загружается и создается экземпляр, что расточительно. Способ получения класса реализации недостаточно гибкий, его можно получить только в виде Итератора, а соответствующий класс реализации нельзя получить по определенному параметру.
- Небезопасно использовать экземпляр класса ServiceLoader с несколькими параллельными потоками.
Более интересно, добро пожаловать на официальный аккаунт автора [архитектура распределенной системы]
Ссылаться на
Базовая технология Java 36 лекций
Service Provider Interface: Creating Extensible Java Applications
Использование и разрешение Java ServiceLoader