SpringBoot запускает анализ исходного кода и обучение соответствующим навыкам

Spring Boot
SpringBoot запускает анализ исходного кода и обучение соответствующим навыкам

Для изучения исходного кода, я думаю, будет лучше, если мы рассмотрим его вместе с вопросами.

1. Каков принцип запуска Springboot?

Без лишних слов, давайте перейдем к [start.spring.io] Скачиваем демо на сайте, выбираем версию springboot2.1.4, а затем давайте шаг за шагом разберем суть, чтобы понять принцип запуска Springboot.

Каталог нашего проекта выглядит следующим образом:

все будет от нашегоDemoApplication.javaфайл запускается. код показывает, как показано ниже:

@SpringBootApplication
public class DemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}
}

Навык один

я часто вижу друзейDemoApplicationреализован в классеApplicationContextAwareинтерфейс, а затем получитьApplicationContextобъект, например следующий код:

@SpringBootApplication
public class DemoApplication implements ApplicationContextAware {
    private static ApplicationContext applicationContext = null;
    public static void main(String[] args) {
    	SpringApplication.run(DemoApplication.class, args);
    	// 获取某个bean
    	System.out.println(applicationContext.getBean("xxxx"));
    }
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    	DemoApplication.applicationContext = applicationContext;
    }
}

Конечно, это возможно, но на самом делеSpringApplication.runМетод вернул контекст Spring, мы можем использовать его напрямую~~~ Код выглядит следующим образом:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
    	ConfigurableApplicationContext applicationContext = SpringApplication.run(DemoApplication.class, args);
    	// 获取某个bean
    	System.out.println(applicationContext.getBean("xxxx"));
    }
}

код перейти кSpringApplicationсорт263Ряд

@SuppressWarnings({ "unchecked", "rawtypes" })
    public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
        // 1、初始化一个类加载器
    	this.resourceLoader = resourceLoader;
    	Assert.notNull(primarySources, "PrimarySources must not be null");
    	// 2、启动类集合
    	this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    	// 3、当前应用类型,有三种:NONE、SERVLET、REACTIVE
    	this.webApplicationType = WebApplicationType.deduceFromClasspath();
    	// 4、初始化Initializer
    	setInitializers((Collection) getSpringFactoriesInstances(
    			ApplicationContextInitializer.class));
    	// 5、初始化Listeners
    	setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    	// 6、初始化入口类
    	this.mainApplicationClass = deduceMainApplicationClass();
    }

О шагах 1-3 говорить нечего, просто инициализируем некоторые флаги и списки.Сосредоточьтесь на пунктах 4 и 5. Пункты 4 и 5 помогают нам загрузить все зависимости.ApplicationListenerа такжеApplicationContextInitializerЭлемент конфигурации, код перемещается вSpringFactoriesLoaderпервый132Хорошо, мы видим, что Springboot загрузит этот файл в каждую банку.META-INF/spring.factoriescontent, а также начиная с загрузчика классовClassLoaderДля значения ключа создается Map cache для всех конфигураций.

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    // cache做了缓存,我们可以指定classloader,默认为Thread.currentThread().getContextClassLoader();
    // (可在ClassUtils类中getDefaultClassLoader找到答案)
    MultiValueMap<String, String> result = cache.get(classLoader);
    if (result != null) {
    	return result;
    }
    
    try {
    	Enumeration<URL> urls = (classLoader != null ?
    	        // FACTORIES_RESOURCE_LOCATION的值就是META-INF/spring.factories
    			classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
    			ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
    	result = new LinkedMultiValueMap<>();
    	while (urls.hasMoreElements()) {
    		URL url = urls.nextElement();
    		UrlResource resource = new UrlResource(url);
    		Properties properties = PropertiesLoaderUtils.loadProperties(resource);
    		for (Map.Entry<?, ?> entry : properties.entrySet()) {
    			String factoryClassName = ((String) entry.getKey()).trim();
    			for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
    				result.add(factoryClassName, factoryName.trim());
    			}
    		}
    	}
    	cache.put(classLoader, result);
    	return result;
    }
    catch (IOException ex) {
    	throw new IllegalArgumentException("Unable to load factories from location [" +
    			FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

давайте просто посмотримspring-boot-autoconfigure-2.1.4.RELEASE.jarвнизspring.factoriesВзгляните на следующее:

# Initializers初始化器
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners监听器
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configure自动配置(下文将会有讲原理)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
......

Навык 2

Теперь давайте посмотрим на шаг 6. Здесь мы можем научиться небольшому трюку: как получить информацию о классе промежуточного метода в текущей цепочке вызовов методов? Давайте посмотрим на исходный код:

private Class<?> deduceMainApplicationClass() {
    try {
    	StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
    	// 获取运行时方法栈
    	for (StackTraceElement stackTraceElement : stackTrace) {
    	    // 根据名称找到类名
    		if ("main".equals(stackTraceElement.getMethodName())) {
    			return Class.forName(stackTraceElement.getClassName());
    		}
    	}
    }
    catch (ClassNotFoundException ex) {
    	// Swallow and continue
    }
    return null;
}

Пока мы сделали толькоSpringApplicationИнициализация этого класса работает, у нас естьMETA-INF/spring.factoriesВсе имена классов, включая прослушиватели и инициализаторы, настроены в каталоге, и эти классы создаются и, наконец, сохраняются вSpringApplicationв этом классе.

код перемещается вSpringApplication.javaпервый295строка, код выглядит следующим образом

public ConfigurableApplicationContext run(String... args) {
    // 1、计时器,spring内部封装的计时器,用于计算容器启动的时间
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    // 2、创建一个初始化上下文变量
    ConfigurableApplicationContext context = null;
    // 3、这是spring报告之类的,没深入了解
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    configureHeadlessProperty();
    // 4、获取配置的SpringApplicationRunListener类型的监听器,并且启动它
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    try {
    	ApplicationArguments applicationArguments = new DefaultApplicationArguments(
    			args);
    	// 5、准备spring上下文环境
    	ConfigurableEnvironment environment = prepareEnvironment(listeners,
    			applicationArguments);
    	configureIgnoreBeanInfo(environment);
    	// 6、打印banner
    	Banner printedBanner = printBanner(environment);
    	// 7、为context赋值
    	context = createApplicationContext();
    	exceptionReporters = getSpringFactoriesInstances(
    			SpringBootExceptionReporter.class,
    			new Class[] { ConfigurableApplicationContext.class }, context);
    	// 8、准备好context上下文各种组件,environment,listeners
    	prepareContext(context, environment, listeners, applicationArguments,
    			printedBanner);
    	// 9、刷新上下文
    	refreshContext(context);
    	afterRefresh(context, applicationArguments);
    	// 10、计时器关闭
    	stopWatch.stop();
    	if (this.logStartupInfo) {
    		new StartupInfoLogger(this.mainApplicationClass)
    				.logStarted(getApplicationLog(), stopWatch);
    	}
    	listeners.started(context);
    	// 11、调用runners,后面会讲到
    	callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
    	handleRunFailure(context, ex, exceptionReporters, listeners);
    	throw new IllegalStateException(ex);
    }
    
    try {
    	listeners.running(context);
    }
    catch (Throwable ex) {
    	handleRunFailure(context, ex, exceptionReporters, null);
    	throw new IllegalStateException(ex);
    }
    return context;
}

Третий навык

Таймер использовался на шаге 1StopWatchЭтот инструмент, этот инструмент мы также можем использовать напрямую, обычно мы считаем время выполнения куска кода и метод, который мы будем использоватьSystem.currentTimeMillisДля этого мы также можем использоватьStopWatchвместо,StopWatchЕго сила в том, что он может подсчитывать долю времени, отнимающую каждый период времени, которая примерно выглядит следующим образом:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
    	StopWatch stopWatch = new StopWatch();
    	stopWatch.start("startContext");
    	ConfigurableApplicationContext applicationContext = SpringApplication.run(DemoApplication.class, args);
    	stopWatch.stop();
    	stopWatch.start("printBean");
    	// 获取某个bean
    	System.out.println(applicationContext.getBean(DemoApplication.class));
    	stopWatch.stop();
    	System.err.println(stopWatch.prettyPrint());
    }
}

Код шага 4 перемещается вSpringApplicationпервый413строка, код выглядит следующим образом:

private SpringApplicationRunListeners getRunListeners(String[] args) {
    Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
    return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(
		SpringApplicationRunListener.class, types, this, args));
}

Видно, что springboot еще идетMETA-INF/spring.factoriesНаходитьSpringApplicationRunListenerНастроены классы и старт.

Шаг 5 Создайте модуль Spring Environment по умолчаниюStandardServletEnvironmentСтандартная среда.

Тип контекста, созданный по умолчанию на шаге 7:AnnotationConfigServletWebServerApplicationContext, видно, что это контекст сервлета на основе аннотаций в контексте Spring, поэтому наш первыйDemoApplication.javaАннотации, объявленные в классе@SpringBootApplicationбудут просканированы и проанализированы.

Шаг 9. Обновление контекста является ядром, и вы знаете, что после прочтения исходного кода Spring этотrefresh()Метод очень классический, за подробностями вы можете обратиться к другой статье редактора.Процесс инициализации IOC контейнера Spring

Шаг 11 будет выполнять весь контекст, все реализованоApplicationRunnerа такжеCommandLineRunnerфасоль,SpringApplicationпервый787код показывает, как показано ниже:

private void callRunners(ApplicationContext context, ApplicationArguments args) {
    List<Object> runners = new ArrayList<>();
    runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
    runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
    // 对所有runners进行排序并执行
    AnnotationAwareOrderComparator.sort(runners);
    for (Object runner : new LinkedHashSet<>(runners)) {
    	if (runner instanceof ApplicationRunner) {
    		callRunner((ApplicationRunner) runner, args);
    	}
    	if (runner instanceof CommandLineRunner) {
    		callRunner((CommandLineRunner) runner, args);
    	}
    }
}

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

@Component
public class MyRunner implements ApplicationRunner {

	@Override
	public void run(ApplicationArguments args) throws Exception {
		System.err.println("执行了ApplicationRunner~");
	}

}
@Component
public class MyCommandRunner implements CommandLineRunner {

	@Override
	public void run(String... args) throws Exception {
		System.out.println("执行了commandrunner");
	}

}

будь осторожен:

  • 1. Период выполнения CommandLineRunner и ApplicationRunner выполняется после запуска контейнера Spring.
  • 2. Весь жизненный цикл контейнера выполняется только один раз

2. Какова функция аннотации @EnableAutoConfiguration?

При нормальных обстоятельствах bean-компоненты, объявленные в файле jar, представленном java, не будут сканироваться Spring, так как же наши различные стартеры инициализируют свои собственные bean-компоненты? Ответ вMETA-INF/spring.factoriesзаявление вorg.springframework.boot.autoconfigure.EnableAutoConfiguration=xxx, как вspring-cloud-netflix-zuulСодержимое, заявленное в этом стартере, следующее:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration,\
org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration

Что означает это утверждение? То есть во время процесса запуска Springboot он будетorg.springframework.boot.autoconfigure.EnableAutoConfiguration=xxxОбъявленный экземпляр класса становится bean-компонентом и регистрируется в контейнере Ниже приведен тестовый пример:

мы вmydemoОбъявите bean-компонент в коде следующим образом:

@Service
public class MyUser {

}

существуетdemo, напечатайте bean-компонент MyUser и напечатайте следующим образом:

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'css.demo.user.MyUser' available
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:343)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:335)
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1123)
	at com.example.demo.DemoApplication.main(DemoApplication.java:15)

насmydemoДобавьте эту конфигурацию в проект:

demoПроект печатается следующим образом:

2019-08-02 19:31:34.814  INFO 21984 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2019-08-02 19:31:34.818  INFO 21984 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 2.734 seconds (JVM running for 3.254)
执行了ApplicationRunner~
执行了commandrunner
css.demo.user.MyUser@589b028e

Зачем его настраивать? На самом деле, во время процесса запуска springboot вAutoConfigurationImportSelector#getAutoConfigurationEntryпозвонюgetCandidateConfigurationsметод, исходный код этого метода выглядит следующим образом:

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
			AnnotationAttributes attributes) {
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
            // 此处会去调用EnableAutoConfiguration注解
    		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;
}

getSpringFactoriesLoaderFactoryClassИсходный код метода выглядит следующим образом:

protected Class<?> getSpringFactoriesLoaderFactoryClass() {
	return EnableAutoConfiguration.class;
}

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

3. Резюме

В этой статье анализируется общий процесс запуска Springboot с общего направления.В некоторых местах мы не проводили углубленных исследований, но изучаем исходный код, чтобы впитать суть его кодирования и написать лучший код, а также понять соответствующие принципы, что удобнее и удобнее.Быстро найдите и решите проблему, если что-то не так, пожалуйста, поправьте меня, добро пожаловать, чтобы оставить сообщение в области комментариев, спасибо!