Как устроен объект SpringApplication? Исходный код SpringBoot (8)

Spring Boot
Как устроен объект SpringApplication? Исходный код SpringBoot (8)

[Примечания к источнику] Проект анализа исходного кода Github доступен онлайн! ! ! Ниже приведен адрес Github заметки:

GitHub.com/Примечания к источнику/…

Эта статья продолжаетКаков процесс запуска SpringBoot? Исходный код SpringBoot (семь)

1 Уроки прошлого

Ознакомившись со старым и узнав новое, давайте кратко повторим содержание предыдущей статьи.В предыдущей статье мы разобралиПроцесс запуска SpringBoot, ключевые шаги теперь сжаты и обобщены:

  1. ПостроитьSpringApplicationОбъект, используемый для запуска SpringBoot;
  2. отspring.factoriesзагрузить в файл конфигурацииEventPublishingRunListenerОбъекты используются для генерации различных событий жизненного цикла на разных этапах запуска;
  3. Подготовьте переменные среды, включая системные переменные, переменные среды, параметры командной строки и файлы конфигурации (такие какapplication.properties)Ждать;
  4. Создать контейнерApplicationContext;
  5. Выполните некоторую работу по инициализации объекта-контейнера, созданного на шаге 4, подготовьте некоторые значения свойств контейнера и т. д. и вызовите каждый из них.ApplicationContextInitializerМетод инициализации для выполнения некоторой логики инициализации и т.д.;
  6. Обновление контейнера, этот шаг очень важен, это ключевой момент, здесь реализована слишком сложная логика;
  7. перечислить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Присваиваются только свойства-члены класса, и выполняется некоторая работа по инициализации:

  1. даватьresourceLoaderуступка имущества,resourceLoaderАтрибут, загрузчик ресурсов, переданный в это времяresourceLoaderПараметрыnull;
  2. даватьprimarySourcesуступка имущества,primarySourcesАтрибуты即SpringApplication.run(MainApplication.class,args);входящийMainApplication.class, этот класс является классом запуска проекта SpringBoot, в основном через этот класс для сканированияConfigurationзагрузка классаbean;
  3. даватьwebApplicationTypeуступка имущества,webApplicationTypeатрибут, представляющий тип приложения, согласноclasspathсоответствующий существованиюApplicationкласс судить. потому что позжеwebApplicationTypeчтобы определить, какойEnvironmentобъект и что создатьApplicationContext, для подробного анализа, пожалуйста, обратитесь к следующему第3.1小节;
  4. даватьinitializersуступка имущества,initializersсобственностьList<ApplicationContextInitializer<?>>Коллекция с использованием механизма SpringBoot SPI изspring.factoriesЭти инициализаторы, загруженные в файл конфигурации, будут применяться для выполнения некоторой работы по инициализации позже, когда контейнер будет инициализирован. Поскольку механизм SPI, реализованный самим SpringBoot, более важен, он анализируется в отдельном разделе, а для более подробного анализа см. следующие разделы.第4小节;
  5. даватьlistenersуступка имущества,listenersсобственностьList<ApplicationListener<?>>Коллекция, также использующая механизм SpringBoot SPI изspring.factoriesзагружается в файл конфигурации. Поскольку Spring Boot будет генерировать некоторые события на разных этапах процесса запуска, эти загруженные прослушиватели должны прослушивать некоторые события жизненного цикла в процессе запуска Spring Boot;
  6. давать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 Резюме

Что ж, это конец этой статьи. Давайте сначала подытожим предыдущие точки знаний:

  1. АнализыSpringApplicationстроительство объекта;
  2. Проанализирован набор механизмов SPI, реализованных самим SpringBoot.

6 Чувство

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