springboot自动装配详解

Springboot自动装配原理解析

1.run方法的解析

首先,程序的主入口,也就是xxxApplication类中main方法下的run方法

SpringApplication.run(AlumniApplication.class, args);

如果我们自动补全对象,会得到,这个run方法的返回值是一个ConfigurableApplicationContext类型的对象

ConfigurableApplicationContext run = SpringApplication.run(AlumniApplication.class, args);

当我们调出ConfigurableApplicationContext类的关系图时,可以发现他有很多级的父级类,上一级是一个叫ApplicationContext的类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C6Iw9JXf-1639019234455)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20211130232429630.png)]

而这个类我们并不陌生,在初学Spring时我们常用这个类加载xxxbean.xml文件,即以通用方式加载文件资源的能力,

那我们便得到合理推测,run方法可能就是通过加载一些配置文件,完成springboot项目的自动装配

为了验证猜想,我们通过逐层进入run方法进行验证,最终我们来到最后的一个run方法

/**
 * 运行应用程序,创建并刷新一个新的应用程序上下文
 *
 * @param args
 * @return
 */
public ConfigurableApplicationContext run(String... args) {
	/**
	 *  StopWatch: 简单的秒表,允许定时的一些任务,公开每个指定任务的总运行时间和运行时间。
	 *  这个对象的设计不是线程安全的,没有使用同步。SpringApplication是在单线程环境下,使用安全。
	 */
	StopWatch stopWatch = new StopWatch();
	// 设置当前启动的时间为系统时间startTimeMillis = System.currentTimeMillis();
	stopWatch.start();
	// 创建一个应用上下文引用
	ConfigurableApplicationContext context = null;
	// 异常收集,报告启动异常
	Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
	/**
	 * 系统设置headless模式(一种缺乏显示设备、键盘或鼠标的环境下,比如服务器),
	 * 通过属性:java.awt.headless=true控制
	 */
	configureHeadlessProperty();
	/*
	 * 获取事件推送监器,负责产生事件,并调用支某类持事件的监听器
	 * 事件推送原理看上面的事件推送原理图
	 */
	SpringApplicationRunListeners listeners = getRunListeners(args);
	/**
	 * 发布一个启动事件(ApplicationStartingEvent),通过上述方法调用支持此事件的监听器
	 */
	listeners.starting();
	try {
		// 提供对用于运行SpringApplication的参数的访问。取默认实现
		ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
		/**
		 * 构建容器环境,这里加载配置文件
		 */
		ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
		// 对环境中一些bean忽略配置
		configureIgnoreBeanInfo(environment);
		// 日志控制台打印设置
		Banner printedBanner = printBanner(environment);
		// 创建容器
		context = createApplicationContext();
		exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context);
		/**
		 * 准备应用程序上下文
		 * 追踪源码prepareContext()进去我们可以发现容器准备阶段做了下面的事情:
		 * 容器设置配置环境,并且监听容器,初始化容器,记录启动日志,
		 * 将给定的singleton对象添加到此工厂的singleton缓存中。
		 * 将bean加载到应用程序上下文中。
		 */
		prepareContext(context, environment, listeners, applicationArguments, printedBanner);
		/**
		 * 刷新上下文
		 * 1、同步刷新,对上下文的bean工厂包括子类的刷新准备使用,初始化此上下文的消息源,注册拦截bean的处理器,检查侦听器bean并注册它们,实例化所有剩余的(非延迟-init)单例。
		 * 2、异步开启一个同步线程去时时监控容器是否被关闭,当关闭此应用程序上下文,销毁其bean工厂中的所有bean。
		 * 。。。底层调refresh方法代码量较多
		 */
		refreshContext(context);
		afterRefresh(context, applicationArguments);
		// stopwatch 的作用就是记录启动消耗的时间,和开始启动的时间等信息记录下来
		stopWatch.stop();
		if (this.logStartupInfo) {
			new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
		}
		// 发布一个已启动的事件
		listeners.started(context);
		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;
}
	public ConfigurableApplicationContext run(String... args) {
		StopWatch stopWatch = new StopWatch();//监听执行时间
		stopWatch.start();//监听开始
		DefaultBootstrapContext bootstrapContext = createBootstrapContext();
		ConfigurableApplicationContext context = null;//申明了一个上下文对象
		configureHeadlessProperty();
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting(bootstrapContext, this.mainApplicationClass);
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
			configureIgnoreBeanInfo(environment);
			Banner printedBanner = printBanner(environment);
			context = createApplicationContext();//创建了上下文对象
			context.setApplicationStartup(this.applicationStartup);
			prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
			refreshContext(context);//刷新容器对象
			afterRefresh(context, applicationArguments);
			stopWatch.stop();//监听结束
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
			listeners.started(context);
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}

小插曲,springboot如何加载application.properties配置文件

这里有个小插曲,顺便分析一下springboot如何加载application.properties配置文件的,在上面的run方法中,有这么一行,作用是构造容器环境,加载配置文件

ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);

进入listeners,其中就包含加载application.properties的类,org.springframework.boot.context.config.ConfigFileApplicationListener

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cXUAqtd5-1639019234457)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20211201093606917.png)]

进入ConfigFileApplicationListener,看一下实现方式

private static final String DEFAULT_NAMES = "application";//默认加载的配置文件名为application

ConfigFileApplicationListener 实现onApplicationEvent方法和supportsEventType方法

@Override
	public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
		return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType)
				|| ApplicationPreparedEvent.class.isAssignableFrom(eventType);
	}

	@Override
	public void onApplicationEvent(ApplicationEvent event) {
		throw new IllegalStateException("ConfigFileApplicationListener [" + getClass().getName()
				+ "] is deprecated and can only be used as an EnvironmentPostProcessor");
	}

ApplicationEnvironmentPreparedEvent类中会找到一个load方法

private void load(String location, String name, Profile profile,
				DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
			if (!StringUtils.hasText(name)) {
				for (PropertySourceLoader loader : this.propertySourceLoaders) {
					if (canLoadFileExtension(loader, location)) {
						load(loader, location, profile,
								filterFactory.getDocumentFilter(profile), consumer);
						return;
					}
				}
			}
			Set<String> processed = new HashSet<>();
			for (PropertySourceLoader loader : this.propertySourceLoaders) {
				for (String fileExtension : loader.getFileExtensions()) {
					if (processed.add(fileExtension)) {
					//加载配置文件,文件路径 //location+name++“."+fileExtension(文件扩展名)
					//profile 是不同环境的配置文件
				
						loadForFileExtension(loader, location + name, "." + fileExtension,
								profile, filterFactory, consumer);
					}
				}
			}

profile代表不同环境 将profile传入loadForFileExtension()方法中如下:

loadForFileExtension(loader, location + name, "." + fileExtension,profile, filterFactory, consumer)

进入方法看一下核心逻辑

if (profile != null) {
				// Try profile-specific file & profile section in profile file (gh-340)
				String profileSpecificFile = prefix + "-" + profile + fileExtension;
				load(loader, profileSpecificFile, profile, defaultFilter, consumer);
				load(loader, profileSpecificFile, profile, profileFilter, consumer);
				// Try profile specific sections in files we've already processed
				for (Profile processedProfile : this.processedProfiles) {
					if (processedProfile != null) {
						String previouslyLoaded = prefix + "-" + processedProfile
								+ fileExtension;
						load(loader, previouslyLoaded, profile, profileFilter, consumer);
					}
				}
			}

对于激活文件也就是不同环境各自的配置文件和默认的文件的优先级
下面的代码根据profile进行判断 profile=null的 addFirst profile!=null 的addLast
很明显,先加载环境配置为空的也就是application.properties文件,在加载激活的环境配置文件 application-xxx.properties文件

public void load() {
			this.profiles = new LinkedList<>();
			this.processedProfiles = new LinkedList<>();
			this.activatedProfiles = false;
			this.loaded = new LinkedHashMap<>();
			initializeProfiles();
			while (!this.profiles.isEmpty()) {
				Profile profile = this.profiles.poll();
				if (profile != null && !profile.isDefaultProfile()) {
					addProfileToEnvironment(profile.getName());
				}
				load(profile, this::getPositiveProfileFilter,
						addToLoaded(MutablePropertySources::addLast, false));
				this.processedProfiles.add(profile);
			}
			resetEnvironmentProfiles(this.processedProfiles);
			load(null, this::getNegativeProfileFilter,
					addToLoaded(MutablePropertySources::addFirst, true));
			addLoadedPropertySources();
		}

关于文件扩展名由此接口PropertySourceLoader 加载

这个接口里有一个方法String[] getFileExtensions();一共有两种实现方式

@Override
	public String[] getFileExtensions() {
		return new String[] { "properties", "xml" };
	}

@Override
	public String[] getFileExtensions() {
		return new String[] { "yml", "yaml" };
	}

所以配置文件的后缀可以有四种不同的方式 加载先后顺序为 properties,xml,yml,yaml 后加载的覆盖先加载

最后具体的路径就是 location + name + “-” + profile + “.” + ext

话归正题

我们摘出run方法关键代码,该方法中申明并创建了了一个上下文对象,并刷新了这个上下文对象最终返回

//申明
ConfigurableApplicationContext context = null;
//创建
context = createApplicationContext();
//刷新
refreshContext(context);
//返回
return context;

重点进入refresh方法,可以很清晰的看到,这里面执行了一些初始化方法,将创建的实例交由bean factory管理(即spring的ioc思想),但是没有我们想看到的与自动装配相关的方法

	@Override
	public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");

			// Prepare this context for refreshing.
			prepareRefresh();

			// Tell the subclass to refresh the internal bean factory.
			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

			// Prepare the bean factory for use in this context.
			prepareBeanFactory(beanFactory);

			try {
				// Allows post-processing of the bean factory in context subclasses.
				postProcessBeanFactory(beanFactory);

				StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
				// Invoke factory processors registered as beans in the context.
				invokeBeanFactoryPostProcessors(beanFactory);

				// Register bean processors that intercept bean creation.
				registerBeanPostProcessors(beanFactory);
				beanPostProcess.end();

				// Initialize message source for this context.
				initMessageSource();

				// Initialize event multicaster for this context.
				initApplicationEventMulticaster();

				// Initialize other special beans in specific context subclasses.
				onRefresh();

				// Check for listener beans and register them.
				registerListeners();

				// Instantiate all remaining (non-lazy-init) singletons.
				finishBeanFactoryInitialization(beanFactory);

				// Last step: publish corresponding event.
				finishRefresh();
			}

			catch (BeansException ex) {
				if (logger.isWarnEnabled()) {
					logger.warn("Exception encountered during context initialization - " +
							"cancelling refresh attempt: " + ex);
				}

				// Destroy already created singletons to avoid dangling resources.
				destroyBeans();

				// Reset 'active' flag.
				cancelRefresh(ex);

				// Propagate exception to caller.
				throw ex;
			}

			finally {
				// Reset common introspection caches in Spring's core, since we
				// might not ever need metadata for singleton beans anymore...
				resetCommonCaches();
				contextRefresh.end();
			}

总结,run方法进行的是spring ioc的初始化

2.@SpringBootApplication的解析

第1步中我们并没有得到自动装配相关的线索

我们将目光转移向启动类上的注解@SpringBootApplication,进入@SpringBootApplication注解,会发现他还有7个注解,前四个为元注解,所以我们重点看后三个

@Target(ElementType.TYPE)//规定注解可以使用在类,接口上
@Retention(RetentionPolicy.RUNTIME)//规定注解在程序运行时生效
@Documented//注解若被修饰会被注册到Api文档中
@Inherited//@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

@ComponentScan注解的作用为扫描包,若未指定扫描路径,则会扫描当前注解所修饰的类所在的包下所有被@Component注解修饰的类.

@SpringBootConfiguration注解下依然有多个注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration//此注解标记的类为java配置类
@Indexed//为@ComponentScan提升性能

@Indexed 解析

在项目中使用了@Indexed之后,编译打包的时候会在项目中自动生成META-INT/spring.components文件。

当Spring应用上下文执行ComponentScan扫描时,META-INT/spring.components将会被CandidateComponentsIndexLoader 读取并加载,转换为CandidateComponentsIndex对象,这样的话@ComponentScan不在扫描指定的package,而是读取CandidateComponentsIndex对象,从而达到提升性能的目的。

简单点说,使用了@Indexed注解后,原来@ComponentScan可能需要扫描非常多的类,现在只需要在启动后对spring.components进行一次io,将类路径写入文件,后续将只需要读取文件即可获得所有类路径

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qtQbIG3c-1639019234458)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20211201002333837.png)]

@EnableAutoConfiguration解析

进入@EnableAutoConfiguration注解,又有以下组合注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)

照例,前四个不管,我们进入@AutoConfigurationPackage注解,发现都有一个叫做@Import的注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)

@Import注解解析

  1. @Import注解初级使用(静态使用)
@Import(value = {User.class,Student.class})//将类注入spring容器
  1. @Import,当注入的类实现了ImportSelector接口,就不会将该类型注入到容器中,而是会注入selectImports方法返回的信息对应的对象(动态使用)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jtZuA9Tt-1639019234459)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20211201004513944.png)]

  1. @Import,当注入的类实现了ImportBeanDefinitionRegistrar接口,就不会将该类型注入到容器中,而是会将对象的注册器传递给抽象方法(动态使用)

自动装配核心代码

了解了@Import注解,我们进入AutoConfigurationImportSelector类,AutoConfigurationImportSelector类实现了DeferredImportSelector接口,如上述,我们需要找selectImports方法

@Override
	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}

进入getAutoConfigurationEntry,有一行为候选的配置信息

List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);

进入getCandidateConfigurations

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;
	}

进入loadFactoryNames,一步步进入关键路径,得到最终访问的文件"META-INF/spring.factories",也就是说getCandidateConfigurations就是加载所有的META-INF/spring.factories文件

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);

debug打上断点以后,就能清楚地看到spring.factories文件中加载了多少个Java类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sFPmQCvD-1639019234460)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20211201020454130.png)]

我们找到这个文件,内容完全对应,文件中是加载到内存中的java类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gZ14kRrb-1639019234461)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20211201021949806.png)]

而spring.factories文件其实有多个,如下,mybatis-plus-starter中同样也有这个spring.factories

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ULdColn4-1639019234462)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20211201022611876.png)]

进入com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration,能够轻松找到mybatis的SqlSessionFactory核心类,也就是说,spring将核心类注入了spring容器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h1m3Oqqw-1639019234463)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20211201022755262.png)]

顺便对getAutoConfigurationEntry方法做一下解析

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return EMPTY_ENTRY;
		}
		AnnotationAttributes attributes = getAttributes(annotationMetadata);
		List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
		configurations = removeDuplicates(configurations);//去除重复的
		Set<String> exclusions = getExclusions(annotationMetadata, attributes);
		checkExcludedClasses(configurations, exclusions);
		configurations.removeAll(exclusions);
    //根据META-INF/spring-autoconfigure-metadata.properties中的条件进行过滤,如果能找到对应的class,则加载,否则,过滤
		configurations = getConfigurationClassFilter().filter(configurations);
		fireAutoConfigurationImportEvents(configurations, exclusions);
		return new AutoConfigurationEntry(configurations, exclusions);
	}
protected static final String PATH = "META-INF/spring-autoconfigure-metadata.properties";

3.springboot的意义

Spring Boot 对开发效率的提升是全方位的,我们可以简单做一下对比:

在没有使用 Spring Boot 之前我们开发一个 web 项目需要做哪些工作:

1)配置 web.xml,加载 Spring 和 Spring mvc
2)配置数据库连接、配置 Spring 事务
3)配置加载配置文件的读取,开启注解
4)配置日志文件
n) 配置完成之后部署 tomcat 调试

而springboot可以直接下载组件,通过application.properties直接进行统一的配置

自带springboot-test,测试简单

部署更加便捷,自带maven容器,一键打jar包或者war包即可进行部署

springboot启动内嵌tomcat过程详解

讲的特别好,也顺带把整个springboot自动装配讲完了

SpringBoot启动tomcat源码解读 - darendu - 博客园 (cnblogs.com)

这个讲解的顺序最好

springboot的启动时的一个自动装配过程 - IT-QI - 博客园 (cnblogs.com)

在没有使用 Spring Boot 之前我们开发一个 web 项目需要做哪些工作:

1)配置 web.xml,加载 Spring 和 Spring mvc
2)配置数据库连接、配置 Spring 事务
3)配置加载配置文件的读取,开启注解
4)配置日志文件
n) 配置完成之后部署 tomcat 调试

而springboot可以直接下载组件,通过application.properties直接进行统一的配置

自带springboot-test,测试简单

部署更加便捷,自带maven容器,一键打jar包或者war包即可进行部署

springboot启动内嵌tomcat过程详解

讲的特别好,也顺带把整个springboot自动装配讲完了

SpringBoot启动tomcat源码解读 - darendu - 博客园 (cnblogs.com)

这个讲解的顺序最好

springboot的启动时的一个自动装配过程 - IT-QI - 博客园 (cnblogs.com)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Acerola-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值