学到长痘之 - Spring Boot
努力学习,成为一个吃喝不愁的人。
1 Spring 优缺点分析
优点:AOP + IOC
缺点:Spring 的代码是轻量的,基本版本大于 2 MB,但 Spring 的配置繁多,依赖管理耗时耗力,一旦选错依赖版本,不兼容问题就会找上门。
2 Spring Boot 横空出世
Spring Boot 是 Spring 开源组织下的子项目,它对 Spring 的缺点进行了改善与优化,基于约定优于配置的思想,简化了配置,让开发人员专注于业务逻辑的代码编写。
2.1 约定优于配置
约定优于配置简单来理解,就是遵循约定。比如说项目有一个名为 User 的类,那么数据库中对应的表就会默认命名 user。
具体体现
(1)默认将主程序类所在包及所有子包下的组件扫描到 spring 容器中;
(2)使用 Spring Initializr 方式构建 Spring Boot 项目时,会在 resource 目录下自动生成一个空的 application.properties 文件,Spring Boot项目启动时会自动加载 application.properties 文件;
(3)@ComponentScan注解默认扫描 Spring Boot 项目主程序启动类所在的包路径;
(4)spring-boot-starter-web 中默认包含 spring-mvc 相关依赖以及内置的 tomcat 容器等,使得在开发阶段可以直接通过 main 方法或是 jar 包独立运行一个 web 项目。
2.2 起步依赖
将具备某些功能的坐标打包到一起,并提供一些默认的功能,如下图的 spring-boot-starter-web
2.3 自动配置
Spring Boot的自动配置,指的是 Spring Boot 会自动将一些配置类的 Bean 注册进 IoC 容器,我们可以需要的地方使用 @autowired 或者 @resource 等注解来使用它。 “自动”的表现形式就是我们只需要引我们想用功能的包,相关的配置可以不管,直接使用即可。
3 Spring Boot 源码剖析
带着问题去看源码,效果更好哦。
3.1 依赖管理
问题 1:为什么导入 dependency 时不需要指定版本?
在Spring Boot入门程序中,项目pom.xml文件有两个核心依赖,分别是spring-boot-starter-parent和spring-boot-starter-web,具体介绍如下:
(1) spring-boot-starter-parent 依赖
<!-- Spring Boot 父项目依赖管理 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent<11./artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/>
</parent>
----------------------------------------------------------
<!-- 文件加载顺序 -->
<resource>
<filtering>true</filtering>
<directory>${basedir}/src/main/resources</directory>
<includes>
<include>**/application*.yml</include>
<include>**/application*.yaml</include>
<include>**/application*.properties</include>
</includes>
spring-boot-starter-parent 依赖作为 Spring Boot 项目的统一父项目依赖管理,并将项目版本号统一为 2.2.2.RELEASE,该版本号可以修改 。使用“Ctrl+鼠标左键”进入并查看 spring-boot-starter-parent 底层源文件,发现 spring-boot-starter-parent 有一个父依赖 spring-boot-dependencies
(2)spring-boot-dependencies
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.0.RELEASE</version>
<relativePath>../../spring-boot-dependencies</relativePath>
</parent>
------------------------------------------------------------------
<!-- 版本号统一管理 -->
<properties>
<activemq.version>5.15.11</activemq.version>
...
<solr.version>8.2.0</solr.version>
<mysql.version>8.0.18</mysql.version>
<kafka.version>2.3.1</kafka.version>
<spring-amqp.version>2.2.2.RELEASE</spring-amqp.version>
<spring-restdocs.version>2.0.4.RELEASE</spring-restdocs.version>
<spring-retry.version>1.2.4.RELEASE</spring-retry.version>
<spring-security.version>5.2.1.RELEASE</spring-security.version>
<spring-session-bom.version>Corn-RELEASE</spring-session-bom.version>
<tomcat.version>9.0.29</tomcat.version>
<thymeleaf.version>3.0.11.RELEASE</thymeleaf.version>
...
</properties>
该文件通过标签对一些常用技术框架的依赖文件进行了版本号统一管理,例如 activemq、spring、tomcat 等,它们的版本都与 Spring Boot 的版本相匹配,所以 pom.xml 引入依赖文件不需要标注版本号。 如果 pom.xml 引入的依赖文件不是 spring-boot-starter-parent 管理的,那么在 pom.xml 引入依赖文件时,需要使用标签指定依赖文件的版本号。
问题 2:spring-boot-starter-parent 父依赖启动器的主要作用是进行版本统一管理,那么项目运行依赖的JAR包是从何而来的?
通过依赖其他的启动器
(3) spring-boot-starter-web依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.2.2.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.2.2.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.2.2.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.2.2.RELEASE</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<artifactId>tomcat-embed-el</artifactId>
<groupId>org.apache.tomcat.embed</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.2.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.2.RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
spring-boot-starter-web 依赖启动器的主要作用是提供 Web 开发场景所需的底层所有依赖。因此在 pom.xml 中引入 spring-boot-starter-web 依赖启动器时,就可以实现 Web 场景开发,而不需要额外导入 Tomcat 服务器以及其他 Web 依赖文件。当然,这些引入的依赖文件的版本号还是由 spring-boot-starter-parent 父依赖进行统一管理。
Spring Boot 除了提供有上述介绍的 Web 依赖启动器,还提供了其他许多开发场景的相关依赖, 打开Spring Boot 官方文档,搜索 “Starters” 即可。
3.2 自动配置
概念:在我们添加 jar 包依赖时,自动为我们配置一些组件的相关配置,我们无需配置或者只需要少量配置就能运行编写的项目
问题:Spring Boot到底是如何进行自动配置的,都把哪些组件进行了自动配置?
@SpringBootApplication
@Target({ElementType.TYPE}) //注解的适用范围,Type表示注解可以描述在类、接口、注解或枚举中
@Retention(RetentionPolicy.RUNTIME) //表示注解的生命周期,Runtime运行时
@Documented //表示注解可以记录在javadoc中
@Inherited //表示可以被子类继承该注解
@SpringBootConfiguration // 标明该类为配置类
@EnableAutoConfiguration // 启动自动配置功能
@ComponentScan // 包扫描器 <context:component-scan base-package="com.xxx.xxx"/>
public @interface SpringBootApplication {
......
}
@SpringBootApplication注解是一个组合注解,前面 4 个是注解的元数据信息, 主要看后面 3 个注解:@SpringBootConfiguration、@EnableAutoConfiguration、 @ComponentScan
(1)@SpringBootConfiguration
@SpringBootConfiguration 表示当前类为一个配置类,并可以被组件扫描器扫描。由此可见,@SpringBootConfiguration注解的作用与@Configuration 注解相同,只不过 @SpringBootConfiguration 是被 Spring Boot 进行了重新封装命名而已。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration //配置IOC容器
public @interface SpringBootConfiguration {}
(2)@EnableAutoConfiguration
@EnableAutoConfiguration 注解表示开启自动配置功能,是 Spring Boot 框架最重要的注解。
@AutoConfigurationPackage //自动配置包 : 会把@springbootApplication注解标注的类所在包名拿到,并且对该包及其子包进行扫描,将组件添加到容器中
@Import(AutoConfigurationImportSelector.class) //可以帮助springboot应用将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器(ApplicationContext)中
public @interface EnableAutoConfiguration {}
可以发现它是一个组合注解,Spring 中有很多以 Enable 开头的注解,其作用就是借助 @Import 来收集并注册特定场景相关的bean,并加载到 IoC 容器。@EnableAutoConfiguration 就是借助 @Import 来收集所有符合自动配置条件的 bean 定义,并加载到IoC 容器。
@AutoConfigurationPackage
主要作用就是将主程序类所在包及所有子包下的组件扫描到 spring 容器中。 因此在定义项目包结构时,要求定义的包结构非常规范,项目主程序启动类要定义在最外层的根目录位置。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
//spring框架的底层注解,它的作用就是给容器中导入某个组件类,
//例如@Import(AutoConfigurationPackages.Registrar.class),它就是将Registrar这个组件类导入到容器中
@Import(AutoConfigurationPackages.Registrar.class) // 默认将主配置类(@SpringBootApplication)所在的包及其子包里面的所有组件扫描到Spring容器中
public @interface AutoConfigurationPackage {}
Registrar
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
// 获取的是项目主程序启动类所在的目录
// 程序启动会调用该方法
//metadata:注解标注的元数据信息
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 将会得到启动类的包路径,如 com.wyd
register(registry, new PackageImport(metadata).getPackageName());
}
}
默认将 @SpringBootApplication 标注的主配置类所在的包及其子包下的所有组件扫描到 IoC 容器中。
等等!!!这不是 @ComponentScan 的功能吗?去官方文档找找答案。
it will be used when scanning for code @Entity classes. It is generally recommended that you place EnableAutoConfiguration (if you’re not using @SpringBootApplication) in a root package so that all sub-packages and classes can be searched.
比如说,你用了Spring Data JPA,可能会在实体类上写@Entity
注解。这个 @Entity
注解由 @AutoConfigurationPackage
扫描并加载,而我们平时开发用的 @Controller/@Service/@Component/@Repository
这些注解是由ComponentScan
来扫描并加载的。
- 简单理解:这二者扫描的对象是不一样的。
验证一下:待补充
AutoConfigurationImportSelector
// 这个方法告诉springboot都需要导入那些组件
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
//判断 enableautoconfiguration注解有没有开启,默认开启(是否进行自动装配)
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
//1. 加载配置文件META-INF/spring-autoconfigure-metadata.properties,从中获取所有支持自动配置类的条件
//作用:SpringBoot使用一个Annotation的处理器来收集一些自动装配的条件,那么这些条件可以在META-INF/spring-autoconfigure-metadata.properties进行配置。
// SpringBoot会将收集好的@Configuration进行一次过滤进而剔除不满足条件的配置类
// 自动配置的类全名.条件=值
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
AutoConfigurationMetadataLoader
final class AutoConfigurationMetadataLoader {
protected static final String PATH = "META-INF/" + "spring-autoconfigure-metadata.properties";//文件中为需要加载的配置类的类路径
static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader, String path) {
try {
//1.读取spring-boot-autoconfigure.jar包中spring-autoconfigure-metadata.properties的信息生成urls枚举对象
// 获得 PATH 对应的 URL 们
Enumeration<URL> urls = (classLoader != null) ? classLoader.getResources(path) : ClassLoader.getSystemResources(path);
// 遍历 URL 数组,读取到 properties 中
Properties properties = new Properties();
//2.解析urls枚举对象中的信息封装成properties对象并加载
while (urls.hasMoreElements()) {
properties.putAll(PropertiesLoaderUtils.loadProperties(new UrlResource(urls.nextElement())));
}
// 将 properties 转换成 PropertiesAutoConfigurationMetadata 对象
//根据封装好的properties对象生成AutoConfigurationMetadata对象返回
return loadMetadata(properties);
} catch (IOException ex) {
throw new IllegalArgumentException("Unable to load @ConditionalOnClass location [" + path + "]", ex);
}
}
spring-autoconfigure-metadata.properties
# 类全名
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration
# 条件
.AutoConfigureAfter
=
org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration
......
Spring.factories
@EnableAutoConfiguration 就是从 classpath 中搜寻 META-INF/spring.factories 配置文件,并将其中 org.springframework.boot.autoconfigure.EnableutoConfiguration 对应的配置项通过反射实例化为对应的标注了 @Configuration 的 JavaConfig 形式的配置类,并加载到 IoC 容器中。
org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#getAutoConfigurationEntry
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
// 1. 判断是否开启注解。如未开启,返回空串
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
// 2. 获得注解的属性
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 3. getCandidateConfigurations()用来获取默认支持的自动配置类名列表
// spring Boot在启动的时候,使用内部工具类SpringFactoriesLoader,查找classpath上所有jar包中的META-INF/spring.factories,
// 找出其中key为org.springframework.boot.autoconfigure.EnableAutoConfiguration的属性定义的工厂类名称,
// 将这些值作为自动配置类导入到容器中,自动配置类就生效了
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 3.1 //去除重复的配置类,若我们自己写的starter 可能存在重复的
configurations = removeDuplicates(configurations);
// 4. 如果项目中某些自动配置类,我们不希望其自动配置,我们可以通过EnableAutoConfiguration的exclude或excludeName属性进行配置,
// 或者也可以在配置文件里通过配置项“spring.autoconfigure.exclude”进行配置。
//找到不希望自动配置的配置类(根据EnableAutoConfiguration注解的一个exclusions属性)
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
// 4.1 校验排除类(exclusions指定的类必须是自动配置类,否则抛出异常)
checkExcludedClasses(configurations, exclusions);
// 4.2 从 configurations 中,移除所有不希望自动配置的配置类
configurations.removeAll(exclusions);
// 5. 对所有候选的自动配置类进行筛选,根据项目pom.xml文件中加入的依赖文件筛选出最终符合当前项目运行环境对应的自动配置类
//@ConditionalOnClass : 某个class位于类路径上,才会实例化这个Bean。
//@ConditionalOnMissingClass : classpath中不存在该类时起效
//@ConditionalOnBean : DI容器中存在该类型Bean时起效
//@ConditionalOnMissingBean : DI容器中不存在该类型Bean时起效
//@ConditionalOnSingleCandidate : DI容器中该类型Bean只有一个或@Primary的只有一个时起效
//@ConditionalOnExpression : SpEL表达式结果为true时
//@ConditionalOnProperty : 参数设置或者值一致时起效
//@ConditionalOnResource : 指定的文件存在时起效
//@ConditionalOnJndi : 指定的JNDI存在时起效
//@ConditionalOnJava : 指定的Java版本存在时起效
//@ConditionalOnWebApplication : Web应用环境下起效
//@ConditionalOnNotWebApplication : 非Web应用环境下起效
//总结一下判断是否要加载某个类的两种方式:
//根据spring-autoconfigure-metadata.properties进行判断。
//要判断@Conditional是否满足
// 如@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })表示需要在类路径中存在SqlSessionFactory.class、SqlSessionFactoryBean.class这两个类才能完成自动注册。
configurations = filter(configurations, autoConfigurationMetadata);
// 6. 将自动配置导入事件通知监听器
//当AutoConfigurationImportSelector过滤完成后会自动加载类路径下Jar包中META-INF/spring.factories文件中 AutoConfigurationImportListener的实现类,
// 并触发fireAutoConfigurationImportEvents事件。
fireAutoConfigurationImportEvents(configurations, exclusions);
// 7. 创建 AutoConfigurationEntry 对象
return new AutoConfigurationEntry(configurations, exclusions);
}
(3) @ComponentScan
@ComponentScan 注解具体扫描的包的根路径由 Spring Boot 项目主程序启动类所在包位置决定,在扫描过程中由前面介绍的@AutoConfifigurationPackage 注解进行解析,从而得到 Spring Boot 项目主程序启动类所在包的具体位置
还没有评论,来说两句吧...