[Примечания к источнику] Проект анализа исходного кода Github доступен онлайн! ! ! Ниже приведен адрес Github заметки:
GitHub.com/Примечания к источнику/…
Эта статья продолжаетКаков процесс запуска SpringBoot? Исходный код SpringBoot (семь)
1 Уроки прошлого
Ознакомившись со старым и узнав новое, давайте кратко повторим содержание предыдущей статьи.В предыдущей статье мы разобралиПроцесс запуска SpringBoot, ключевые шаги теперь сжаты и обобщены:
- Построить
SpringApplicationОбъект, используемый для запуска SpringBoot; - от
spring.factoriesзагрузить в файл конфигурацииEventPublishingRunListenerОбъекты используются для генерации различных событий жизненного цикла на разных этапах запуска; - Подготовьте переменные среды, включая системные переменные, переменные среды, параметры командной строки и файлы конфигурации (такие как
application.properties)Ждать; - Создать контейнер
ApplicationContext; - Выполните некоторую работу по инициализации объекта-контейнера, созданного на шаге 4, подготовьте некоторые значения свойств контейнера и т. д. и вызовите каждый из них.
ApplicationContextInitializerМетод инициализации для выполнения некоторой логики инициализации и т.д.; - Обновление контейнера, этот шаг очень важен, это ключевой момент, здесь реализована слишком сложная логика;
- перечислить
ApplicationRunnerиCommandLineRunnerМетод run может реализовать эти два интерфейса для загрузки некоторых бизнес-данных после запуска контейнера;
В процессе запуска SpringBoot каждая отдельная фаза запуска будет генерировать разные встроенные события жизненного цикла, а затем соответствующие прослушиватели будут прослушивать эти события для выполнения некоторой логической работы по инициализации, такой какConfigFileApplicationListenerбудет контролироватьonApplicationEnvironmentPreparedEventсобытия для загрузки переменных среды и т. д.
2 Введение
В прошлой статье объяснялось, как SpringBoot запускает процесс, мы видели новыйSpringApplicationОбъект SpringBoot используется для запуска проекта. Что ж, сегодня мы посмотримSpringApplicationПроцесс построения объекта и объяснение механизма SPI, реализованного самой SpringBoot.
3 Процесс построения объекта SpringApplication
Этот раздел начинаетсяSpringApplicationПроцесс построения объекта, потому что построение объекта — это не что иное, как присвоение значений некоторым его свойствам-членам в его конструкторе, редко включая другую дополнительную бизнес-логику (конечно, иногда мы также можем открывать некоторые потоки в конструкторе). конструктор или что-то в этом роде). Итак, давайте сначала посмотрим на структуруSpringApplicationНекоторые свойства членов, которые необходимо использовать, когда объект:
// SpringApplication.java
/**
* SpringBoot的启动类即包含main函数的主类
*/
private Set<Class<?>> primarySources;
/**
* 包含main函数的主类
*/
private Class<?> mainApplicationClass;
/**
* 资源加载器
*/
private ResourceLoader resourceLoader;
/**
* 应用类型
*/
private WebApplicationType webApplicationType;
/**
* 初始化器
*/
private List<ApplicationContextInitializer<?>> initializers;
/**
* 监听器
*/
private List<ApplicationListener<?>> listeners;
можно посмотреть сборкуSpringApplicationЦель в основном состоит в том, чтобы присвоить значения шести свойствам членов в приведенном выше коде, и теперь я рассмотрю это.SpringApplicationпроцесс строительства объекта.
Вернемся к конструкции, описанной в предыдущей статье.SpringApplicationПо коду объекта:
// SpringApplication.java
// run方法是一个静态方法,用于启动SpringBoot
public static ConfigurableApplicationContext run(Class<?>[] primarySources,
String[] args) {
// 构建一个SpringApplication对象,并调用其run方法来启动
return new SpringApplication(primarySources).run(args);
}
следовать заSpringApplicationв конструкторе:
// SpringApplication.java
public SpringApplication(Class<?>... primarySources) {
// 继续调用SpringApplication另一个构造函数
this(null, primarySources);
}
следовать заSpringApplicationДругой конструктор:
// SpringApplication.java
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// 【1】给resourceLoader属性赋值,注意传入的resourceLoader参数为null
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
// 【2】给primarySources属性赋值,传入的primarySources其实就是SpringApplication.run(MainApplication.class, args);中的MainApplication.class
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 【3】给webApplicationType属性赋值,根据classpath中存在哪种类型的类来确定是哪种应用类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 【4】给initializers属性赋值,利用SpringBoot自定义的SPI从spring.factories中加载ApplicationContextInitializer接口的实现类并赋值给initializers属性
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
// 【5】给listeners属性赋值,利用SpringBoot自定义的SPI从spring.factories中加载ApplicationListener接口的实现类并赋值给listeners属性
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 【6】给mainApplicationClass属性赋值,即这里要推断哪个类调用了main函数,然后再赋值给mainApplicationClass属性,用于后面启动流程中打印一些日志。
this.mainApplicationClass = deduceMainApplicationClass();
}
можно посмотреть сборкуSpringApplicationОбъект на самом деле к 6 упомянутым вышеSpringApplicationПрисваиваются только свойства-члены класса, и выполняется некоторая работа по инициализации:
-
давать
resourceLoaderуступка имущества,resourceLoaderАтрибут, загрузчик ресурсов, переданный в это времяresourceLoaderПараметрыnull; -
давать
primarySourcesуступка имущества,primarySourcesАтрибуты即SpringApplication.run(MainApplication.class,args);входящийMainApplication.class, этот класс является классом запуска проекта SpringBoot, в основном через этот класс для сканированияConfigurationзагрузка классаbean; -
давать
webApplicationTypeуступка имущества,webApplicationTypeатрибут, представляющий тип приложения, согласноclasspathсоответствующий существованиюApplicationкласс судить. потому что позжеwebApplicationTypeчтобы определить, какойEnvironmentобъект и что создатьApplicationContext, для подробного анализа, пожалуйста, обратитесь к следующему第3.1小节; -
давать
initializersуступка имущества,initializersсобственностьList<ApplicationContextInitializer<?>>Коллекция с использованием механизма SpringBoot SPI изspring.factoriesЭти инициализаторы, загруженные в файл конфигурации, будут применяться для выполнения некоторой работы по инициализации позже, когда контейнер будет инициализирован. Поскольку механизм SPI, реализованный самим SpringBoot, более важен, он анализируется в отдельном разделе, а для более подробного анализа см. следующие разделы.第4小节; -
давать
listenersуступка имущества,listenersсобственностьList<ApplicationListener<?>>Коллекция, также использующая механизм SpringBoot SPI изspring.factoriesзагружается в файл конфигурации. Поскольку Spring Boot будет генерировать некоторые события на разных этапах процесса запуска, эти загруженные прослушиватели должны прослушивать некоторые события жизненного цикла в процессе запуска Spring Boot; -
давать
mainApplicationClassуступка имущества,mainApplicationClassпредставление атрибута содержитmainКласс функции, то есть, какой класс здесь вывести для вызоваmainфункцию, а затем присвойте полное имя классаmainApplicationClassАтрибут, который используется для печати некоторых журналов в последующем процессе запуска.Для подробного анализа см. следующее第3.2小节.
3.1 Определите тип приложения проекта
Затем анализируем структуруSpringApplicationпервый объект【3】шагWebApplicationType.deduceFromClasspath();Этот код:
// WebApplicationType.java
public enum WebApplicationType {
// 普通的应用
NONE,
// Servlet类型的web应用
SERVLET,
// Reactive类型的web应用
REACTIVE;
private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
"org.springframework.web.context.ConfigurableWebApplicationContext" };
private static final String WEBMVC_INDICATOR_CLASS = "org.springframework."
+ "web.servlet.DispatcherServlet";
private static final String WEBFLUX_INDICATOR_CLASS = "org."
+ "springframework.web.reactive.DispatcherHandler";
private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";
private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";
static WebApplicationType deduceFromClasspath() {
// 若classpath中不存在"org.springframework." + "web.servlet.DispatcherServlet"和"org.glassfish.jersey.servlet.ServletContainer"
// 则返回WebApplicationType.REACTIVE,表明是reactive应用
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
// 若{ "javax.servlet.Servlet",
// "org.springframework.web.context.ConfigurableWebApplicationContext" }
// 都不存在在classpath,则说明是不是web应用
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
// 最终返回普通的web应用
return WebApplicationType.SERVLET;
}
}
Как указано выше, в соответствии сclasspathОпределить тип приложения, то есть загрузить его через отражениеclasspathСудить, существует ли указанный класс флагов или нет, чтобы судить, существует ли он.Reactiveприменение,ServletЭтот тип веб-приложения также является обычным приложением.
3.2 Сделайте вывод, какой класс вызывает основную функцию
Давайте сначала пропустим строительствоSpringApplicationпервый объект【4】шаг и【5】Шаг, сначала проанализировать структуруSpringApplicationпервый объект【6】шагthis.mainApplicationClass = deduceMainApplicationClass();Этот код:
// SpringApplication.java
private Class<?> deduceMainApplicationClass() {
try {
// 获取StackTraceElement对象数组stackTrace,StackTraceElement对象存储了调用栈相关信息(比如类名,方法名等)
StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
// 遍历stackTrace数组
for (StackTraceElement stackTraceElement : stackTrace) {
// 若stackTraceElement记录的调用方法名等于main
if ("main".equals(stackTraceElement.getMethodName())) {
// 那么就返回stackTraceElement记录的类名即包含main函数的类名
return Class.forName(stackTraceElement.getClassName());
}
}
}
catch (ClassNotFoundException ex) {
// Swallow and continue
}
return null;
}
можно увидетьdeduceMainApplicationClassОсновная функция метода состоит в том, чтобыStackTraceElementПолучить, какой класс вызывается в массиве стека вызововmainметод, а затем верните задание вmainApplicationClassатрибут, а затем используется для печати некоторых журналов в более позднем процессе запуска.
4 Интерпретация принципа механизма SPI SpringBoot
Поскольку механизм SPI SpringBoot является очень важной точкой знаний, он анализируется здесь в отдельном разделе. Все мы знаем, что SpringBoot не использует механизм SPI Java (механизм SPI Java может видетьКак Java реализует собственный механизм SPI?, действительно полно галантереи), а нестандартная реализация собственного механизма SPI. SpringBoot использует механизм пользовательской реализации SPI для загрузки классов реализации инициализатора, классов реализации прослушивателя, классов автоконфигурации и т. д. Если мы хотим добавить классы автоконфигурации или пользовательские слушатели, то важным шагом для нас являетсяspring.factoriesНастроен, а затем загружен SpringBoot.
Хорошо, теперь давайте сосредоточимся на анализе.Как SpringBoot реализует собственный механизм SPI.
Вот построение подраздела 3SpringApplicationпервый объект【4】шаг и【5】код шага, потому что первый【4】шаг и【5】Шаги заключаются в использовании механизма SPI SpringBoot для загрузки класса реализации расширения, поэтому здесь анализируется только первая часть.【4】шагsetInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));Посмотрите на этот кодgetSpringFactoriesInstancesКак SpringBoot реализует собственный набор SPI для загрузки в методApplicationContextInitializerРасширенный класс реализации интерфейса инициализатора?
// SpringApplication.java
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
// 继续调用重载的getSpringFactoriesInstances方法进行加载
return getSpringFactoriesInstances(type, new Class<?>[] {});
}
продолжать следить за перегруженнымgetSpringFactoriesInstancesметод:
// SpringApplication.java
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
Class<?>[] parameterTypes, Object... args) {
// 【1】获得类加载器
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
// 【2】将接口类型和类加载器作为参数传入loadFactoryNames方法,从spring.factories配置文件中进行加载接口实现类
Set<String> names = new LinkedHashSet<>(
SpringFactoriesLoader.loadFactoryNames(type, classLoader));
// 【3】实例化从spring.factories中加载的接口实现类
List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
classLoader, args, names);
// 【4】进行排序
AnnotationAwareOrderComparator.sort(instances);
// 【5】返回加载并实例化好的接口实现类
return instances;
}
Видно, что самое важное в коде механизма SPI пользовательской реализации SpringBoot — это приведенный выше код.【1】,【2】,【3】Следующие три шага анализируются отдельно.
4.1 Получить загрузчик классов
все еще помнюКак Java реализует собственный механизм SPI?Механизм SPI в Java в этой статье использует загрузчик класса контекста потока для загрузки классов расширения по умолчанию, а затемКакой загрузчик классов используется для загрузки механизма SPI, реализованного самим SpringBootspring.factoriesКак насчет класса реализации расширения в файле конфигурации?
Давайте посмотрим непосредственно на【1】шагClassLoader classLoader = getClassLoader();Этот код, краткий обзор:
// SpringApplication.java
public ClassLoader getClassLoader() {
// 前面在构造SpringApplicaiton对象时,传入的resourceLoader参数是null,因此不会执行if语句里面的逻辑
if (this.resourceLoader != null) {
return this.resourceLoader.getClassLoader();
}
// 获取默认的类加载器
return ClassUtils.getDefaultClassLoader();
}
следовать заgetDefaultClassLoaderметод:
// ClassUtils.java
public static ClassLoader getDefaultClassLoader() {
ClassLoader cl = null;
try {
// 【重点】获取线程上下文类加载器
cl = Thread.currentThread().getContextClassLoader();
}
catch (Throwable ex) {
// Cannot access thread context ClassLoader - falling back...
}
// 这里的逻辑不会执行
if (cl == null) {
// No thread context class loader -> use class loader of this class.
cl = ClassUtils.class.getClassLoader();
if (cl == null) {
// getClassLoader() returning null indicates the bootstrap ClassLoader
try {
cl = ClassLoader.getSystemClassLoader();
}
catch (Throwable ex) {
// Cannot access system ClassLoader - oh well, maybe the caller can live with null...
}
}
}
// 返回刚才获取的线程上下文类加载器
return cl;
}
Видно, что загрузчик класса контекста потока также используется для загрузки оригинального механизма SpringBoot SPI.spring.factoriesКласс реализации расширения в файле!
4.2 Загрузите класс расширения SPI в конфигурационный файл spring.factories
давайте посмотрим на следующий【2】шагSpringFactoriesLoader.loadFactoryNames(type, classLoader)Как загрузить этот кодspring.factoriesКласс расширения SPI в файле конфигурации?
// SpringFactoriesLoader.java
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
// factoryClass即SPI接口,比如ApplicationContextInitializer,EnableAutoConfiguration等接口
String factoryClassName = factoryClass.getName();
// 【主线,重点关注】继续调用loadSpringFactories方法加载SPI扩展类
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
следовать заloadSpringFactoriesметод:
// SpringFactoriesLoader.java
/**
* The location to look for factories.
* <p>Can be present in multiple JAR files.
*/
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 以classLoader作为键先从缓存中取,若能取到则直接返回
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
// 若缓存中无记录,则去spring.factories配置文件中获取
try {
// 这里加载所有jar包中包含"MATF-INF/spring.factories"文件的url路径
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
// 遍历urls路径,将所有spring.factories文件的键值对(key:SPI接口类名 value:SPI扩展类名)
// 加载放到 result集合中
while (urls.hasMoreElements()) {
// 取出一条url
URL url = urls.nextElement();
// 将url封装到UrlResource对象中
UrlResource resource = new UrlResource(url);
// 利用PropertiesLoaderUtils的loadProperties方法将spring.factories文件键值对内容加载进Properties对象中
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
// 遍历刚加载的键值对properties对象
for (Map.Entry<?, ?> entry : properties.entrySet()) {
// 取出SPI接口名
String factoryClassName = ((String) entry.getKey()).trim();
// 遍历SPI接口名对应的实现类即SPI扩展类
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
// SPI接口名作为key,SPI扩展类作为value放入result中
result.add(factoryClassName, factoryName.trim());
}
}
}
// 以classLoader作为key,result作为value放入cache缓存
cache.put(classLoader, result);
// 最终返回result对象
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
Как и выше код,loadSpringFactoriesГлавное, что делает метод, это использовать загрузчик класса контекста потока, полученный перед преобразованиемclasspathвсе вspring.factoriesВсе классы реализации расширений всех интерфейсов SPI в файле конфигурации загружаются и затем помещаются в кэш.Уведомление, здесь загрузить все классы реализации расширения SPI за один раз, поэтому после этого вы можете получить классы расширения SPI непосредственно из кеша в соответствии с интерфейсом SPI, поэтому вам не нужно идти сноваspring.factoriesКласс реализации расширения, соответствующий интерфейсу SPI, получается в конфигурационном файле. например, последующее приобретениеApplicationListener,FailureAnalyzerиEnableAutoConfigurationРасширенные классы реализации интерфейса можно получить непосредственно из кеша.
Мысль 1:Почему здесь одноразовый
spring.factoriesПолучить все классы расширения в файле конфигурации и поместить их в кеш? Вместо того, чтобы каждый раз идти по интерфейсу SPIspring.factoriesПолучить его в файле конфигурации?
Мысль 2:Помните исходный код автоматической настройки SpringBoot, упомянутый ранее.
AutoConfigurationImportFilterЭтот интерфейс работает? Теперь мы должны более четко понять роль этого интерфейса.
После загрузки всех классов реализации расширения SPI вызовите его снова в это время.getOrDefault(factoryClassName, Collections.emptyList())Метод фильтрует текущий соответствующий расширенный класс реализации в соответствии с именем интерфейса SPI, например, переданным здесь.factoryClassNameпараметр с именемApplicationContextInitializerинтерфейс, то этот интерфейс будет использоваться какkeyПолучить данные из кеша прямо сейчасApplicationContextInitializerКласс реализации расширения SPI, соответствующий интерфейсу. из которых отspring.factoriesполучен изApplicationContextInitializerВсе классы реализации расширения SPI, соответствующие интерфейсу, показаны на следующем рисунке:
4.3 Создайте экземпляр класса расширения SPI, загруженного из spring.factories
спереди отspring.factoriesполучено вApplicationContextInitializerПосле того, как все классы реализации расширения SPI, соответствующие интерфейсу, будут реализованы, в это время будут созданы экземпляры этих классов расширения SPI.
Теперь давайте посмотрим на предыдущий【3】Код создания шага:List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);.
// SpringApplication.java
private <T> List<T> createSpringFactoriesInstances(Class<T> type,
Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args,
Set<String> names) {
// 新建instances集合,用于存储稍后实例化后的SPI扩展类对象
List<T> instances = new ArrayList<>(names.size());
// 遍历name集合,names集合存储了所有SPI扩展类的全限定名
for (String name : names) {
try {
// 根据全限定名利用反射加载类
Class<?> instanceClass = ClassUtils.forName(name, classLoader);
// 断言刚才加载的SPI扩展类是否属于SPI接口类型
Assert.isAssignable(type, instanceClass);
// 获得SPI扩展类的构造器
Constructor<?> constructor = instanceClass
.getDeclaredConstructor(parameterTypes);
// 实例化SPI扩展类
T instance = (T) BeanUtils.instantiateClass(constructor, args);
// 添加进instances集合
instances.add(instance);
}
catch (Throwable ex) {
throw new IllegalArgumentException(
"Cannot instantiate " + type + " : " + name, ex);
}
}
// 返回
return instances;
}
Приведенный выше код очень прост, главное создать экземпляр класса расширения SPI. Что ж, пользовательский механизм SPI SpringBoot был проанализирован.
Мысль 3:Почему SpringBoot не поддерживает SPI Java и настраивает набор SPI?
5 Резюме
Что ж, это конец этой статьи. Давайте сначала подытожим предыдущие точки знаний:
- Анализы
SpringApplicationстроительство объекта; - Проанализирован набор механизмов SPI, реализованных самим SpringBoot.
6 Чувство
С тех пор, как в феврале я начал писать статьи по анализу исходного кода, я также познакомился с некоторыми техническими экспертами, по которым я вижу, что чем сильнее люди, тем усерднее они работают. Оглядываясь назад, я понимаю, что сейчас мои знания очень узки, и, что более важно, у меня нет глубоких знаний в области технологий. Я могу обобщить их в одном предложении, и я очень хорош. Я вижу, что большие коровы, которые лучше меня, все еще так тяжело, что у меня есть причины не работать? Мне очень нравятся слова г-на Дин Вэя: "только упорство«. Затем делайте шаг за шагом, верьте, что вы можете добиться большего прогресса, и продолжайте идти.