Полное название SPI — Service Provider Interface, что дословно переводится как «Интерфейс поставщика услуг», что звучит неуклюже, поэтому я пытаюсь перевести его как «Интерфейс поставщика услуг».
Все мы знаем, что интерфейс может иметь множество реализаций. Например, поиск можно использовать для поиска на жестком диске системы или для поиска в базе данных. Чтобы уменьшить связь, разработчик системы не хочет чтобы жестко запрограммировать конкретный метод поиска. , но надеюсь, что поставщик услуг может выбрать, какой метод поиска использовать, в настоящее время вы можете использовать механизм SPI.
Механизм SPI широко используется в различных средах с открытым исходным кодом, таких как:
- ExtensionLoader в dubbo, с которым все знакомы, может добавлять некоторые настраиваемые подключаемые функции через эти точки расширения, такие как добавление фильтра для реализации доступа к белому списку, реализация ограничения тока интерфейса и другие функции; или вы можете напрямую заменить его собственный протокол, транспорт и т.д.
- При разработке плагина idea intellij необходимо определить файл /META-INF/plugin.xml. В этом plugin.xml есть много мест для настройки serviceInterface и serviceImplementation, который также является механизмом SPI. Это позволяет разработчикам подключаемых модулей не только использовать API, предоставляемый его базовым SDK, но также позволяет разработчикам настраивать функции, а связь довольно низка.При разработке подключаемого модуля intellij он напрямую использует ServiceLoader в JDK
- Механизм SPI также широко используется весной, и в этой статье будет разобрана его часть.
SPI в JDK
Оценки SPI, понятные каждому, давайте возьмем очень простой пример, чтобы взглянуть на механизм SPI в Java.
- Определение интерфейса поиска Поиск
package com.north.spilat.service;
import java.util.List;
public interface Search {
List<String> search(String keyword);
}
- Реализовать интерфейс для запроса из базы данных
package com.north.spilat.service.impl;
import com.north.spilat.service.Search;
import java.util.List;
/**
* @author lhh
*/
public class DatabaseSearch implements Search {
@Override
public List<String> search(String keyword) {
System.out.println("now use database search. keyword:" + keyword);
return null;
}
}
- Реализовать интерфейс для запроса из файловой системы
package com.north.spilat.service.impl;
import com.north.spilat.service.Search;
import java.util.List;
/**
* @author lhh
*/
public class FileSearch implements Search {
@Override
public List<String> search(String keyword) {
System.out.println("now use file system search. keyword:" + keyword);
return null;
}
}
-
Создайте каталог META-INF\services\com.north.spilat.service.Search в src\main\resources, а затем создайте два файла в каталоге com.north.spilat.service.Search с конкретным классом реализации вышеуказанного интерфейса. Полное имя — это имя файла, т. е.:
com.north.spilat.service.impl.DatabaseSearch
com.north.spilat.service.impl.FileSearch
Весь каталог проекта выглядит следующим образом:
-
Создайте новый основной метод для тестирования
package com.north.spilat.main;
import com.north.spilat.service.Search;
import java.util.Iterator;
import java.util.ServiceLoader;
public class Main {
public static void main(String[] args) {
System.out.println("Hello World!");
ServiceLoader<Search> s = ServiceLoader.load(Search.class);
Iterator<Search> searchList = s.iterator();
while (searchList.hasNext()) {
Search curSearch = searchList.next();
curSearch.search("test");
}
}
}
Бегом, вывод такой:
Hello World!
now use database search. keyword:test
now use file system search. keyword:test
Как видите, механизм SPI определил структуру процесса для загрузки сервисов, вам просто нужно следовать соглашению, в каталоге META-INF/services создать папку с полным именем интерфейса (com.north. spilat .service.Search), поместите в папку полное имя конкретного класса реализации (com.north.spilat.service.impl.DatabaseSearch), и система сможет загружать различные классы реализации в соответствии с этими файлами. Общий процесс SPI.
Анализ класса ServiceLoader
Вернемся к основному методу выше, там нет ничего особенного, кроме одного предложения
ServiceLoader.load(Search.class);
ServiceLoader.class — это класс инструмента, который загружает определенные классы реализации в соответствии с именами файлов в META-INF/services/xxxInterfaceName.
Из load(Search.class) давайте взглянем на этот класс.Следующее в основном предназначено для вставки кода, а анализ находится в комментариях к коду.
- Видно, что логики в нем не так много, а основная логика отдана в LazyIterator
/*
*入口, 获取一下当前类的类加载器,然后调用下一个静态方法
*/
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
/*
*这个也没有什么逻辑,直接调用构造方法
*/
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
/**
* 也没有什么逻辑,直接调用reload
*/
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
/**
* 直接实例化一个懒加载的迭代器
*/
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
- Итератор Lazyiterator должен только заботиться о HASNEXT () и Next (), и hasnext () просто звонит hasnextservice (). Излишне говорить, следующий () должен также просто позвонить Nextservice ();
private boolean hasNextService() {
if (nextName != null) {
// nextName不为空,说明加载过了,而且服务不为空
return true;
}
// configs就是所有名字为PREFIX + service.getName()的资源
if (configs == null) {
try {
// PREFIX是 /META-INF/services
// service.getName() 是接口的全限定名称
String fullName = PREFIX + service.getName();
// loader == null, 说明是bootstrap类加载器
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 通过名字加载所有文件资源
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
//遍历所有的资源,pending用于存放加载到的实现类
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
//遍历完所有的文件了,直接返回
return false;
}
// parse方法主要调用了parseLine,功能:
// 1. 分析每个PREFIX + service.getName() 目录下面的所有文件
// 2. 判断每个文件是否是合法的java类的全限定名称,如果是就add到pending变量中
pending = parse(service, configs.nextElement());
}
// 除了第一次进来,后面每次调用都是直接到这一步了
nextName = pending.next();
return true;
}
- Посмотрим, что делает nextService
private S nextService() {
// 校验一下
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 尝试一下是否能加载该类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,"Provider " + cn + " not found");
}
// 是不是service的子类,或者同一个类
if (!service.isAssignableFrom(c)) {
fail(service,"Provider " + cn + " not a subtype");
}
try {
// 实例化这个类, 然后向上转一下
S p = service.cast(c.newInstance());
// 缓存起来,避免重复加载
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,"Provider " + cn + " could not be instantiated",x);
}
throw new Error(); // This cannot happen
}
Как видно из приведенного выше кода, так называемая отложенная загрузка заключается в ожидании вызова hasNext() для поиска службы, а затем вызове next() для создания экземпляра класса службы.
SPI JDK, вероятно, является такой логикой.Поставщик услуг помещает конкретное имя класса реализации в /META-INF/services/xxx в соответствии с соглашением, а ServiceLoader может загружать различные реализации в соответствии с пожеланиями поставщика услуг. логика жесткого кодирования для достижения цели развязки.
Конечно, из приведенного выше простого примера вы можете не увидеть, как SPI достигает эффекта развязки, поэтому давайте посмотрим, как платформа с открытым исходным кодом использует механизм SPI для развязки.Используйте SPI Charm.
SPI в спрингбуте
Как программист, если вам нечего делать, вы можете изучить больше фреймворков с открытым исходным кодом, потому что эти открытые исходники не знают, сколько раз их ломают каждый день, поэтому их коды очень хороши от дизайна до реализации, и мы можем многому у них учусь..
На протяжении многих лет можно сказать, что фреймворк Spring является лидером в мире открытого исходного кода, и никто об этом не знает.Дизайн его исходного кода также славится своей элегантностью, сверхвысокой масштабируемостью и сверхнизкой связанностью. .
Вот как это отделяет его?Точечный механизм расширения является одним из волшебных орудий
от волшебного стартера
Когда я впервые столкнулся со springboot, я действительно почувствовал, что всякие spring-xx-starter и xx-spring-starter очень волшебны. Почему сложный плагин можно внедрить, добавив зависимость к файлу pom? этим вопросом я начал свой путь в науку.
В Китае много компаний используют фреймворк dubbo, поэтому здесь мы возьмем в качестве примера dubbo-spring-boot-starter, чтобы увидеть, насколько эффективна развязка в springboot.
Напомним, если мы хотим внедрить модуль dubbo в проект springboot, что нужно сделать.
- Внесите зависимость dubbo-spring-boot-starter в файл pom.
<dependency>
<groupId>com.alibaba.spring.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
- Настройте параметры, связанные с dubbo в файле Application.properties
spring.dubbo.server=true
spring.dubbo.application.name=north-spilat-server
#
spring.dubbo.registry.id=defaultRegistry
#
spring.dubbo.registry.address=127.0.0.1
#
spring.dubbo.registry.port=2181
#
spring.dubbo.registry.protocol=zookeeper
#
spring.dubbo.protocol.name=dubbo
#
spring.dubbo.protocol.port=20881
#
spring.dubbo.module.name=north-spilat-server
#
spring.dubbo.consumer.check=false
#
spring.dubbo.provider.timeout=3000
#
spring.dubbo.consumer.retries=0
#
spring.dubbo.consumer.timeout=3000
- Добавьте соответствующую аннотацию в класс запуска пружинного загрузки
package com.north.spilat.main;
import com.alibaba.dubbo.spring.boot.annotation.EnableDubboConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
/**
* @author lhh
*/
@SpringBootApplication
@ComponentScan(basePackages = {"com.north.*"})
@EnableDubboConfiguration
public class SpringBootMain {
public static void main(String[] args) {
SpringApplication.run(SpringBootMain.class, args);
}
}
- Определить интерфейс, реализовать и вызвать
интерфейс
package com.north.spilat.service;
/**
* @author lhh
*/
public interface DubboDemoService {
String test(String params);
}
реализовать интерфейс
package com.north.spilat.service.impl;
import com.alibaba.dubbo.config.annotation.Service;
import com.north.spilat.service.DubboDemoService;
import org.springframework.stereotype.Repository;
/**
* @author lhh
*/
@Service
@Repository("dubboDemoService")
public class DubboDemoServiceImpl implements DubboDemoService {
@Override
public String test(String params) {
return System.currentTimeMillis() + "-" + params ;
}
}
Напишите контроллер для вызова интерфейса dubbo
package com.north.spilat.controller;
import com.north.spilat.service.DubboDemoService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @author lhh
*/
@RestController
public class HelloWorldController {
@Resource
private DubboDemoService dubboDemoService;
@RequestMapping("/saveTheWorld")
public String index(String name) {
return dubboDemoService.test(name);
}
}
После выполнения вышеуказанных 4 шагов (самостоятельная установка среды, такой как zookeeper), запустите класс SpringBootMain, проект springboot с модулем dubbo настроен так, это действительно так просто.
Тем не менее, в мире нет такого понятия, как спокойное время, это просто кто-то, кто несет за вас груз, и этот человек, который несет за вас груз, - «даббо-спринг-бут-стартер».
Тайна dubbo-spring-boot-starter
На картинке выше показана структура пакета dubbo-spring-boot-starter.jar. Содержимого много, но вы, должно быть, подумали об этом. Поскольку в предыдущем разделе мы упоминали, что SPI тесно связан с META-INF , то наш Фестиваль должен быть таким же.Поэтому давайте сначала посмотрим, что находится в каталоге META-INF.
dubbo/com.alibaba.dubbo.rpc.InvokerListener
dubbosubscribe=com.alibaba.dubbo.spring.boot.listener.ConsumerSubscribeListener
Файл в этом каталоге имеет только одну строку, и он выглядит как SPI jdk выше Да, это действительно точка расширения, соглашение о точках расширения в dubbo, то есть ExtensionLoader, о котором мы упоминали в начале.
- spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.dubbo.spring.boot.DubboAutoConfiguration,\
com.alibaba.dubbo.spring.boot.DubboProviderAutoConfiguration,\
com.alibaba.dubbo.spring.boot.DubboConsumerAutoConfiguration
org.springframework.context.ApplicationListener=\
com.alibaba.dubbo.spring.boot.context.event.DubboBannerApplicationListener
Вау вау вау, файл назван в честь весны, а содержимое файла также включает в себя так много классов весны После подтверждения глаз я встретил правильный... файл. Но не волнуйтесь, ниже также есть файл spring.providers
- spring.providers
provides: dubbo-spring-boot-starter
Spring.providers — такое простое предложение, что немного разочаровывает, так что давайте обратим внимание на spring.factories.
Ищите Spring.Factory в IDEA.Не спрашивайте,не знаю,поиск пугает.Оказывается такой файл будет в принципе в каждом JAR пакете связанном с springboot.Прежде чем проводить эксперименты, физики всегда любят рассуждать и получать предсказанный вывод, а затем использовать экспериментальные результаты для подтверждения или опровержения предсказанного вывода.
Поэтому, исходя из механизма SPI в JDK, здесь тоже можно сделать смелый прогноз: в фреймворке spring должен быть класс, аналогичный ServiceLoader, специально загружающий определенные интерфейсы из конфигурации в META-INF/spring.factories.
Излишне говорить, что это предсказание должно быть точным, иначе я бы написал столько слов напрасно.Но как доказать, что наше предсказание является точным.Проведем также «эксперимент».
Процесс запуска Springboot
Чтобы разобраться в процессе запуска Springboot, лучше всего изучить его исходный код.
Кодекс Sprilboot по-прежнему очень «гуманизированный». Спрингбат ясно говорит вам, что его запись является основным методом. Следовательно, довольно комфортно читать код SpringBoot.,
На приведенном выше рисунке показан процесс запуска проекта Springboot. Во-первых, есть два последовательных перегруженных метода статического запуска. Внутри метода статического запуска будет вызываться конструктор для создания экземпляра объекта SpringApplication, а конструктор будет вызывать initialiaze() для инициализации. , создайте экземпляр, затем вызовите метод run() для официального запуска.
Видно, что основная логика всего процесса запуска находится внутри метода initialiaze и метода запуска участника.
Взгляните на логику initialiaze(), далее тоже старое правило, основной код выложен, анализ в комментариях к коду
@SuppressWarnings({ "unchecked", "rawtypes" })
private void initialize(Object[] sources) {
// sources一般是Configuration类或main方法所在类
// 可以有多个
if (sources != null && sources.length > 0) {
this.sources.addAll(Arrays.asList(sources));
}
// 判断是否是web环境
// classLoader能加载到
// "javax.servlet.Servlet",
// "org.springframework.web.context.ConfigurableWebApplicationContext"
// 这两个类就是web环境
this.webEnvironment = deduceWebEnvironment();
// 加载initializers 和listeners
// getSpringFactoriesInstances顾名思义,
// 就是加载某个接口的工厂实例,
// 看起来像是我们要找的"ServiceLoader"了
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 找到main方法所在的类
this.mainApplicationClass = deduceMainApplicationClass();
}
К счастью, всплыл "подозрительный" getSpringFactoriesInstances, давайте посмотрим на его логику
/**
* 参数type就是要加载的接口的class
*/
private <T> Collection<? extends T>
getSpringFactoriesInstances(Class<T> type) {
// 直接调用重载方法getSpringFactoriesInstances
return getSpringFactoriesInstances(type, new Class<?>[] {});
}
private <T> Collection<? extends T>
getSpringFactoriesInstances(Class<T> type,
Class<?>[] parameterTypes,
Object... args) {
// 获取当前线程的classLoader
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// Use names and ensure unique to protect against duplicates
// 翻译一下原文注释就是用names来去重
// 注意这里, 我们寻找的"ServiceLoader"终于出现了
// 就是SpringFactoriesLoader
Set<String> names = new LinkedHashSet<String>(
SpringFactoriesLoader.loadFactoryNames(type, classLoader));
// 是用java反射来实例化
List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
classLoader, args, names);
// 根据@Order注解来排一下序
AnnotationAwareOrderComparator.sort(instances);
// 返回这个接口的所有实现实例
return instances;
}
Затем мы быстро нашли искомый SpringFactoriesLoader, и этот класс очень маленький, с меньшим количеством кода, чем ServiceLoader JDK.Давайте подробнее рассмотрим, что в нем есть.
- FACTORIES_RESOURCE_LOCATION указывает на META-INF/spring.factories, о которых мы упоминали выше.
- loadFactory, найдите указанный класс реализации интерфейса из META-INF/spring.factories и создайте его экземпляр, где поиск выполняется путем вызова loadFactoryNames
- loadFactoryNames находит полное имя реализующего класса определенного интерфейса из указанного местоположения.
- instanceiateFactory инстанцирование
Этот класс является «ServiceLoader» в Springboot, который предоставляет поставщикам услуг механизм для указания реализации определенного интерфейса (которого может быть несколько), например интерфейсы ApplicationContextInitializer.class и ApplicationListener.class выше, если мы хотим указать наша реализация в нашем модуле, или если вы хотите добавить одну из наших реализаций в существующий код, вы можете указать это в /META-INF/spring.factories.Я напишу конкретный пример ниже, для лучшего понимания.
/**
* 省略import
**/
public abstract class SpringFactoriesLoader {
private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);
/**
* The location to look for factories.
* 查找工厂实现类的位置
* <p>Can be present in multiple JAR files.
* 可以在多个jar包中
* 这不就是我们一直在寻找的META-INF/spring.factories嘛
* 终于找到了
*/
public static final String FACTORIES_RESOURCE_LOCATION =
"META-INF/spring.factories";
/**
* 查找并实例化指定的工厂类实现
*/
public static <T> List<T> loadFactories(Class<T>
factoryClass, ClassLoader classLoader) {
Assert.notNull(factoryClass, "'factoryClass'
must not be null");
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse =
SpringFactoriesLoader.class.getClassLoader();
}
// 最终是调用loadFactoryNames
List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);
if (logger.isTraceEnabled()) {
logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames);
}
List<T> result = new ArrayList<T>(factoryNames.size());
for (String factoryName : factoryNames) {
// 一个个的实例化
result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));
}
// 排序
AnnotationAwareOrderComparator.sort(result);
return result;
}
/**
* 从META-INF/spring.factories查找指定接口的实现类的
* 全限定类名称
*/
public static List<String> loadFactoryNames(
Class<?> factoryClass, ClassLoader classLoader) {
// 接口的类名称
String factoryClassName = factoryClass.getName();
try {
//加载所有的META-INF/spring.factories文件资源
Enumeration<URL> urls =
(classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
List<String> result = new ArrayList<String>();
while (urls.hasMoreElements()) {
// 一个url代表一个spring.factories文件
URL url = urls.nextElement();
// 加载所有的属性, 一般是 xxx接口=impl1,impl2 这种形式的
Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
// 根据接口名获取的类似"impl1,impl2"的字符串
String factoryClassNames = properties.getProperty(factoryClassName)
// 以逗号分隔,转化成列表
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
}
// 返回实现类名的列表
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() +
"] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
/**
* 根据类名的全限定名称实例化
*/
@SuppressWarnings("unchecked")
private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) {
try {
// 查找类
Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader);
// 校验是不是该接口类或该接口类的实现类
if (!factoryClass.isAssignableFrom(instanceClass)) {
throw new IllegalArgumentException(
"Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]");
}
Constructor<?> constructor = instanceClass.getDeclaredConstructor();
ReflectionUtils.makeAccessible(constructor);
// 反射实例化
return (T) constructor.newInstance();
}
catch (Throwable ex) {
throw new IllegalArgumentException("Unable to instantiate factory class: " + factoryClass.getName(), ex);
}
}
}
После прочтения класса SpringFactoriesLoader логика метода initialize() завершена, теперь рассмотрим еще один важный метод run(String... args)
/**
* Run the Spring application, creating and refreshing a new
* {@link ApplicationContext}.
* @param args the application arguments (usually passed from a Java main method)
* @return a running {@link ApplicationContext}
*/
public ConfigurableApplicationContext run(String... args) {
// 用于监测启动时长等等
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// springboot的上下文
ConfigurableApplicationContext context = null;
FailureAnalyzers analyzers = null;
// 配置headless模式
configureHeadlessProperty();
// 启动监听器, 可以配置到spring.factories中去
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
// 封装参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
// 配置environment
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
// 打印banner
Banner printedBanner = printBanner(environment);
// 创建上下文
context = createApplicationContext();
analyzers = new FailureAnalyzers(context);
// 先初始化上下文
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
// spring 经典的refresh()过程, 大部分的逻辑都在里面
// 这里不再深入, 读者可以自行研读代码或搜索引擎
refreshContext(context);
afterRefresh(context, applicationArguments);
listeners.finished(context, null);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
return context;
}
catch (Throwable ex) {
handleRunFailure(context, listeners, analyzers, ex);
throw new IllegalStateException(ex);
}
}
Этот метод является основной логикой запуска springboot.В нем очень много содержания.Если вы хотите, чтобы все было понятно, боюсь, что вы не сможете написать статью несколько раз.Весь фреймворк,люди не Я не буду идти дальше. Для этой статьи, если вы знаете, что метод run() является основной логикой запуска, вы также можете помнить
context = createApplicationContext();
refreshContext(context);
Эти две строки кода мы увидим позже.
Принцип dubbo-spring-boot-starter
Выше было сказано много, но почему springboot вводит зависимость starter для внедрения сложного модуля Вот исследование через dubbo-spring-boot-starter.
Давайте взглянем на spring.factories в dubbo-spring-boot-starter, и вы обнаружите, что в нем настроено два интерфейса: один EnableAutoConfiguration, а другой ApplicationListener.
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.dubbo.spring.boot.DubboAutoConfiguration,\
com.alibaba.dubbo.spring.boot.DubboProviderAutoConfiguration,\
com.alibaba.dubbo.spring.boot.DubboConsumerAutoConfiguration
org.springframework.context.ApplicationListener=\
com.alibaba.dubbo.spring.boot.context.event.DubboBannerApplicationListener
Слушатель знает, глядя на имя, которое используется для печати баннера при его запуске, поэтому я пока не буду рассматривать его здесь.Давайте посмотрим, где используется EnableAutoConfiguration.
Начиная с основного метода и заканчивая отладкой, я наконец нашел строку кода в классе AutoConfigurationImportSelector:
SpringFactoriesLoader.loadFactoryNames(
getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader())
Где getSpringFactoriesLoaderFactoryClass() записывается мертвым и возвращает EnableAutoConfiguration.class
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
/**
* Return the class used by {@link SpringFactoriesLoader} to load configuration
* candidates.
* @return the factory class
*/
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
Как вы можете видеть на рисунке ниже, будет много реализаций EnableAutoConfiguration.class, если вы настроите его в spring.fatories, он будет загружен для вас.
После загрузки, что вы делаете?Посмотрев вниз, вы можете обнаружить, что примерный процесс выглядит так:- this.reader.loadBeanDefinitions(configClasses); configClasses — это все классы реализации, прочитайте эти классы и подготовьтесь к синтаксическому анализу
- registerBeanDefinition для регистрации в beanDefinitionNames
- В операции spring refresh() последним шагом является finishBeanFactoryInitialization(beanFactory), который инициализирует все одноэлементные объекты и, наконец, считывает все BeanDefinitions из beanDefinitionNames, включая все вышеперечисленные реализации EnableAutoConfiguration, а затем создает экземпляр изменения
- При создании экземпляра конкретной реализации EnableAutoConfiguration будет выполняться определенная логика в этих классах реализации.В качестве примера на примере Dubbo будет инициализирован com.alibaba.dubbo.spring.boot.DubboAutoConfiguration.
com.alibaba.dubbo.spring.boot.DubboProviderAutoConfiguration,
com.alibaba.dubbo.spring.boot.DubboConsumerAutoConfiguration Эти три класса реализации запускают и регистрируют dubbo в контейнере spring.
Реализовать spring-boot-starter
После того, как вы поймете принцип, очень просто реализовать свой собственный стартер.
Предположим у меня есть компонент, очень быдло, есть возможность спасти мир, и есть возможность спасти мир после того, как вы получите доступ к миру.Как сделать, чтобы ваша система spring-boot быстро обращалась к этому быдлу Компоненты. чтобы реализовать стартер, вы можете положиться на меня этот стартер.
Сначала определите интерфейс, чтобы спасти мир
package com.north.lat.service;
/**
* @author lhh
*/
public interface SaveTheWorldService {
/**
* 拯救世界
* @param name 留名
* @return
*/
String saveTheWorld(String name);
}
абстрактный класс
package com.north.lat.service;
import lombok.extern.log4j.Log4j;
import java.util.Random;
/**
* @author lhh
*/
@Log4j
public abstract class AbstractSaveTheWorldService implements SaveTheWorldService {
private final static Random RANDOM = new Random();
private final static String SUCCESS_MSG = "WAOOOOOOO! 大英雄";
private final static String FAIL_MSG = "拯救世界是个高风险行业";
@Override
public String saveTheWorld(String name) {
int randomInt = RANDOM.nextInt(100);
String msg;
if((randomInt + 1) > getDieRate()){
msg = SUCCESS_MSG +"," + name + "拯救了这个世界!";
}else{
msg = FAIL_MSG + "," + name + ",你失败了,下辈子再来吧";
}
log.info(msg);
return msg;
}
/**
* 指定死亡率
* @return
*/
public abstract int getDieRate();
}
Для обычных людей, чтобы спасти мир, средний процент неудач составляет 99%.
package com.north.lat.service.impl;
import com.north.lat.service.AbstractSaveTheWorldService;
/**
* 普通人拯救世界
* @author lhh
*/
public class CommonSaveTheWorldServiceImpl extends AbstractSaveTheWorldService {
private final static int DIE_RATE = 99;
@Override
public int getDieRate() {
return DIE_RATE;
}
}
Спасите мир как герой, вероятность успеха составляет 99%
package com.north.lat.service.impl;
import com.north.lat.service.AbstractSaveTheWorldService;
/**
* 英雄拯救世界
* @author lhh
*/
public class HeroSaveTheWorldImpl extends AbstractSaveTheWorldService {
private final static int DIE_RATE = 1;
@Override
public int getDieRate() {
return DIE_RATE;
}
}
Что ж, наш супер классный компонент родился Давайте подготовимся к доступу к springboot и реализуем NbAutoConfiguration следующим образом:
package com.north.lat;
import com.north.lat.service.SaveTheWorldService;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.SpringFactoriesLoader;
import java.util.List;
/**
* @author lhh
* 注入environment和applicationContext 以便做一些后续操作
*/
@Configuration
@ConditionalOnClass(SaveTheWorldService.class)
public class NbAutoConfiguration implements EnvironmentAware,ApplicationContextAware,BeanDefinitionRegistryPostProcessor {
private Environment environment;
private ApplicationContext applicationContext;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
// 我这里是从spring.factories加载了SaveTheWorldService的所有实现,
List<SaveTheWorldService> saveTheWorldServices = SpringFactoriesLoader.loadFactories(SaveTheWorldService.class, this.getClass().getClassLoader());
// 然后用BeanDefinitionRegistry 注册到BeanDefinitions
saveTheWorldServices.forEach(saveTheWorldService->{
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(saveTheWorldService.getClass());
beanDefinition.setLazyInit(false);
beanDefinition.setAbstract(false);
beanDefinition.setAutowireCandidate(true);
beanDefinition.setScope("singleton");
registry.registerBeanDefinition(saveTheWorldService.getClass().getSimpleName(), beanDefinition);
});
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
}
Снова настройте spring.factories
На ранней стадии разработки компонента герой еще не найден, поэтому можем отправить только обычного человека, поэтому файл spring.factories of niubility-spring-starter-1.0-SNAPSHOT.jar такой
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.north.lat.NbAutoConfiguration
com.north.lat.service.SaveTheWorldService=\
com.north.lat.service.impl.CommonSaveTheWorldServiceImpl
Позже, после бесчисленных дней и ночей сверхурочной работы разработчиков, герой, наконец, был найден, поэтому файл spring.factories of niubility-spring-starter-2.0-SNAPSHOT.jar стал таким
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.north.lat.NbAutoConfiguration
com.north.lat.service.SaveTheWorldService=\
com.north.lat.service.impl.HeroSaveTheWorldImpl
Это сделано, структура проекта показана ниже:
Итак, как получить к нему доступ?Давайте попробуем получить доступ к нему в проекте spilat прямо сейчас:
Опираясь на пакет jar, на этот раз он подключен к версии 1.0, на этом доступ завершается.
<dependency>
<groupId>com.north.lat</groupId>
<artifactId>niubility-spring-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
Так называемый полный доступ означает, что все реализации SaveTheWorldService были зарегистрированы весной, а именно CommonSaveTheWorldServiceImpl (версия 1.0) или HeroSaveTheWorldImpl (версия 2.0).
Мы вводим звонки в контроллере
package com.north.spilat.controller;
import com.north.lat.service.SaveTheWorldService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @author lhh
*/
@RestController
public class HelloWorldController {
@Resource
private SaveTheWorldService saveTheWorldService;
@RequestMapping("/saveTheWorld")
public String index(String name) {
return saveTheWorldService.saveTheWorld(name);
}
}
При использовании версии 1.0 частота отказов действительно составляет 99%, и результаты следующие:
После выхода версии 2.0 быстро перейдите на версию 2.0 и обновите номер версии в pom.xml:
<dependency>
<groupId>com.north.lat</groupId>
<artifactId>niubility-spring-starter</artifactId>
<version>2.0-SNAPSHOT</version>
</dependency>
再看看运行结果, 就非常完美啦
В приведенном выше примере, независимо от того, получаем ли мы доступ к компонентам или обновляем их, мы просто полагаемся на пакет jar, который действительно реализует подключаемость и низкую связанность. Конечно, в реальном сценарии приложения нам также может понадобиться добавить небольшую конфигурацию, такую как приведенная выше spring-boot-starter-dubbo, а также druid-spring-boot-starter, spring-boot-starter-disconf и т. д. , что мы часто используем
Суммировать
Можно сказать, что декаплинг — это то, чем занимались поколения программистов всю свою жизнь.За прошедшие годы было предложено и реализовано бесчисленное множество инструментов и идей, и SPI — одна из них.
Механизм SPI очень распространен в различных фреймворках с открытым исходным кодом, и механизмы SPI разных фреймворков различны, более или менее претерпели некоторую эволюцию, но на самом деле принципы, лежащие в основе, схожи.
Таким образом, понимание этих механизмов, с одной стороны, может помочь нам лучше понять принципы работы фреймворков с открытым исходным кодом и избежать обходных путей; с другой стороны, его также можно использовать в качестве справочного материала для нашего ежедневного написания кода и проектирования систем. чтобы написать более элегантный код.