SpringBoot底层剖析
SpringBoot原理
配置优先级
- SpringBoot中支持三种格式的配置文件:
// application.properties
server.port = 8081
// application.yaml
server:
port: 8082
// application.yml
server:
port: 8083
如果同时存在以上三份配置文件,那么配置生效的优先级为:properties > yml > yaml
不过,开发中尽量统一配置文件的格式,只使用同一种格式的配置文件
- SpringBoot除了支持文件属性配置,还支持Java系统属性和命令行参数的方式进行属性配置
- Java系统属性:
-Dserver.port=9000 - 命令行参数:
--server.port=10010


命令行参数优先级大于Java系统属性,且它们都优先于配置文件
通过Maven打包后的Jar包,使用
Java -Dserver.port=9000 -jar tlias-web-management-0.0.1-SNAPSHOT.jar --server.port=10010命令即可运行(Springboot项目需要引入spring-boot-maven-plugin依赖进行打包)
Bean管理
Bean的作用域
- Spring支持五种作用域,后三种在web环境才生效
| 作用域 | 说明 |
|---|---|
| singleton | 容器内 同名称 的bean只有一个实例(单例)(默认) |
| prototype | 每次使用该bean时会创建新的实例(非单例/多例) |
| request | 每个请求范围内会创建新的实例(web环境中,了解) |
| session | 每个会话范围内会创建新的实例(web环境中,了解) |
| application | 每个应用范围内会创建新的实例(web环境中,了解) |
声明作用域:
// 在bean类中添加注解@Scope
@Scope("prototype")
@RequestMapping("/depts")
@RestController
public class DeptController {...}
如果不指定作用域,默认IOC容器内的同名称bean对象都是同一个
获取IOC容器内的Bean对象:
@Autowired
private ApplicationContext applicationContext;
@Test
public void getIOCBean() {
applicationContext.getBean("deptController");
}
Bean对象在IOC容器中的名称默认是类名首字母小写
当然Bean也可以起别名,通过
@Component("bean")、@Bean(name = {"bean"})等等
- 默认singleton作用域的bean创建时机:
- 在**项目启动时创建**,创建完毕后bean存入IOC容器,后续就不会再重新创建
- 可以在Bean类上加上
@Lazy注解,这样只有当**第一次使用**到这个Bean对象时,才会创建并存入IOC容器,后续不再创建 - prototype作用域的bean创建时机:
- 每当需要使用到该bean对象时,就会创建一个新的bean对象
- prototype bean本身就是按需创建,**lazy对其无效**
Bean作用域的应用场景
-
Singleton的适用场景:
-
无状态的bean就可以设置为singleton
- 无状态指的就是bean对象中不会保存数据,只有逻辑相关的操作
- 当bean保存了数据后,该数据会在所有对该bean的调用中共享,如果多个调用方同时对其进行修改,会导致数据竞争和状态污染,无法保证数据的一致性和正确性
-
Prototype的适用场景:
-
有状态的bean就应该设置为prototype
- 有状态指的是bean对象中会保存数据,这些数据通常是用户相关或请求相关的临时信息
- 当bean保存了数据后,每个调用方都需要独立的实例来存储自己的数据,避免数据在不同调用方之间共享造成的状态污染和数据不一致问题
开发中大部分的bean都是无状态的,所以不需要配置scope。只需要遵循Bean类中保存数据使用Prototype,不保存使用Singleton
Singleton作用域的bean对于系统的性能压力更小
Bean作用域面试题
面试题1:Spring容器的bean是单例还是多例?单例的bean是什么时候实例化的?
答1:默认是单例Singleton。不配置@Lazy注解,在程序启动时进行实例化,配置@Lazy在使用到时进行实例化
面试2:Spring容器的bean是线程安全的吗?
答2:
- bean的线程安全取决于bean的状态和作用域
- 单例bean:如果是无状态的bean,内部不保存任何状态信息,则是**线程安全**的
- 单例bean:如果是有状态的bean,内部会保存状态信息,多个线程操作该bean时,可能会出现数据不一致的问题,这样的bean则是**线程不安全**的
- 多例bean:无论是有状态还是无状态,每次调用底层都会创建单独的实例,所以不存在多个线程操作同一个bean,所以多例bean是**线程安全**的
第三方bean
- 当引入第三方依赖时,是无法通过添加
@Component注解将某个类交给IOC容器的(只读权限,且一定避免对源码进行直接修改) - 不过,可以通过
@Bean注解解决以上问题(注意不是在源码中加)
@Configuration
public class ThirdPartyConfig {
@Bean
public ThirdPartyService thirdPartyService() {
return new ThirdPartyService();
}
@Bean
public TargetBean targetBean(@Autowired DependencyBean dependencyBean) {// @Autowired可以省略,这里加上方便理解
TargetBean target = new TargetBean();
target.setDependency(dependencyBean);
return target;
}
}
在 @Bean 方法中,返回值类型就是你想要注册到IOC容器中的Bean类型
当Bean依赖其他Bean时,可以通过方法参数注入依赖(Spring会自动从IOC容器中找到对应的bean对象并传入)
建议:虽然可以直接在启动类中通过@Bean添加第三方bean,但是为了方便第三方bean的管理,建议单独创建一个配置类(@Configuration),在配置类中对第三方的bean进行统一管理
起步依赖
SpringBoot中的起步依赖就是Maven中的依赖传递,通过引入起步依赖将所需要的所有依赖自动引入

像包名中带有
xxx-starter的包,大部分就是起步依赖包
自动配置(面试重点)
- SpringBoot的自动配置就是当spring项目启动后,一些配置类、bean对象就自动存入到了IOC容器中,不需要我们手动去声明,从而简化了开发,省去了繁琐的配置操作
实现方案
环境搭建:先在项目中导入第三方包:
<!-- 第三方依赖 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>itheima-utils</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
方案一:@Component + @ComponentScan
如果第三方包下声明了bean(带有@Component的类),在注入时,还要考虑组件扫描的范围(默认扫描启动类所在包及其子包)。所以需要修改扫描范围:
@ComponentScan(basePackages = {"com.example","com.itheima"})
@SpringBootApplication
public class SpringbootWebConfigApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootWebConfigApplication.class, args);
}
}
com.example包下有一个工具类(TokenParser)被声明为了bean
@ComponentScan(basePackages = ...)注解可以换成@SpringBootApplication(scanBasePackages = ...)有可能会存在IDEA无法识别到目标包,但是实际上是可以运行的
@Autowired
private TokenParser tokenParser;
@Test
public void testTokenParser() {
tokenParser.parse();
}
方案一的问题:首先需要对方被声明为bean才可以使用,其次由于扫描范围的增加,导致配置繁琐且性能降低
方案二:通过@Import导入。@Import导入的类会被Spring加载到IOC容器中,导入形式主要有以下几种
- 导入普通类
- 导入配置类(@Configuration)
// 导入普通类 导入配置类
@Import({TokenParser.class, HeaderConfig.class})
@SpringBootApplication
public class SpringbootWebConfigApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootWebConfigApplication.class, args);
}
}
- 导入
ImportSelector接口的实现类(第三方可以决定哪些类能够被加载到IOC容器中,且有批量导入的功能)
-
当第三方依赖中有一个类实现了
ImportSelector接口public class MyImportSelector implements ImportSelector { public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[]{"com.example.HeaderConfig"}; } }- 使用的时候直接导入这个类即可@Import({MyImportSelector.class}) @SpringBootApplication public class SpringbootWebConfigApplication { public static void main(String[] args) { SpringApplication.run(SpringbootWebConfigApplication.class, args); } }4. 根据第三种方式,第三方还可以提供注解类简化使用方的导入操作(封装@Import注解) -
注解类如下:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Import(MyImportSelector.class) public @interface EnableHeaderConfig { }- 使用方不再需要使用@Import注解并指定目标类了,可以直接使用提供的注解类进行导入:@EnableHeaderConfig @SpringBootApplication public class SpringbootWebConfigApplication { public static void main(String[] args) { SpringApplication.run(SpringbootWebConfigApplication.class, args); } }
源码跟踪
源码跟踪的着重点在启动类上,通过@SpringBootApplication注解查看被层层封装的源码。该注解标识在SpringBoot工程引导类上,是SpringBoot中最最最重要的注解。该注解由三个部分组成:
- @SpringBootConfiguration:该注解与@Configuration注解作用相同,用来声明当前也是一个配置类
- @ComponentScan:组件扫描,默认扫描引导类所在包及其子包
- @EnableAutoConfiguration:SpringBoot实现自动化配置的**核心注解**






@Conditional注解
- 作用:按照一定的条件进行判断,在满足给定条件后才会注册对应的bean对象到Spring IOC容器中
- 位置:方法、类
- @Conditional本身是一个父注解,派生出大量的子注解,以下为常见子注解:
- @ConditionalOnClass:判断环境中是否有对应字节码文件,才注册bean到IOC容器
- @ConditionalOnMissionBean:判断环境中没有对应的bean(类型或名称),才注册bean到IOC容器
- @ConditionalOnProperty:判断配置文件中有对应属性和值,才注册bean到IOC容器
示例:
package com.example;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HeaderConfig {
@Bean
@ConditionalOnClass(name = "io.jsonwebtoken.Jwts") // 判断环境中是否有对应的字节码文件,如果有则创建bean
public HeaderParser headerParser(){
return new HeaderParser();
}
@Bean
@ConditionalOnProperty(name = "myname", havingValue = "itheima")
public HeaderGenerator headerGenerator(){
return new HeaderGenerator();
}
}
自己定义自动配置类的核心是定义自动配置类,将自动配置类配置在
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中
自定义starter
- 场景:在实际开发中,经常会定义一些公共组件,提供给各个项目团队使用。而在SpringBoot的项目中,一般会将这些公共组件封装为SpringBoot的starter(包含了起步依赖和自动配置的功能)

- 需求:自定义aliyun-oss-spring-boot-starter,完成阿里云OSS操作工具类AliyunOSSOperator的自动配置
- 目标:引入起步依赖后,想要使用阿里云OSS,注入AliyunOSSOperator直接使用即可
- 步骤: 1. 创建aliyun-oss-spring-boot-starter模块 2. 创建aliyun-oss-spring-boot-autoconfigure模块,在starter中引入该模块 3. 在aliyun-oss-spring-boot-autoconfigure模块中定义自动配置功能,并定义自动配置文件META-INF/spring/xxxx.imports
aliyun-oss-spring-boot-autoconfigure下的依赖(就是业务逻辑代码中需要的依赖 + Springboot起步依赖):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 阿里云OSS依赖 -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.3</version>
</dependency>
aliyun-oss-spring-boot-starter下的依赖(引入自动配置模块的依赖即可):
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-oss-spring-boot-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
阿里云OSS配置模块
首先是业务逻辑的相关代码,直接从Tlias系统中复制过来即可:
AliyunOSSOperator,要把@Component注解去掉,因为不通过扫描组件的方式引入bean对象:
public class AliyunOSSOperator {
private final AliyunOSSProperties aliyunOSSProperties;
public AliyunOSSOperator(AliyunOSSProperties aliyunOSSProperties) {
this.aliyunOSSProperties = aliyunOSSProperties;
}
public String upload(byte[] content, String originalFilename) throws Exception {
String endpoint = aliyunOSSProperties.getEndpoint();
String bucketName = aliyunOSSProperties.getBucketName();
String region = aliyunOSSProperties.getRegion();
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
// 填写Object完整路径,例如202406/1.png。Object完整路径中不能包含Bucket名称。
//获取当前系统日期的字符串,格式为 yyyy/MM
String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));
//生成一个新的不重复的文件名
String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));
String objectName = dir + "/" + newFileName;
// 创建OSSClient实例。
ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
OSS ossClient = OSSClientBuilder.create()
.endpoint(endpoint)
.credentialsProvider(credentialsProvider)
.clientConfiguration(clientBuilderConfiguration)
.region(region)
.build();
try {
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content));
} finally {
ossClient.shutdown();
}
return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + objectName;
}
}
AliyunOSSProperties属性配置类也同样去掉@Component,只留一个引入配置文件属性的注解:
package com.aliyun.oss;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliyunOSSProperties {
private String endpoint;
private String bucketName;
private String region;
public AliyunOSSProperties() {
}
public AliyunOSSProperties(String endpoint, String bucketName, String region) {
this.endpoint = endpoint;
this.bucketName = bucketName;
this.region = region;
}
public String getEndpoint() {
return endpoint;
}
public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}
public String getBucketName() {
return bucketName;
}
public void setBucketName(String bucketName) {
this.bucketName = bucketName;
}
public String getRegion() {
return region;
}
public void setRegion(String region) {
this.region = region;
}
@Override
public String toString() {
return "AliyunOSSProperties{" +
"endpoint='" + endpoint + '\'' +
", bucketName='" + bucketName + '\'' +
", region='" + region + '\'' +
'}';
}
}
最后就是将工具类通过AliyunOSSAutoConfiguration配置类保存到IOC容器中:
/**
* AliyunOSS的自动配置类
*/
@EnableConfigurationProperties(AliyunOSSProperties.class)
@Configuration
public class AliyunOSSAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public AliyunOSSOperator aliyunOSSOperator(AliyunOSSProperties aliyunOSSProperties) {
return new AliyunOSSOperator(aliyunOSSProperties);
}
}
最重要的一步,在resource目录下创建META-INF/Spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件(文件名、路径不能错),并引入配置类:
com.aliyun.oss.AliyunOSSAutoConfiguration
这样当其他项目使用时SpringBoot就会自动配置并将工具类作为bean对象保存到IOC容器中
测试
在测试的项目中引入依赖:
<!-- 引入自定义starter -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-oss-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
其他依赖会通过依赖传递的方式引入
测试代码:
@RestController
public class UploadController {
private final AliyunOSSOperator aliyunOSSOperator;
public UploadController(AliyunOSSOperator aliyunOSSOperator) {
this.aliyunOSSOperator = aliyunOSSOperator;
}
@PostMapping("/upload")
public String upload(MultipartFile image) throws Exception {
//上传文件到阿里云 OSS
String url = aliyunOSSOperator.upload(image.getBytes(), image.getOriginalFilename());
System.out.println(url);
return null;
}
}