作为一名Java
程序员,Spring
框架是我们平常工作中非常重要的一个框架。其为我们提供了IOC
容器和AOP
的功能,在很大程度上简化了我们项目中的开发,但随着我们使用Spring
接入的组件的增加,相应的配置文件也越来越多,这些配置文件的维护也成为了一个比较繁琐的工作,这也是很多开发人员比较诟病的地方。
SpringBoot
的出现便很好的解决了这个问题,在SpringBoot
项目中我们不再关注那么多的配置文件,其提供的自动装配功能使我们能够更容易的用其来快速的搭建我们的应用。
在今天的文章中我们就介绍下SpringBoot
的自动装配、应用的启动流程,并在最后介绍下如何自定义一个starter
。
1、示例代码
在介绍之前,我们先通过示例代码看看如何使用SpringBoot
搭建一个web应用,示例代码使用的是SpringBoot
的最新版本3.0.0
,该版本依赖JDK17,如果本地使用的JDK8请使用低版本的SpringBoot
或者升级JDK。
创建spring-boot-sample
的maven
项目并添加相关依赖,其pom文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.xh</groupId>
<artifactId>spring-boot-sample</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
复制代码
在创建好的项目中编写一个启动类,其内容如下:
package com.xh.sample.spring.boot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @description: Create by IntelliJ IDEA
* @author: xh
* @createTime: 2022/12/3 18:31
*/
@RestController
@SpringBootApplication
public class SpringBootSampleApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootSampleApplication.class, args);
}
@RequestMapping(value = "/hello")
public String hello() {
return "Hello World!!!";
}
}
复制代码
至此我们的示例代码就编写完成了,我们直接执行启动类中的main
方法,启动完成后可以访问http://localhost:8080/hello
进行测试。
我们在pom中引入了spring-boot-starter-web
的依赖,便集成好了SpringMVC
的功能,是不是很简单方便呢,这便是自动装配的魔力。
启动类中只需要添加@SpringBootApplication
注解并调用SpringApplication.run
方法即可,在下面的内容中我们主要就是围绕着这两点来进行介绍。
2、自动装配
自动装配是SpringBoot
的核心,是基于Spring
的注解及SPI
机制进行的实现。当SpringBoot
启动时会加载各个依赖包中META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件(我们使用的是SpringBoot3.0.0
版本,之前的版本中需要自动装配的类也是定义在spring.factories
中的),并将该文件中配置的类加载到Spring
容器中。
开启自动装配的逻辑关键在@SpringBootApplication
这个注解上,这个注解的源码如下:
@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 {
// 需要排除的类
@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};
@AliasFor(annotation = EnableAutoConfiguration.class)
String[] excludeName() default {};
// 扫描的包 默认是当前包路径
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
@AliasFor(annotation = Configuration.class)
boolean proxyBeanMethods() default true;
}
复制代码
从这段源码中我们可以看到@SpringBootApplication
是一个复合注解;
@SpringBootConfiguration
@Configuration
的派生注解@EnableAutoConfiguration
启用SpringBoot
的自动装配@ComponentScan
Spring
中的注意,用于扫描相应的包下被@Component
及其派生注解修饰的类
因此,我们完全可以将这个注解替换为@Configuration
、@EnableAutoConfiguration
及@ComponentScan
三个注解。
这里我们单独看下@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 {};
}
复制代码
在这个注解中的@AutoConfigurationPackage
注解的作用是将启动类所在包下的所有组件注册到Spring
容器中,跟踪源码会发现其会调用到BeanDefinitionRegistry.register
方法,这个是Spring
中相关的类,我们这里就不展开了。
@Import
注解中跟着的类,在Spring
容器启动时会进行加载,这里面设置的是AutoConfigurationImportSelector
,这个类的作用是加载自动装配类,这个类实现了DeferredimportSelector
接口并实现了selectImports
方法,这个方法的作用是收集所有需要加载到Spring
容器中的类名。其逻辑如下:
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
// 是否开启了自动装配 判断的是spring.boot.enableautoconfiguration配置
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
// 获取注解的属性信息
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 获取所有需要自动装配的类
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 移除重复的类
configurations = removeDuplicates(configurations);
// 移除需要排除的类
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
// 获取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中配置的类
List<String> configurations = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader())
.getCandidates();
Assert.notEmpty(configurations,
"No auto configuration classes found in "
+ "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
// ImportCandidates类中的方法,LOCATION的值为:"META-INF/spring/%s.imports"
public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
Assert.notNull(annotation, "'annotation' must not be null");
ClassLoader classLoaderToUse = decideClassloader(classLoader);
String location = String.format(LOCATION, annotation.getName());
Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
List<String> importCandidates = new ArrayList<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
importCandidates.addAll(readCandidateConfigurations(url));
}
return new ImportCandidates(importCandidates);
}
复制代码
到这里关于SpringBoot
自动装配相关的逻辑已经介绍完了,通过查看源码的方式,我们可以知道:
@SpringBootApplication
是一个复合注解,拥有@Configuration
、@EnableAutoConfiguration
及@ComponentScan
的功能@EnableAutoConfiguration
是开启自动装配的注解,在其中定义了@Import(AutoConfigurationImportSelector.class)
AutoConfigurationImportSelector
类会在容器启动时获取到META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件中配置的类交给Spring
进行初始化
3、启动过程
在上面的内容中我们介绍了SpringBoot
的自动装配的原理,在接下来的内容中我们介绍下SpringBoot
的启动过程。自动装配的逻辑是在@SpringBootApplication
中,其启动过程我们应该跟踪SpringApplication.run
方法。
跟进这个源码会调用到如下的方法中:
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
// 创建SpringApplication对象并调用run方法
return new SpringApplication(primarySources).run(args);
}
复制代码
SpringApplication
对象的创建逻辑如下:
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// 这里传递的resourceLoader为null
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
// 这里的primarySources传递的是我们的启动类
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 获取应用类型 NONE SERVLET REACTIVE 我们的示例代码是一个web项目 这里的类型是SERVLET
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 获取META-INF/spring.factories配置的BootstrapRegistryInitializer实例
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
// 获取META-INF/spring.factories配置的BootstrapRegistryInitializer实例
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 获取META-INF/spring.factories配置的ApplicationListener实例
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
复制代码
在SpringApplication
构造方法中设置应用类型并获取到一些在META-INF/spring.factories
中配置的类实例。
应用类型有如下三种:
- NONE
- SERVLET
- REACTIVE
getSpringFactoriesInstances
方法最后会调用到SpringFactoriesLoader.load
方法中,SpringFactoriesLoader
类是Spring SPI
的工具类,使用这个类可以获取到所有在META-INF/spring.factories
中配置的信息并可按照某个类型获取实例列表。
通过这种方式获取对象的方法,都可以作为我们的扩展点,那么该如何使用呢?我们以BootstrapRegistryInitializer
为例,我们在示例代码中定义一个SelfBootstrapRegistryInitializer
类,创建META-INF/spring.factories
文件并添加配置,代码如下:
public class SelfBootstrapRegistryInitializer implements BootstrapRegistryInitializer {
@Override
public void initialize(BootstrapRegistry registry) {
System.out.println("=============== SelfBootstrapRegistryInitializer ==================");
}
}
// spring.factories
org.springframework.boot.BootstrapRegistryInitializer=\
com.xh.sample.spring.boot.SelfBootstrapRegistryInitializer
复制代码
然后再重启项目,可以看到控制台会打印出我们输出的内容。
介绍完SpringApplication
的构造方法后,我们看看这个对象中的run
方法逻辑,这个方法中的逻辑就是启动SpringBoot
的流程,其源码如下:
public ConfigurableApplicationContext run(String... args) {
long startTime = System.nanoTime();
//创建启动上下文对象 这里会调用在上面中获取到的BootstrapRegistryInitializer实例的initialize方法
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
// 设置java.awt.headless属性
configureHeadlessProperty();
// 获取META-INF/spring.factories中配置的SpringApplicationRunListener实例
// SpringApplicationRunListener里面提供了starting environmentPrepared contextPrepared 等方法
// 使用这个扩展点可以完成对启动的各个阶段进行扩展
SpringApplicationRunListeners listeners = getRunListeners(args);
// 调用starting方法
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 准备环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
// 打印banner图 如果自定义banner图的话 只需在resources目录下创建banner.txt文件即可
Banner printedBanner = printBanner(environment);
// 创建上下文对象 创建的是AnnotationConfigServletWebServerApplicationContext
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 启动容器的流程,这里会调用到AbstractApplicationContext.refresh方法中启动spring容器
refreshContext(context);
// 这里是个空方法,子类可以实现该方法做些扩展 模版方法
afterRefresh(context, applicationArguments);
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
// 容器启动成功 调用listeners的started方法
listeners.started(context, timeTakenToStartup);
// 从容器中获取所有的ApplicationRunner CommandLineRunner对象 调用里面的run方法
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
if (ex instanceof AbandonedRunException) {
throw ex;
}
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
if (context.isRunning()) {
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
listeners.ready(context, timeTakenToReady);
}
}
catch (Throwable ex) {
if (ex instanceof AbandonedRunException) {
throw ex;
}
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
复制代码
相应的逻辑在上面的备注中进行了介绍,通过跟踪源码,我们发现SpringBoot
中也提供了很多扩展点,我们简单的梳理下如下:
- 自定义
BootstrapRegistryInitializer
类 配置到spring.factories
文件中 - 自定义
ApplicationContextInitializer
类 配置到spring.factories
文件中 - 自定义
ApplicationListener
类 配置到spring.factories
文件中 - 自定义
SpringApplicationRunListener
类 配置到spring.factories
文件中 用于扩展启动的各个阶段 - 自定义banner图,在
classpath
下创建banner.txt
文件 - 自定义
ApplicationRunner CommandLineRunner
类,使用@Component
交给spring
容器管理,容器启动完成后调用其run方法
在我们之前的spring
搭建的web项目,需要部署到tomcat
或者其他的web
服务容器中,但我们使用SpringBoot
搭建的web应用只需执行一个SpringApplication.run
方法便可对外提供web服务了,这是因为在SpringBoot
中给我们内置了web
容器,这块代码的逻辑在ServletWebServerApplicationContext.onRefresh
方法中,看过spring
容器启动源码的应该会发现,在容器启动完成后会调用这个方法。这里的代码就不进行粘贴了,在这里会创建WebServer
对象,默认使用的是Tomcat
,内容的容器有如下几种:
TomcatWebServer
JettyWebServer
NettyWebServer
UndertowWebServer
UndertowServletWebServer
3、自定义starter
在上面的内容中我们介绍了SpringBoot
自动装配的逻辑,我们只需要按照他的规则便可以很方便的自定义一个starter。接下来的内容给出一个简单的示例。在示例代码中我们定义一个单机版redis的客户端redisson
。
创建一个redisson-spring-boot-starter
的Maven
项目,并添加相关依赖如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.3</version>
</dependency>
复制代码
创建RedissonProperties
类,用于接收相关配置,内容如下:
@ConfigurationProperties(prefix = "redis")
public class RedissonProperties {
private String host = "localhost";
private Integer port = 6379;
public String getHost() {
return host;
}
public Integer getPort() {
return port;
}
}
复制代码
创建RedissonAutoConfiguration
类,内容如下:
@Configuration
@EnableConfigurationProperties(value = RedissonProperties.class)
public class RedissonAutoConfiguration {
@Autowired
private RedissonProperties redissonProperties;
@Bean
public RedissonClient redissonClient() {
System.out.println("============= 初始化RedissonClient对象 ===============");
Config config = new Config();
config.useSingleServer().setAddress("redis://" + redissonProperties.getHost() + ":" + redissonProperties.getPort());
return Redisson.create(config);
}
}
复制代码
在META-INF/spring
目录下创建org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件,并将RedissonAutoConfiguration
类配置进去,内容如下:
com.xh.starter.redisson.RedissonAutoConfiguration
复制代码
然后使用Maven install
到本地仓库,在spring-boot-sample
中引入其依赖,然后重启项目,可以在启动日志中看到我们打印的日志,代表我们自定义的starter
可以正常使用了。
4、总结
今天的内容就这些了,在本文中介绍了SpringBoot
的自动装配原理、启动过程及如何自定义starter。
本文中的示例代码使用的SpringBoot
版本为3.0.0
,JDK
版本为17
。
@SpringBootApplication
是一个复合注解,是自动装配的关键,AutoConfigurationImportSelector
会获取到项目中所有依赖包中META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件中配置的类交给spring
容器。
SpringBoot
启动过程中会启动spring
容器和web
服务容器,并在其启动过程中有一些扩展点。