啥?你连SpringBoot自动装配的原理是啥都不知道???

前言

平时的项目中一直用springboot用的挺溜,但对其中的奥妙知之甚少,今天来研究研究SpringBoot的源码,探究下其中的奥秘。

SpringBoot的两大特性

先来了解一个概念:约定优于配置

约定优于配置:又称按约定编程,是一种软件设计范式。

本质上是说,系统、类库或框架应该假定合理的默认值,而非提供不必要的配置。比如说模型中有个叫User的类,那么数据库中对应的表就会默认命名为user,只有在偏离这一约定的时候,例如要将该表命名为person,才需要写这个名字相关的配置。

简单理解,约定优于配置就是遵循约定

在约定优于配置的思想下,springboot有两大特性:①起步依赖、②自动配置。

起步依赖

起步依赖本质上是一个maven项目对象模型(Poject Object Model,POM),定义了对其他库的传递依赖,这些东西加在一起即支持某些功能。

简单来说,起步依赖就是将具备某种功能的坐标打包到一起,并提供一些默认的功能。

自动配置

自动配置指的是,我们在添加jar包依赖的时候,自动为我们配置一些组件的相关配置,我们无需配置或者只需少量的配置就能运行编写。

springboot的自动配置,指的是springboot会自动将一些配置类的bean注册进IOC容器,我们可以在需要的地方使用@Autowired或者@Resource等注解来直接使用她们。

自动”的表现形式就是我们只需要引入我们想要的功能的包,相关的配置我们完全不用管,springboot会自动注入这些配置bean,我们直接使用这些bean即可。

Springboot自动配置的流程

我们了解了自动配置的概念之后,不禁要问,springboot是如何进行自动配置的呢?她都把哪些组件进行了自动配置?

要回答好这个问题,我们必须从springboot的源码开始分析。

Springboot的启动入口是@SpringBootApplication注解标注类中的main()方法:

image-20210705103354936

@SpringBootApplication注解能够扫描Spring组件并自动配置Springboot。下面我们对@SpringBootApplication注解的内部源码进行分析。

@SpringBootApplication注解的源码定义如下:

@Target(ElementType.TYPE)   //注解的适用范围,type表示可以描述在类、接口、注解或枚举上
@Retention(RetentionPolicy.RUNTIME) //注解的生命周期,Runtime运行时
@Documented       //注解可以记录在javadoc中
@Inherited  //注解可以被子类继承
@SpringBootConfiguration  //标明该类为配置类
@EnableAutoConfiguration   //启动自动配置功能
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })  //包扫描器
public @interface SpringBootApplication {
    ...
}

该注解是一个组合注解,前面4个是注解的元数据信息,我们主要看后面三个:@SpringBootConfiguration 、@EnableAutoConfiguration 、@ComponentScan。

@SpringBootConfiguration

@SpringBootConfiguration注解的源码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
    ...
}

可以看到@SpringBootConfiguration注解的核心注解@Configuration,该注解我们很熟悉了,她是spring提供的,表示当前类是一个配置类(XML配置文件的注解表现形式),并可以被组件扫描器扫描。

可以得出结论,@SpringBootConfiguration注解的作用和@Configuration注解相同,都是标识一个可以被组件扫描器扫描的配置类,只不过@SpringBootConfiguration是被springboot重新封装命名了而已。

@EnableAutoConfiguration

@EnableAutoConfiguration注解表示开启自动配置功能,该注解是springboot框架最重要的注解,也是实现自动化配置的注解。

@EnableAutoConfiguration注解的核心源码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage      //自动配置包
@Import(AutoConfigurationImportSelector.class)  //自动配置类扫描导入
public @interface EnableAutoConfiguration {
  String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
	Class<?>[] exclude() default {};
	String[] excludeName() default {};
}

可以发现她也是一个组合注解,Spring中有很多以Enable开头的注解,其作用就是借助@Import来收集并注册特定场景相关的bean,并加载到IOC容器。@EnableAutoConfiguration就是借助@Import来收集所有符合自动配置条件的bean定义,并加载到IOC容器。

下面对这两个核心注解进行分析。

@AutoConfigurationPackage

查看@AutoConfigurationPackage注解的源码,如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class) //导入Registrar中注册的组件
public @interface AutoConfigurationPackage {

}

可以看到@AutoConfigurationPackage注解的功能是由@Import注解实现的,它是spring框架的底层注解,它的作用就是给容器导入某个组件类,例如@Import(AutoConfigurationPackages.Registrar.class),就是将Registrar这个组件类导入到容器中。查看Registrar类中的registerBeanDefinitions方法:

image-20210705110525204

我们用debug模式启动,可以看到选中的部分就是我们启动类所在的包路径。

由此,我们可以知道,@AutoConfigurationPackage注解的作用就是将主程序类所在包及所有子包下的组件都扫描到spring容器中去。因此在定义项目包结构时,要求包结构的定义要规范,项目主程序启动类要定义在最外层的根目录位置,然后在根目录位置内部建立子包和类进行业务开发,这样才能够保证定义的类能够被组件扫描器扫描到。

@Import(AutoConfigurationImportSelector.class)

这个@Import是将AutoConfigurationImportSelector这个类导入到spring容器中去,AutoConfigurationImportSelector可以帮助springboot应用将所有符合条件的@Configuration配置都加载到当前的Springboot创建并使用的IOC容器(ApplicationContext)中去。

分析AutoConfigurationImportSelector的源码,其中有一个selectImports方法:

image-20210705125833565

这个方法是告诉springboot需要导入哪些组件。

分析getAutoConfigurationEntry方法源码如下:

image-20210705130144530

里面有一个getCandidateConfigurations方法:

image-20210705130319536

里面有个loadFactoryNames方法。

	List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
				getBeanClassLoader());

这个方法需要传入两个参数getSpringFactoriesLoaderFactoryClass()和getBeanClassLoader(),

getSpringFactoriesLoaderFactoryClass()返回的是EnableAutoConfiguration.class:

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

getBeanClassLoader()返回的是类加载器:

protected ClassLoader getBeanClassLoader() {
		return this.beanClassLoader;
	}

继续点开loadFactoryNames方法:

public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
		String factoryTypeName = factoryType.getName();
		return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
	}

	private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
		MultiValueMap<String, String> result = cache.get(classLoader);
		if (result != null) {
			return result;
		}

		try {
            //如果类加载器不为null,则加载类路径下spring.factories文件,将其中设置的配置类的全路径信息封装为Enumeration类对象
			Enumeration<URL> urls = (classLoader != null ?
					classLoader.getResources("META-INF/spring.factories") :
					ClassLoader.getSystemResources("META-INF/spring.factories"));
			result = new LinkedMultiValueMap<>();
            
           //循环Enumeration类对象,根据对应的节点信息生成Properties对象,通过传入的键获取值,在将值切割为一个个小的字符串转换为Array,方法result集合中
			while (urls.hasMoreElements()) {
				URL url = urls.nextElement();
				UrlResource resource = new UrlResource(url);
				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
				for (Map.Entry<?, ?> entry : properties.entrySet()) {
					String factoryTypeName = ((String) entry.getKey()).trim();
					for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
						result.add(factoryTypeName, factoryImplementationName.trim());
					}
				}
			}
			cache.put(classLoader, result);
			return result;
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load factories from location [" +
					FACTORIES_RESOURCE_LOCATION + "]", ex);
		}
	}

debug跟踪了一下,看了下result里面的值:

image-20210705131745720

中间有一步加载类路径下spring.factories文件,这个文件是在哪儿呢?如下图,是在META-INF/spring.factories路径下:

image-20210705132236161

查看这个文件的路径:

image-20210705132033038

spring.factories文件中配置的自动配置类的全路径:

image-20210705132432752

@EnableAutoConfiguration就是从classpath中搜寻META-INF/spring.factories配置文件,并将其中org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的配置项通过反射(Java Reflection)实例化为对应的标注了@Configuration的JavaConfig形式的配置类,并加载到IOC容器中。

例如:我们在项目中添加了Web环境依赖启动器,对应的WebMvcAutoConfiguration自动配置类就会生效,在该自动配置类里面通过全注解配置的方式对SpringMVC运行所需要的环境进行了默认配置,包括默认前缀、默认后缀、视图解析器、MVC校验器等。这些自动配置类的本质是传统SpringMVC框架对应的XML配置文件,只不过在SpringBoot中以自动配置类的形式进行了预先配置。因此,在SpringBoot项目中加入了相关依赖启动器之后,基本上不需要任何配置就可以运行程序。当然,我们也可以对这些自动配置类中的默认配置进行更改。

总结-自动配置的步骤:

springboot底层实现自动配置的步骤是:

  1. SpringBoot应用启动;
  2. @SpringBootApplication起作用;
  3. @EnableAutoConfiguration;
  4. @AutoConfigurationPackage:这个组合注解主要是@Import(AutoConfigurationPackages.Registrar.class),她通过将Registrar类导入到容器中,Registrar类的主要作用是扫描主配置类同级目录及子包,并将相应的组件导入到Springboot创建管理的容器中去;
  5. @Import(AutoConfigurationImportSelector.class):通过将AutoConfigurationImportSelector类导入到容器中,AutoConfigurationImportSelector类的主要作用是通过selectImports方法执行过程中,会使用内部工具类SpringFactoriesLoader,查找classpath上所有jar包中的的META/spring.factories进行加载,实现将配置类信息交给SpringFactory加载器进行一系列的容器创建过程。

@ComponentScan

@ComponentScan注解的作用是扫描包路径,并将对应的类加载到容器中。具体扫描的包路径由SpringBoot项目的主程序启动类的位置所决定,而主启动类的位置是由前面的@AutoConfigurationPackage注解进行解析得到的。

对于@ComponentScan和@AutoConfigurationPackage的关系,可以这样理解,虽然@ComponentScan开启了包的扫描,但是她没有指定扫描的package,至于要扫描哪些packages,是由@EnableAutoConfiguration包含的@AutoConfigurationPackage决定的。

总结

@SpringBootApplication的注解的功能,就是几个组合注解:

image-20210705140119067

微信公众号

程序员资料站(微信中搜索:code_data)
纯技术分享,欢迎关注。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

程序员资料站

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

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

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

打赏作者

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

抵扣说明:

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

余额充值