Серия JDK Часть 4 JavaSPI как я понимаю работает?
1. Вступительные замечания
В этой статье основное внимание уделяется реализации демо на основе Java SPI и анализу принципа его реализации, то есть анализу исходного кода класса ServiceLoader.
На самом деле, причина, по которой я изначально хотел написать эту статью, заключалась в том, что в предыдущем интервью интервьюер задал мне вопрос о Java SPI, но не смог дать на него удовлетворительного ответа, поэтому я подумал о составлении статьи о SPI. , кстати Также закрепите свои знания о механизме родительского делегирования.
2. Краткое описание СПИ
Полное название SPI — интерфейс поставщика услуг, что переводится как интерфейс поставщика услуг. Это встроенный в Java механизм обнаружения сервисов, нужно только добавить реализацию соответствующего интерфейса в переменную окружения, и программа может автоматически загрузить класс и использовать его.
SPI обладает хорошей масштабируемостью.Фреймворк имеет установленные правила (интерфейсы), а конкретные производители предоставляют реализации (implementation interfaces).Если вы хотите переключить схему реализации, вам достаточно поставить реализацию другого производителя в переменную окружения, и вы не нужно.Измените код, видимо, это шаблон стратегии.
Далее давайте реализуем простую демонстрацию на основе SPI.
2. Шаги по реализации SPI в Java
2.1 Определение интерфейса
Прежде всего необходимо определить интерфейс, который является так называемым «стандартом», и производитель реализует его в соответствии с этим стандартным интерфейсом, например, стандартный интерфейс JDBC включает в себя реализацию MySQL и реализацию Oracle. .
В этой демонстрации стандарт интерфейса HelloSpi, который я определил, заключается в реализации метода say, а его основная функция — вывод фрагмента текста на основе метода say.
public interface HelloSpi {
/**
* spi接口的方法
*/
void say();
}
2.2 Создайте класс реализации интерфейса
Создайте два класса реализации HelloInEnglish и HelloInChinese, соответственно выведите строку утверждений о приветствии.
public class HelloInChinese implements HelloSpi {
@Override
public void say() {
System.out.println("from HelloInChinese: 你好");
}
}
public class HelloInEnglish implements HelloSpi {
@Override
public void say() {
System.out.println("from HelloInEnglish: hello");
}
}
Здесь есть заметка,Класс реализации должен иметь конструктор без аргументов., иначе будет сообщено об ошибке, потому что, когда ServiceLoader создаст экземпляр класса реализации, он будет реализован через конструктор без аргументов, а конкретный код будет проанализирован позже.
2.3 Создать метафайл конфигурации полного имени интерфейса
Следующим шагом является создание ресурса в каталоге ресурсов.META-INF/servicesпапку, затем создайте файл с именемПолное имя интерфейса HelloSpi, то есть org.walker.planes.spi.HelloSpi в моем проекте.
Содержимое файла представляет собой полные имена двух только что созданных классов реализации, и каждая строка в файле представляет класс реализации.
org.walker.planes.spi.HelloInEnglish
org.walker.planes.spi.HelloInChinese
2.4 Используйте ServiceLoader для загрузки классов в файл конфигурации
Создайте тестовый класс с основным методом, вызовите метод ServiceLoader#load(Class) для загрузки соответствующего класса и выполните его.
public class SpiMain {
public static void main(String[] args) {
// 加载 HelloSpi 接口的实现类
ServiceLoader<HelloSpi> shouts = ServiceLoader.load(HelloSpi.class);
// 执行say方法
for (HelloSpi s : shouts) {
s.say();
}
}
}
执行结果为:
from HelloInEnglish: hello
from HelloInChinese: 你好
До сих пор была реализована простая демонстрация на основе механизма Java SPI.
На данный момент простая демонстрация на основе механизма Java SPI завершена. В этом примере мы можем знать, что в дополнение к добавлению файла конфигурации более важным классом является ServiceLoader, который загружается в класс реализации интерфейса путем вызова метода ServiceLoader#load(Class) и запускает метод say.
Видно, что JavaSPI с высокой вероятностью основан на работе класса ServiceLoader. Разберем принцип реализации класса ServiceLoader интерфейса загрузки.
3. Анализ исходного кода ServiceLoader
Благодаря приведенной выше демонстрации реализации Java SPI, я думаю, вы уже знаете его рабочий процесс Давайте поговорим о том, как класс ServiceLoader находит и загружает класс реализации в соответствии с классом интерфейса.
3.1 ServiceLoader#load(Class)
Во-первых, давайте рассмотрим шаг 4 в приведенном выше примере, загрузим соответствующий класс, вызвав метод ServiceLoader#load(Class), и взглянем на исходный код метода загрузки.
// 示例 main 方法中的 load 方法
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 调用重载方法,通过 cl 线程上下文类加载器加载 service 目标类
return ServiceLoader.load(service, cl);
}
// load 重载方法
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}
Как описано в блокировке комментариев в приведенном выше коде, при вызове метода ServiceLoader#load(Class) сначала получается загрузчик класса контекста потока, а затем через загрузчик класса контекста потока загружается целевой класс.
Сказав это, я должен упомянуть механизм родительского делегирования JVM и загрузчик класса контекста потока (Thread Context ClassLoader).
3.2 Механизм родительского делегирования JVM
Я полагаю, что все знают механизм родительского делегирования JVM, то есть когда загрузчик класса получает запрос на загрузку класса, он не будет пытаться загрузить класс сам по себе, а делегирует запрос родительскому загрузчику для завершения, только когда родительский класс Когда загрузчик сообщает, что не может выполнить запрос на загрузку, вспомогательный загрузчик попытается загрузить класс самостоятельно.
Как правило, вызываемые нами загрузчики классов включают Bootstrap ClassLoader, Extension ClassLoader, AppClassLoader и Custom ClassLoader.
Что вам нужно понять в этой статье, так это то, что загрузчик классов запуска отвечает за загрузку основных классов Java, в том числе:
- Классы расположены в JAVA_HOME\lib
- по пути, указанному параметром -Xbootclasspath
- Библиотеки классов, распознаваемые виртуальной машиной, такие как rt.jar, tools.jar
Существует также загрузчик классов приложения, который отвечает за загрузку библиотеки классов, указанной в пути к классам пользователя.
До сих пор мы видели механизм родительского делегирования и загрузчик классов запуска, давайте вернемся к SPI. Как упоминалось выше, так называемый SPI — это некоторые стандартные интерфейсы, предоставляемые JDK, которые реализуются производителем.Когда реализация существует в переменной среды, класс может быть загружен автоматически, чтобы можно было использовать реализацию производителя.
Стоит отметить, что стандартный интерфейс SPI, предоставляемый JDK, обычно объединяется с базовой кодовой базой, например, интерфейс Driver.class драйвера JDBC находится в пакете rt.jar. Иными словами, интерфейс SPI загружается загрузчиком класса запуска. Если основываться на традиционном механизме родительского делегирования, класс реализации производителя фактически загружается загрузчиком класса запуска. В это время будет обнаружено, что реализация производителя class находится в пути к классам, он должен быть загружен загрузчиком классов приложения, а не загрузчиком классов запуска.
На основе такой дилеммы возникла загрузка класса контекста потока, которая также является методом, используемым для нарушения механизма родительского делегирования.
3.3 Загрузчик класса контекста потока
Загрузчик класса контекста потока — это атрибут класса Thread, который используется для кэширования загрузчика класса текущего потока.
В методе ServiceLoader#load(Class) сначала получается загрузчик класса контекста потока текущего потока.В примере кода поток, выполняющий этот метод, является основным потоком, а загрузчик класса, загружающий основной поток, является приложением. загрузчик классов.
Загрузчик классов приложения отвечает за загрузку библиотеки классов, указанной в пути к классам.Конечно, текущий проект принадлежит к пути к классам, поэтому использование загрузчика классов приложения для загрузки класса реализации интерфейса SPI может быть успешно загружено.
Далее мы продолжим анализ процесса загрузки класса ServiceLoader класса реализации интерфейса SPI на основе загрузки класса контекста потока.
3.4 Отслеживание источника
В перегруженном методе загрузки фактически создается экземпляр класса ServiceLoader, а входящими параметрами являются целевой интерфейс HelloSpi.class и загрузчик класса AppClassLoader.
В конструкторе этого класса ServiceLoader также выполняются некоторые операции, подробности см. в комментариях к следующему коду.
private ServiceLoader(Class<S> svc, ClassLoader cl) {
// 检查目标接口类是否为空,若为空,抛出 NullPointerException
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// 若入参 cl 为空,默认使用系统类加载器(应用类加载器)
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
// Java 安全管理相关,本文不详细说明
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
// 清空缓存提供者 providers ,重新加载所有 SPI 接口实现类
reload();
}
Метод ServiceLoader#reload() фактически очищает поставщики закрытых переменных-членов класса ServiceLoader и создает объект LazyIterator с ленивым итератором.Входящие параметры — это целевой класс интерфейса и загрузчик класса.
// Cached providers, in instantiation order
// 缓存提供者,实际上是存储 SPI 接口实现类对象,key 为实现类类名,value 为 SPI 接口实现类实例
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
// 当前懒加载迭代器,开始迭代时创建对象,然后放入 providers 中
private LazyIterator lookupIterator;
public void reload() {
// 清空 providers
providers.clear();
// 创建懒加载迭代器
lookupIterator = new LazyIterator(service, loader);
}
До сих пор класс ServiceLoader был подготовлен для загрузки класса реализации интерфейса SPI.Когда программа проходит через объект ServiceLoader в цикле for, она фактически вызывает метод hasNext интерфейса Iterator и, наконец, вызывает LazyIterator, упомянутый в предыдущем шаге. , Метод #hasNext(), если он возвращает значение true, вызывает следующий метод, чтобы начать итерацию.
public boolean hasNext() {
// Java 访问控制上下文为null时,调用 hasNextService 方法,这里就是调用了 hasNextService 方法
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
В общем случае суждение acc == null будет верным, и тогда будет вызван метод LazyIterator#hasNextService(), который фактически анализирует файлы в каталоге META-INF/services, генерирует итератор класса реализации интерфейса SPI, и устанавливает свойства nextName., см. комментарии в приведенном ниже коде для конкретной логики.
// Github:https://github.com/Planeswalker23
private boolean hasNextService() {
// 第一次进入此方法时 nextName == null
if (nextName != null) {
return true;
}
// configs 属性表示的是 URL 类型的 Enumeration 对象,第一次进入此方法时为null
if (configs == null) {
try {
// PREFIX = "META-INF/services/",这也解释了为什么要在 META-INF/services/ 目录下建立接口全路径名的文件
// service 属性就是创建懒加载迭代器 LazyIterator 对象时传入的 service 对象,即 SPI 接口类 HelloSpi.class
// 所以 fullName 变量就是 META-INF/services/HelloSpi ,代表了在该目录下创建的文件名
String fullName = PREFIX + service.getName();
// 若类加载器 loader 为 null,会通过系统类加载器加载配置文件
// 若不为 null,则通过该类加载器加载配置文件
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
// 第一次进入此方法时,pending 迭代器为 null
while ((pending == null) || !pending.hasNext()) {
// 若配置文件中没有数据,返回 false
if (!configs.hasMoreElements()) {
return false;
}
// 将配置文件中的全路径类名转换为迭代器的方式存储到 pending 属性中,每行代表 pending 属性中的一个元素
pending = parse(service, configs.nextElement());
}
// 将 nextName 属性设置为 pending 迭代器的下一个元素
nextName = pending.next();
return true;
}
После выполнения метода hasNextService завершен синтаксический анализ файла конфигурации SPI, сгенерирован итератор класса реализации стандартного интерфейса SPI, завершено присвоение имени следующего класса реализации nextName и, наконец, возвращено значение true, что указывает на что итератор имеет следующее значение. Затем будет вызван следующий метод итератора, и, наконец, будет вызван метод LazyIterator#next(). Этот метод фактически вызывает метод LazyIterator#nextService(). В этом методе класс загрузка и создание экземпляра класса завершены. Содержание см. в комментариях к коду ниже.
// Github:https://github.com/Planeswalker23
private S nextService() {
// 不是第一次调用 hasNextService 方法,作用是将 pending.next() 赋值给 nextName
if (!hasNextService())
throw new NoSuchElementException();
// 标记了当前要进行加载和实例化的类名
String cn = nextName;
// 然后将 nextName 属性置为 null
nextName = null;
Class<?> c = null;
try {
// 通过类名 cn 、false 属性(代表不进行初始化)、类加载器 loader(创建 LazyIterator 时传入的线程上下文类加载器,即应用类加载器)这三个参数调用 Class#forName 方法进行类的加载工作
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service, "Provider " + cn + " not a subtype");
}
try {
// 将加载成功的类进行实例化,然后强转为 HelloSpi 类型
S p = service.cast(c.newInstance());
// 将实例化后的对象放到 providers 属性中
providers.put(cn, p);
// 返回实例化的对象,在 for 循环中通过调用 say 方法执行实现类的逻辑
return p;
} catch (Throwable x) {
fail(service, "Provider " + cn + " could not be instantiated", x);
}
throw new Error(); // This cannot happen
}
На данный момент завершен анализ исходного кода всего процесса класса ServiceLoader, реализующего механизм Java SPI на основе загрузчика класса контекста потока.
3.5 Зачем классу реализации интерфейса SPI нужен конструктор без параметров
В разделе 2.2 Создание класса реализации интерфейса я упомянул предложение:Класс реализации должен иметь конструктор без аргументов., а затем проанализируйте причины упоминания этого предложения.
На самом деле это очень просто: при создании экземпляра загруженного класса в методе LazyIterator#nextService() он создается через метод Class#newInstance(), исходный код этого метода выглядит следующим образом.
@CallerSensitive
public T newInstance() {
// 省略大部分代码...
Class<?>[] empty = {};
// 调用 getConstructor0 获取无参构造函数,若没获取到会报 NoSuchMethodException
final Constructor<T> c = getConstructor0(empty, Member.DECLARED);
// 省略大部分代码...
}
// 获取无参构造函数 Class#getConstructor0
private Constructor<T> getConstructor0(Class<?>[] parameterTypes, int which) throws NoSuchMethodException {
// 获取该类的所有构造函数,进行遍历
Constructor<T>[] constructors = privateGetDeclaredConstructors((which == Member.PUBLIC));
for (Constructor<T> constructor : constructors) {
// 返回无参构造函数
if (arrayContentsEq(parameterTypes, constructor.getParameterTypes())) {
return getReflectionFactory().copyConstructor(constructor);
}
}
throw new NoSuchMethodException(getName() + ".<init>" + argumentTypesToString(parameterTypes));
}
Как показано в приведенном выше коде, при создании экземпляра класса конструктор получается путем вызова метода Class#getConstructor0, а этот метод получает конструктор без аргументов, которыйКласс реализации должен иметь конструктор без аргументов.причина.
4. Резюме
В этой статье реализуется простая демонстрация на основе механизма Java SPI, а затем анализируется исходный код класса ServiceLoader для реализации Java SPI на основе загрузчика класса контекста потока.
Часть о Java SPI кратко изложена здесь.Если у вас есть какие-либо другие дополнения, я надеюсь, что вы можете рассказать мне и учиться вместе.
Надеюсь, что смогу помочь всем.
5. Ссылки
- Продвинутые разработчики должны понимать механизм SPI в Java.
- Глубокое понимание виртуальной машины Java
Наконец, эта статья включена в личную базу знаний Юке:Backend технология, как я ее понимаю,Добро пожаловать.