知其然 知其所以然
创作不易 求点赞👍 求关注❤️ 求分享👥
絮叨
本文是SpringBoot系列的第二篇文章,本篇主要是对第一篇的HelloWorld程序的解析。也会涉及到SpringBoot的一些注解原理的讲解。
正文
为什么SpringBoot可以使用这么少的配置,只是写个主启动类,在pom文件导入一个springboot依赖,就能启动一个Web项目呢?为什么会这么神奇呢?接下来我们就基于第一篇的HelloWorld程序去分析下SpringBoot为什么可以这么神奇。
在分析之前,先普及一个名词starter(翻译为启动器,开胃小吃)。它可以说是SpringBoot中一个接触最多的一个名词,可以认为starter是一种服务的整合——使得使用某个功能的开发者不需要关注各种依赖库的处理,不需要具体的配置信息,由Spring Boot自动通过classpath路径下的类发现需要的Bean,并织入bean。
POM解析
首先呢,我们分析一下pom文件。在pom文件中我们导入了一个父项目并且依赖了一个SpringBoot-web的starter。
- 父项目spring-boot-starter-parent
点进父项目,我们发现父项目又依赖了一个叫做spring-boot-dependencies的项目。<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> </parent>
点进这个项目,可以看到它定义了每一个依赖的版本。spring-boot-dependencies这个项目的作用就是管理SpringBoot当前版本的所有依赖的版本。 所以以后我们再导入其他依赖后,只要在SpringBoot的依赖中(基本我们用到的主流依赖都在SpringBoot中集成了),就不需要指定版本号了,它会随着SpringBoot的版本号,自动导入其对应的版本号。如果没有在SpringBoot依赖中的管理,就需要我们自己去指定对应的版本号。<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.2.5.RELEASE</version> <relativePath>../../spring-boot-dependencies</relativePath> </parent>
- 导入的web依赖
我们可以看到,在这里我们并没有指定版本号,因为它在父项目spring-boot-dependencies中声明了版本号,就不需要我们自己去指定了。<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
接下来我们看下spring-boot-starter-web,首先我们可以给他看成两部分,spring-boot-starter和web。- spring-boot-starter它是SpringBoot的场景启动器,每种框架都有自己的启动器,它很好的降低了使用框架时的复杂度。
- web标示着它是web的场景启动器,它会自动导入我们web项目需要用到的依赖。
Spring Boot将所有的功能场景都抽取出来,做成一个个的starters(启动器),我们要用什么功能,就导入对应的starter就可以了,Springboot会自动帮我们导入对应的依赖。
主启动类解析
- @SpringBootApplication标注在类上,标明这是一个主配置类,告诉程序这是一个SpringBoot应用。这是一个SpringBoot的约定(SpringBoot的核心理念就是约定优于配置),只要是标注了这个注解,SpringBoot就要运行这个类下面的main方法去启动这个SpringBoot应用。
下面我们看一下这个注解都干了什么:
我们可以看到@SpringBootApplication是一个组合注解,包括@SpringBootConfiguration,@EnableAutoConfiguration和@ComponentScan@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan( excludeFilters = {@Filter( type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class} ), @Filter( type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class} )} ) public @interface SpringBootApplication {
-
@SpringBootConfiguration标注当前类是一个SpringBoot配置类。就像之前我们在xml中去配置一样,只不过在这里我们用类去代替xml文件。
@SpringBootConfiguration底层被@Configuration注解标注,表明这是一个配置类,它会被@ComponentScan扫描到,也可以说被@SpringBootConfiguration标注的类是容器中的一个组件。@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration public @interface SpringBootConfiguration {
-
@EnableAutoConfiguration开启自动配置。SpringBoot根据我们添加的jar包来配置项目的默认配置。以前我们导入jar要自己配置一些属性,而现在SpringBoot帮我们把这些配置好了。
那SpringBoot是怎么帮我们去配置的呢,请大家继续往下看@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import({AutoConfigurationImportSelector.class}) public @interface EnableAutoConfiguration {
- @AutoConfigurationPackage:自动配置包。
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import({Registrar.class}) public @interface AutoConfigurationPackage {
- @Import({Registrar.class}):Spring的底层注解@Import,给容器中导入一个Registrar.class组件。该组件将主配置类(@SpringBootApplication标注的类)的所在包及下面所有子包里面的所有组件扫描到Spring容器。
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports{ Registrar() { } public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { // registry就是保存要注册到容器中的组件的Bean定义.也就是保存主配置类的所在包及下面所有子包里面的所有Bean定义和一些默认的组件。 // (new AutoConfigurationPackages.PackageImport(metadata)).getPackageName()就是主配置类的所在包及下面所有子包的路径 AutoConfigurationPackages.register(registry, (new AutoConfigurationPackages.PackageImport(metadata)).getPackageName()); } public Set<Object> determineImports(AnnotationMetadata metadata) { return Collections.singleton(new AutoConfigurationPackages.PackageImport(metadata)); } }
所以这也就是为什么主启动类要写在包外面的原因:因为@SpringBootApplication只会扫描@SpringBootApplication注解标记类包下及其子包的类(特定注解标记,比如说@Controller,@Service,@Component,@Configuration和@Bean注解等等)纳入到spring容器,如果我们定义的Bean不在@SpringBootApplication注解标记类相同包下及其子包的类,所以需要我们去配置一下扫包路径。
- @Import({Registrar.class}):Spring的底层注解@Import,给容器中导入一个Registrar.class组件。该组件将主配置类(@SpringBootApplication标注的类)的所在包及下面所有子包里面的所有组件扫描到Spring容器。
- @Import({AutoConfigurationImportSelector.class}):Spring的底层注解@Import,给容器中导入一个AutoConfigurationImportSelector.class组件。该组件是我们要导入哪些组件的选择器。它会将所有需要导入的组件以全类名的方式存到一个AutoConfigurationImportSelector.AutoConfigurationEntry类型的内部类中返回。之后这些组件就会被添加到spring容器中。
我们在AutoConfigurationImportSelector.getAutoConfigurationEntry()方法上添加一个断点并以debug方式启动主启动类。package org.springframework.boot.autoconfigure; public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered { protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) { // annotationMetadata是注解的元信息,包括注解的类的全路径名 if (!this.isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } else { AnnotationAttributes attributes = this.getAttributes(annotationMetadata); // 根据标注类的元信息和属性得到一个configurations的List集合,这个configurations保存的就是我们在容器中要导入的组件。 List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes); // 接下来都是对configurations的一些处理,比如去重、去掉一些不包括的组件等等,最终返回一个AutoConfigurationImportSelector.AutoConfigurationEntry类型的对象。 configurations = this.removeDuplicates(configurations); Set<String> exclusions = this.getExclusions(annotationMetadata, attributes); this.checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = this.filter(configurations, autoConfigurationMetadata); this.fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions); } } }
也就是说AutoConfigurationImportSelector给容器中导入非常多的自动配置类,就是给容器中导入这个场景需要的所有组件,并配置好这些组件。我们也可以看到他的所有自动配置都是叫XxxxAutoConfiguration,这也是SpringBoot的一个约定,我的自动配置类都是叫XxxxAutoConfiguration。以后我们要是找某场景的自动配置,就可以搜XxxxxAutoConfiguration就可以找到了。
由上面代码可以知道最重要的就是这一行,之后都是对这个结果进行处理的。接下来我们看看这个方法到底做了什么。List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes); protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { // 这个configurations实际由这个方法得到,这个方法有两个参数,第一个参数是EnableAutoConfiguration对象,第二个参数是类加载器。 List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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; }
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) { String factoryTypeName = factoryType.getName(); // 调用loadSpringFactories()方法,参数是classLoader。 // 后面的getOrDefault()方法是把loadSpringFactories()方法的返回结果进行判断, // 如果Map集合中有这个factoryTypeName指定的key时,就使用这个key值,返回key对应的value。 // 如果没有就返回默认值Collections.emptyList() // 把结果转换成List返回。 return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList()); }
由此,我们可以知道,SpringFactoriesLoader.loadFactoryNames()方法得作用是从类路径或系统资源路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值,将这些值作为自动配置类导入到容器中。private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader); if (result != null) { return result; } else { try { // 从类路径或系统资源路径下的META-INF/spring.factories中获取资源 Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories"); LinkedMultiValueMap result = new LinkedMultiValueMap(); while(urls.hasMoreElements()) { // 循环上面获取到的资源,转化成Properties资源 URL url = (URL)urls.nextElement(); UrlResource resource = new UrlResource(url); Properties properties = PropertiesLoaderUtils.loadProperties(resource); // 转化成Set集合遍历 Iterator var6 = properties.entrySet().iterator(); while(var6.hasNext()) { // 得到key和value,对value做一些处理后,把key和value保存到LinkedMultiValueMap对象中返回 Entry<?, ?> entry = (Entry)var6.next(); String factoryTypeName = ((String)entry.getKey()).trim(); String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue()); int var10 = var9.length; for(int var11 = 0; var11 < var10; ++var11) { String factoryImplementationName = var9[var11]; result.add(factoryTypeName, factoryImplementationName.trim()); } } } cache.put(classLoader, result); return result; } catch (IOException var13) { throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13); } } }
那它导入了什么呢?我们去看一下。在spring.factories中配置的这些XxxxAutoConfiguration自动配置类正好是我们在上面debug中看到的那些。
那自动配置帮我们配置了什么呢?我们看以下WebMvcAutoConfiguration。它帮我们自动配置了视图解析器。如果没有自动配置,我们都要自己去配置视图解析器。还给我们自动配置了很多的Bean。大家可以自己看一下。
总结一下:Spring Boot在启动的时候从类路径下或系统资源路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值,将这些值作为自动配置类导入到容器中,自动配置类就生效,帮我们进行自动配置工作。以前我们需要自己配置的东西,自动配置类都帮我们。为什么SpringBoot会有那么神奇的效果,会需要那么少的配置,就是因为导入的这些配置类会自动帮我们配置。
- @AutoConfigurationPackage:自动配置包。
-
@ComponentScan扫描当前包及其子包下被@Component,@Controller,@Service,@Repository注解标记的类并纳入到spring容器中进行管理。
-
总结
本篇我们基于第一篇的HelloWorld程序对SpringBoot可以简单方便快速的搭建一个Web项目的解析。
从pom文件和主启动类进行了分析。pom文件中我们依赖SpringBoot的starters(启动器),只需要在项目里面引入这些starter相关场景的所有依赖都会导入进来。
主启动类我们基于@SpringBootApplication注解进行分析,因为包含@SpringBootConfiguration注解,所以这是一个配置类,它会被@ComponentScan扫描到。因为包含@EnableAutoConfiguration注解,所以会把主配置类的所在包及下面所有子包里面的所有组件扫描到Spring容器。所以SpringBoot在启动的时候从类路径下或系统资源路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的配置类,将这些值作为自动配置类导入到容器中,我们就不需要像以前一样去配置。因为包含@ComponentScan,所以它会扫描当前包及其子包下被@Component,@Controller,@Service,@Repository注解标记的类并纳入到spring容器中进行管理。
想要了解SpringBoot的更多细节,请看下回分解。
如果本篇博客有任何错误,请批评指教,不胜感激 !