传统的spring实现一个web服务,需要导入各种依赖的jar包,然后编写相应的xml配置文件等。相较而言,springboot就更加简单、高效、便捷,主要是因为springboot的依赖管理和自动配置
3.1 依赖管理
问题1:为什么导入dependency不需要指定版本?
在Springboot入门程序中,项目pom文件中有两个重要的核心依赖,分别是spring-boot-starter-parent和spring-boot-starter-web。
3.1.1 spring-boot-starter-parent依赖
父工程所执行的事:
代码示例
<properties>
<java.version>1.8</java.version>
<resource.delimiter>@</resource.delimiter>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<build>
<resources>
<resource>
<directory>${basedir}/src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/application*.yml</include>
<include>**/application*.yaml</include>
<include>**/application*.properties</include>
</includes>
</resource>
</resources>
...
</build>
<pluginManagement>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
...
</pluginManagement>
编码的配置(UTF-8)
- JDK版本的配置(1.8)
- 引入配置文件(${basedir}/src/main/resources目录下的/application*.yml文件、/application.yaml文件和**/application.properties文件,properties优先级最高)
- 各种插件管理
父工程的父工程:
- 代码示例
```xml
2.3.14 4.3.5 2021.0.9 5.3.16 1.3.7 …5.5.9
- 在<properties>标签中配置了各种依赖的版本号。
- 在<dependencyManagement>标签中配置了各种依赖管理
- 所以如果在dependencyManagement中管理的依赖导入时就不需要指定版本号,不在管理的依赖则需要指定版本号。
**问题2**:spring-boot-starter-parent主要是进行依赖管理,那么项目运行依赖的jar包从何而来?
<a name="yA9Tc"></a>
## 3.1.2 spring-boot-starter-web依赖
```xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.5.10</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.5.10</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.5.10</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.16</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.16</version>
<scope>compile</scope>
</dependency>
</dependencies>
以上代码就是spring-boot-starter-web里的所有依赖,可以看出,spring-boot-starter-web依赖启动器提供了web开发场景所需要的所有底层依赖。所以,在pom中引入spring-boot-starter-web就可以实现web场景开发,而不需要额外引入tomcat服务器以及其它web依赖文件。
springboot除了提供上述web依赖启动器之外,还提供了许多其它开发场景的依赖启动器,可以打开spring官网搜索starter。springboot并没有为所有场景的开发提供依赖启动器,比如mybatis、druid等等,但是mybatis、druid自己整合了springboot,比如mybatis-spring-boot-starter̵,druid-spring-boot-starter等。
3.2 自动配置(启动流程)
概念:能够在我们添加jar包的时候,自动为我们配置一些组件的相关配置,我们无需配置或者只需少量配置就能运行编写的项目。
问题:springboot到底是如何进行自动配置的?都把哪些组件进行了自动配置?
- springboot应用的启动入口是在@SpringBootApplication注解标注的类中的main方法,@SpringBootApplication能够扫描spring组件并自动配置springboot。
```java @Target({ElementType.TYPE}) // 注解适用范围,TYPE表示在类、接口、注解或枚举中使用 @Retention(RetentionPolicy.RUNTIME) // 表示注解的生命周期,运行时 @Documented // 表示注解可以记录在javadoc中 @Inherited // 表示可以被子类继承该注解@SpringBootApplication
public class WeilanApplication {
public static void main(String[] args) {
SpringApplication.run(WeilanApplication.class, args);
}
}
@SpringBootConfiguration // 标明该类为配置类 @EnableAutoConfiguration // 启动自动配置功能 @ComponentScan( // 包扫描器 excludeFilters = {@Filter( type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class} ), @Filter( type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class} )} ) public @interface SpringBootApplication {}
```java
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration // 配置类
@Indexed
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
- 如上代码所示@EnableAutoConfiguration启动了自动配置的功能,所以解析EnableAutoConfiguration类就能解析自动配置的功能。 ```java @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited
@AutoConfigurationPackage // 自动配置包 // 可以将所有符合条件的@Configuration配置都加载到当前Springboot创建并使用的ioc容器中 @Import({AutoConfigurationImportSelector.class}) public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = “spring.boot.enableautoconfiguration”;
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
2.1 AutoConfigurationPackage类解析
```java
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
// spring框架的底层注解,它的作用就是给容器导入某个组件类
// 例如@Import({Registrar.class})就是将Register这个组件类导入容器中
// 默认将启动类@SpringBootApplication所在的包及其子包所有的组件导入到容器当中
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
}
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
Registrar() {
}
// 获取的是项目主启动类所在的目录
// metadata: 注解标注的元数据信息
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 默认将会扫描@SpringBootApplication标注的主配置类所在的包及其子包下所有组件
AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
}
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata));
}
}
2.2 AutoConfigurationImportSelector.class解析
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
......
......
// 这个方法告诉springboot需要导入哪些组件
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 判断EnableConfigutation注解有没有开启,默认开启(是否进行自动装配)
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
/**
1.加载配置文件META-INF/spring-autoconfigure-metadata.properties,从中获取所 有支持自动配置类的条件
作用:SpringBoot使用一个Annotation的处理器来收集一些自动装配的条件,那么这些条 件可以在META-INF/spring-autoconfigure-metadata.properties
SpringBoot会将收集好的@Configuraion进行一次过滤而剔除不满足条件的配置类
自动配置的类全名.条件=值
*/
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
}
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
// getCandidateConfigurations()用来获取默认支持自动配置的类名列表
// Springboot在启动的时候,使用内部工具类SpringFactoriesLoader,查找classpath上
// 所有的jar包中的META-INF/spring.factories,找出其中key值为
// org.framework.boot.autoconfigure.EnableAutoConfiguration的属性定义的工厂类名称
// 将这些值作为自动配置类导入到容器中,自动配置类就生效了
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
// 去除重复的配置类,若我们自己写的starter可能存在重复的
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
// 对所有候选的自动配置进行筛选,根据项目pom文件中的依赖文件筛选出最终符合当前项目
// 运行环境对应的自动配置类
configurations = this.getConfigurationClassFilter().filter(configurations);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
总结:
- 自动配置的实现,主要依赖与@SpringBootApplication注解类中的@EnableAutoConfiguration注解。
- 在@EnableAutoConfiguration中,又有着一个@AutoConfigurationPackage注解,该注解中使用@Import({Registrar.class})将Register这个组件类导入容器中。而Register实现的功能就是:会把@SpringBootApplication标注的类所在的包名拿到,并封装保存起来,为后续@ComponentScan扫描包时使用,这就是我们开发时要把类的包放在启动类的同一目录下的原因。还注册了一个BasePackage的Bean。
- 在@EnableAutoConfiguration中,还有一个@Import({AutoConfigurationImportSelector.class}) ,它通过将AutoConfigurationImportSelector导入容器中,AutoConfigurationImportSelector类的作用就是通过selectImports方法执行的过程中,会使用内部工具类SpringFactoriesLoader,查找classpath上所有jar包的META-INF/spring.factories进行加载(加载时会根据一些条件进行过滤,比如存不存在某个类),实现将配置信息类交给SpringFactory加载器进行一系列的容器创建过程。
- 再次总结一下AutoConfigurationImportSelector类,通过执行selectImports()方法的过程中,加载到spring-boot-autoconfigure包下的META-INF/spring-autoconfigure-metadata.properties文件,获取到所有支持自动配置类的条件(后续用来筛选),然后调用getAutoConfigurationEntry()方法,这个方法首先调用getCandidateConfigurations()方法,通过SpringFactoriesLoader.loadFactoryNames()加载classpath下的META-INF/spring.factories文件,获取到所有的自动配置类的全限定类名,然后返回;接下来就是对候选的所有自动配置类进行筛选,比如去除重复的配置类、去除不希望自动配置的配置类等,最后根据pom文件加入的依赖筛选出最终需要自动配置的配置类,使用内部工具类SpringFactoriesLoader,实现将配置信息类交给SpringFactory加载器进行一系列的容器创建。这些配置类导入到容器中,自动配置就生效了。
3.3 自定义Starter
3.3.1 SpringBoot starter机制
SpringBoot由众多stater组成,可以理解为一个可插拔的插件,正是因为这些starter使得使用某个功能的开发者不需要关注各种依赖库的处理,不需要具体的配置信息,由SpringBoot自动通过classpath路径下的类发现需要的bean,并织入相应的bean。比如想使用redis插件,就可以使用spring-boot-starter-redis;想使用MongoDB,就可以使用spring-boot-starter-data-mongodb
3.3.2 为什么要自定义starter
开发过程中,经常会有一些独立于业务之外的配置模块。如果我们将这些可独立于业务代码之外的功能配置模块封装成一个个starter,复用的时候只需要其在pom中引用依赖即可,springboot为我们完成自动装配。
3.3.3 自定义starter的命名规则
SpringBoot提供的starter都是以spring-boot-starter-xxx的方式命名的。官方建议自定义的starter使用xxx-spring-boot-starter命名规则。以区分SpringBoot生态提供的starter。
3.3.4 自定义starter过程
创建maven工程,导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
</dependencies>
创建SimpleBean
@EnableConfigurationProperties(SimpleBean.class) // 让@ConfigurationProperties注解生效
@ConfigurationProperties(prefix = "simplebean")
public class SimpleBean {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "SimpleBean{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
创建MyAutoConfiguration ```java @Configuration @ConditionalOnClass public class MyAutoConfiguration { static {
System.out.println("MyAutoConfiguration初始化...");
}
@Bean public SimpleBean simpleBean() {
return new SimpleBean();
}
}
4. 在resources下创建META-INF/spring.factories(因为springboot的自动配置会加载这个文件)
```properties
#根据规则配置好自动配置类的路径
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.atm.pojo.config.MyAutoConfiguration
3.3.5使用自定义的starter
导入自定义的依赖
<dependency>
<groupId>com.atm</groupId>
<artifactId>wdf-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
在全局配置文件中配置属性值
simplebean.id=1
simplebean.name=自定义starter
测试 ```java @Autowired private SimpleBean simpleBean;
@Test public void wdfStarterTest(){ System.out.println(simpleBean); }
<a name="pFUwf"></a>
# 3.4 run方法
<a name="wSGiQ"></a>
## 3.4.1 SpringApplication实例化过程
```java
@SpringBootApplication
public class TestSpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(TestSpringBootApplication.class, args);
}
}
SpringApplication的启动由两部分组成:
- 实例化SpringApplication对象
- 调用run()方法
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
new SpringApplication(primarySources)过程:
public SpringApplication(Class<?>... primarySources) {
this(null, primarySources);
}
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
// 项目启动类设置为属性存储起来
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 设置应用类型是SERVLET应用还是REACTIVE应用(spring5之后出现WebFlux响应式编程)
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
// 设置初始化器,最后会调用这些初始化器(就是ApplicationContextInitializer的实现类)
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 设置监听器
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 用于推断并设置项目main()方法启动的主程序启动类
this.mainApplicationClass = deduceMainApplicationClass();
}
从上述源码可以看出,SpringApplication的初始化过程主要包括4部分,具体说明如下:
- this.webApplicationType = WebApplicationType.deduceFromClasspath():用于判断当前WebApplicationType的类型。deduceFromClasspath()方法用于查看classpath类路径下是否存在某个特征类(使用ClassUtils类来判断该类是否存在),从而判断当前WebApplicationType类型是SERVLET应用(Spring5之前的传统MVC应用)还是REACTIVE应用(Spring5开始出现的WebFlux交互式应用)
- setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)):用于SpringApplication应用的初始化设置。在初始化设置的过程中,会使用Spring类加载器SpringFactoriesLoader从META-INF/spring.factories文件中获取所有可用的应用初始化器类ApplicationContextInitializer。
- setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)):用于SpringApplication的监听器设置。监听器设置的过程与上一步初始化器设置的过程基本一样,会使用Spring类加载器SpringFactoriesLoader从META-INF/spring.factories文件中获取所有可用的监听器类ApplicationListener。
- this.mainApplicationClass = deduceMainApplicationClass():用于推断并设置项目main方法启动的主程序类。
3.4.2 调用run方法过程
public ConfigurableApplicationContext run(String... args) {
long startTime = System.nanoTime();
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
// 1.获取并启动监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 创建ApplicationArguments对象,初始化默认应用参数类
// args是启动spring应用的命令行参数,该参数可以在spring中被访问,如 --server.port=
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 2.项目运行环境的预配置
// 创建并配置当前SpringBoot应用将要使用的Environment
// 并遍历所有调用SpringApplicationRunListener的EnvironmentPrepare()方法
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
// 准备Banner打印器-就是启动SpringBoot时打印在console上的ASCII字体
Banner printedBanner = printBanner(environment);
// 3.创建spring容器
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
// 4.Spring容器前置处理,这一步主要是容器刷新之前的准备动作
// 包含一个非常关键的操作:将启动类注入容器,为后续开启自动化配置奠定基础
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 5.刷新容器
refreshContext(context);
// 6.Spring容器的后置处理
// 拓展接口,设计模式中的模板方法,默认为空实现
// 如果有自定义需求,可以重写该方法。比如打印一些启动结束log,或者一些其它后置处理
afterRefresh(context, applicationArguments);
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
// 7.发出结束执行的事件通知
// Runner运行器用于在服务启动时进行一些业务初始化操作,这些操作只在服务启动后执行一次
// SpringBoot提供了ApplicationRunner和ComandLineRunner两种服务接口
listeners.started(context, timeTakenToStartup);
// 8.执行Runners
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
// 9.发布应用上下文就绪事件
listeners.ready(context, timeTakenToReady);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
从以上源码可以看出,项目初始化过程大致如下:
- 第一步:获取并启动监听器。getRunListeners(args)和listeners.starting(bootstrapContext, this.mainApplicationClass)方法主要运用于获取SpringApplicatin实例化时初始化的SpringApplicationRunListener监听器并运行。
- 第二步:根据SpringApplicationRunListeners以及参数来准备环境。prepareEnvironment(listeners, bootstrapContext, applicationArguments)方法主要用于对项目环境运行进行预设置,同时通过configureIgnoreBeanInfo(environment)方法排除一些不需要运行的环境。
- 第三步:创建Spring容器。根据webApplicationType判断,确定容器类型,如果该类型是SERVLET,会通过反射装载对应的字节码,也就是AnnotationConfigServletWebServerApplicationContext,接着使用之前初始化的context(应用上下文环境)、environment(项目运行环境)、listeners(运行监听器)、applicationArguments(项目参数)和 printedBanner(项目图标信息)进行应用上下文的组装配置,并刷新配置。
- 第四步:Spring容器的前置处理。这一步主要是在容器刷新之前的准备动作,设置容器环境,包括各种变量等等,其中包含一个非常关键的操作:将启动类注入容器,为后续开启自动化配置奠定基础。
- 第五步:刷新容器。开启刷新spring容器,通过refresh方法对整个ioc容器的初始化(包括bean资源的定位、解析、注册等)。也是在这一步获取到tomcat的工厂类(createWebServer()方法),执行tomcat的创建和启动的(getWebServer()方法)。同时向jvm运行时注册一个关机钩子,在jvm关机时会关闭这个上下文,除非当时它已经关闭。
- 第六步:Spring容器的后置处理。扩展接口,设计模式中的模板方法,默认为空实现。如果有自定义需求,可以重写该方法。比如打印一些启动结束log,或者一些其它的后置处理。
- 第七步:发出结束执行的事件。获取EventPublishingRunListener监听器,并执行其started方法,将创建的spring容器传进去,创建了一个ApplicationStartedEvent事件,并执行ConfigurableApplicationContext的publishEvent方法,也就是说这里是在spring容器中发布事件,并不是在SpringApplication发布事件。和第一步的starting是不同的,前面的starting是直接向SpringApplication中的监听器发布事件。
- 第八步:执行Runners。用于调用项目中自定义的执行器xxxRunner类,使得在项目启动完成后执行一些特定程序。其中,SpringBoot提供的执行器接口有ApplicationRunner和ComandLineRunner两种,在使用时只需要自定义一个执行器类实现其中一个接口并重写对应的run()方法接口,然后SpringBoot项目启动后会立即执行这些程序。