[Примечания к источнику] Проект анализа исходного кода 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 Чувство
С тех пор, как в феврале я начал писать статьи по анализу исходного кода, я также познакомился с некоторыми техническими экспертами, по которым я вижу, что чем сильнее люди, тем усерднее они работают. Оглядываясь назад, я понимаю, что сейчас мои знания очень узки, и, что более важно, у меня нет глубоких знаний в области технологий. Я могу обобщить их в одном предложении, и я очень хорош. Я вижу, что большие коровы, которые лучше меня, все еще так тяжело, что у меня есть причины не работать? Мне очень нравятся слова г-на Дин Вэя: "только упорство«. Затем делайте шаг за шагом, верьте, что вы можете добиться большего прогресса, и продолжайте идти.