前言

平时的项目中一直用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()方法:

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

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

1
2
3
4
5
6
7
8
9
10
11
@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注解的源码如下:

1
2
3
4
5
6
7
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
...
}

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

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

@EnableAutoConfiguration

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

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

1
2
3
4
5
6
7
8
9
10
11
@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注解的源码,如下:

1
2
3
4
5
6
7
8
@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方法:

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

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

@Import(AutoConfigurationImportSelector.class)

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

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

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

分析getAutoConfigurationEntry方法源码如下:

里面有一个getCandidateConfigurations方法:

里面有个loadFactoryNames方法。

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

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

getSpringFactoriesLoaderFactoryClass()返回的是EnableAutoConfiguration.class:

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

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

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

继续点开loadFactoryNames方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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里面的值:

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

查看这个文件的路径:

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

@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的注解的功能,就是几个组合注解: