Механизм SPI в springboot-starter

Java

Полное название SPI — Service Provider Interface, что дословно переводится как «Интерфейс поставщика услуг», что звучит неуклюже, поэтому я пытаюсь перевести его как «Интерфейс поставщика услуг».

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

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

  1. ExtensionLoader в dubbo, с которым все знакомы, может добавлять некоторые настраиваемые подключаемые функции через эти точки расширения, такие как добавление фильтра для реализации доступа к белому списку, реализация ограничения тока интерфейса и другие функции; или вы можете напрямую заменить его собственный протокол, транспорт и т.д.
  2. При разработке плагина idea intellij необходимо определить файл /META-INF/plugin.xml. В этом plugin.xml есть много мест для настройки serviceInterface и serviceImplementation, который также является механизмом SPI. Это позволяет разработчикам подключаемых модулей не только использовать API, предоставляемый его базовым SDK, но также позволяет разработчикам настраивать функции, а связь довольно низка.При разработке подключаемого модуля intellij он напрямую использует ServiceLoader в JDK
  3. Механизм SPI также широко используется весной, и в этой статье будет разобрана его часть.

SPI в JDK

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

  1. Определение интерфейса поиска Поиск
 package com.north.spilat.service;
 import java.util.List;
 public interface Search {
     List<String> search(String keyword);
 }
  1. Реализовать интерфейс для запроса из базы данных
    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;
        }
    
    }
  1. Реализовать интерфейс для запроса из файловой системы
 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;
     }
 
 }
  1. Создайте каталог 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
    Весь каталог проекта выглядит следующим образом:

    image

  2. Создайте новый основной метод для тестирования

 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) давайте взглянем на этот класс.Следующее в основном предназначено для вставки кода, а анализ находится в комментариях к коду.

  1. Видно, что логики в нем не так много, а основная логика отдана в 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);
 }
  1. Итератор 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;
 }
  1. Посмотрим, что делает 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, что нужно сделать.

  1. Внесите зависимость dubbo-spring-boot-starter в файл pom.
        <dependency>
            <groupId>com.alibaba.spring.boot</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>
  1. Настройте параметры, связанные с dubbo в файле Application.properties
    image
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
  1. Добавьте соответствующую аннотацию в класс запуска пружинного загрузки
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);
    }
}
  1. Определить интерфейс, реализовать и вызвать

интерфейс

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

image
На картинке выше показана структура пакета 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, о котором мы упоминали в начале.

  1. 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

  1. spring.providers
provides: dubbo-spring-boot-starter

Spring.providers — такое простое предложение, что немного разочаровывает, так что давайте обратим внимание на spring.factories.

imager
Ищите Spring.Factory в IDEA.Не спрашивайте,не знаю,поиск пугает.Оказывается такой файл будет в принципе в каждом JAR пакете связанном с springboot.

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

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

Излишне говорить, что это предсказание должно быть точным, иначе я бы написал столько слов напрасно.Но как доказать, что наше предсказание является точным.Проведем также «эксперимент».

Процесс запуска Springboot

Чтобы разобраться в процессе запуска Springboot, лучше всего изучить его исходный код.

Кодекс Sprilboot по-прежнему очень «гуманизированный». Спрингбат ясно говорит вам, что его запись является основным методом. Следовательно, довольно комфортно читать код SpringBoot.,

image

На приведенном выше рисунке показан процесс запуска проекта 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.Давайте подробнее рассмотрим, что в нем есть.

  1. FACTORIES_RESOURCE_LOCATION указывает на META-INF/spring.factories, о которых мы упоминали выше.
  2. loadFactory, найдите указанный класс реализации интерфейса из META-INF/spring.factories и создайте его экземпляр, где поиск выполняется путем вызова loadFactoryNames
  3. loadFactoryNames находит полное имя реализующего класса определенного интерфейса из указанного местоположения.
  4. 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, он будет загружен для вас.

image
После загрузки, что вы делаете?Посмотрев вниз, вы можете обнаружить, что примерный процесс выглядит так:

  1. this.reader.loadBeanDefinitions(configClasses); configClasses — это все классы реализации, прочитайте эти классы и подготовьтесь к синтаксическому анализу
  2. registerBeanDefinition для регистрации в beanDefinitionNames
  3. В операции spring refresh() последним шагом является finishBeanFactoryInitialization(beanFactory), который инициализирует все одноэлементные объекты и, наконец, считывает все BeanDefinitions из beanDefinitionNames, включая все вышеперечисленные реализации EnableAutoConfiguration, а затем создает экземпляр изменения
  4. При создании экземпляра конкретной реализации 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

Это сделано, структура проекта показана ниже:

image

Итак, как получить к нему доступ?Давайте попробуем получить доступ к нему в проекте 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%, и результаты следующие:

image
После выхода версии 2.0 быстро перейдите на версию 2.0 и обновите номер версии в pom.xml:

    <dependency>
        <groupId>com.north.lat</groupId>
        <artifactId>niubility-spring-starter</artifactId>
        <version>2.0-SNAPSHOT</version>
    </dependency>
再看看运行结果, 就非常完美啦

image

В приведенном выше примере, независимо от того, получаем ли мы доступ к компонентам или обновляем их, мы просто полагаемся на пакет jar, который действительно реализует подключаемость и низкую связанность. Конечно, в реальном сценарии приложения нам также может понадобиться добавить небольшую конфигурацию, такую ​​​​как приведенная выше spring-boot-starter-dubbo, а также druid-spring-boot-starter, spring-boot-starter-disconf и т. д. , что мы часто используем

Суммировать

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

Механизм SPI очень распространен в различных фреймворках с открытым исходным кодом, и механизмы SPI разных фреймворков различны, более или менее претерпели некоторую эволюцию, но на самом деле принципы, лежащие в основе, схожи.

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