缘博客
发布日期

Spring Boot原理:配置优先级、Bean管理与自动配置

作者
  • 姓名
    社交账号

Spring Boot 帮我们省掉了什么

使用 Spring Boot 创建 Web 项目时,只要引入一个依赖、写少量配置,就能启动内嵌服务器、处理请求、注入常用组件。它并不是“没有配置”,而是把大量常见配置封装成约定和自动装配逻辑。

理解 Spring Boot 原理,可以围绕三个问题展开:

  1. 同一个配置出现多份时,最终使用哪一份?
  2. 一个对象为什么会成为 Bean,又应该何时创建?
  3. 引入 Starter 后,为什么很多 Bean 不用手写也能直接注入?

配置文件与配置优先级

Spring Boot 常见的配置文件格式有三种:

application.properties
application.yml
application.yaml

它们可以表达同一个配置。例如端口号:

# application.properties
server.port=8081
# application.yml 或 application.yaml
server:
  port: 8081

项目开发中建议统一选择一种格式,YAML 因层级清晰而很常见。不要在同一个项目里为同一项配置维护三份来源,否则排查“为什么配置不生效”会很困难。

除了配置文件,启动时还能通过 Java 系统属性和命令行参数覆盖配置:

java -Dserver.port=9000 -jar app.jar
java -jar app.jar --server.port=10010

针对同一个配置项,课程中这几种来源的优先级从低到高为:

application.yaml
application.yml
application.properties
Java 系统属性:-Dkey=value
命令行参数:--key=value

也就是说,命令行参数会覆盖 -D 系统属性,二者又会覆盖文件中的同名配置。这非常适合部署时临时调整端口、环境地址等参数,而无需改动打包后的文件。

要把 Spring Boot 项目打成可运行 Jar,Maven 通常需要 Spring Boot 的打包插件;从官方骨架创建的项目一般已自动包含:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

执行 mvn package 后,再用 java -jar 运行即可。实际项目还可能有 profile、环境变量、外部配置目录等来源;当配置冲突难以判断时,应查看启动日志或 Actuator 环境端点,而不是靠猜测。


Bean 作用域:一个对象会创建几次

Bean 是由 Spring IOC 容器创建和管理的对象。默认情况下,Bean 的作用域是 singleton:容器中同一个 Bean 名称只保留一个实例。

Spring 常见作用域如下:

作用域含义使用场景
singletonIOC 容器中只有一个实例,默认值Service、Mapper、Controller 等无状态组件
prototype每次获取 Bean 时新建一个实例有短暂内部状态、不能共享的对象
request每个 HTTP 请求一个实例Web 环境中的请求级对象
session每个会话一个实例Web 环境中的会话级对象
application每个 ServletContext 一个实例Web 应用范围共享对象

可以通过 @Scope 指定作用域:

@Component
@Scope("prototype")
public class TaskContext {
}

绝大多数 Controller、Service、Mapper 都应该保持单例。单例并不等于“永远线程安全”:如果把一次请求的可变数据放到成员变量中,多线程访问仍会有问题。一般把状态放在方法局部变量、数据库或专门的上下文中。

默认单例 Bean 会在容器启动时创建。若某个 Bean 初始化昂贵、且并非每次启动都要用,可以加 @Lazy 延迟到第一次使用时再创建:

@Lazy
@Service
public class ReportService {
}

延迟初始化会把一部分启动成本推迟到首次请求;是否使用应根据实际启动时间和访问频率判断。


自定义 Bean 与第三方 Bean

项目中自己编写的类,可以使用组件注解交给 Spring 管理:

@Component
@Controller
@Service
@Repository

这些注解标记的类必须位于组件扫描范围内。@SpringBootApplication 默认扫描启动类所在包及其子包,因此启动类通常放在项目根包。

来自第三方依赖的类不能直接加 @Component,这时使用 @Bean 方法创建并注册对象。建议把这类配置集中放进 @Configuration 类:

@Configuration
public class OssConfig {

    @Bean
    public AliyunOSSOperator aliyunOSSOperator(
            AliyunOSSProperties ossProperties) {
        return new AliyunOSSOperator(ossProperties);
    }
}

@Bean 默认以方法名作为 Bean 名称;方法参数则由容器按类型自动注入。上例中,AliyunOSSProperties 已经是容器中的 Bean 时,Spring 会把它传入 aliyunOSSOperator()

可以用一句话区分两者:能修改源码的自定义类,通常用 @Component 及其派生注解;来自第三方库、无法加注解的类,通常用 @Bean


起步依赖:一次引入一组常用能力

传统 Java 项目常需要手动找齐很多相互关联的依赖。Spring Boot 把一类场景所需的常用依赖组织成 Starter(起步依赖),例如:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

引入 spring-boot-starter-web 后,Maven 会通过依赖传递引入 Web 开发所需的一组组件。类似地,还有 AOP、测试、数据访问等不同 Starter。

Starter 解决的是“依赖怎么带进来”的问题,但它还不是全部答案。仅仅让类出现在 classpath 上,并不会自动把所有对象变成 Bean;真正让 Spring Boot 根据当前依赖和配置创建组件的是自动配置。


自动配置:按条件把合适的 Bean 放进容器

自动配置可以理解为:应用启动后,Spring Boot 导入一批候选配置类;每个配置类再根据当前环境、依赖和配置决定是否注册 Bean。

启动类上的 @SpringBootApplication 是理解入口。它主要组合了三部分能力:

@SpringBootConfiguration
@ComponentScan
@EnableAutoConfiguration
public @interface SpringBootApplication {
}
注解作用
@SpringBootConfiguration表明启动类也是配置类,本质上与 @Configuration 相关
@ComponentScan扫描当前包及其子包中的组件
@EnableAutoConfiguration导入 Spring Boot 提供的自动配置逻辑

第三方组件不能被默认组件扫描发现时,有两种常见做法:

  1. 使用 @ComponentScan 扩大扫描范围。
  2. 使用 @Import 显式导入。

@Import 可以导入普通类、配置类、ImportSelector 的实现类,也经常被 @EnableXxx 这类启用注解封装。它比把第三方大包全部纳入组件扫描更精确,也避免无意义的扫描开销。

自动配置并非“把候选类全部注册”。它依赖 @Conditional 系列条件注解,只在条件满足时装配 Bean:

@Configuration
@ConditionalOnClass(AliyunOSSOperator.class)
public class OssAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public AliyunOSSOperator aliyunOSSOperator() {
        return new AliyunOSSOperator();
    }
}

常见条件包括:

注解判断条件
@ConditionalOnClassclasspath 中存在指定类
@ConditionalOnMissingBean容器中不存在指定类型或名称的 Bean
@ConditionalOnProperty配置中存在指定属性且值匹配

这带来两个重要结果:只有需要的功能才会生效;应用也可以通过自己声明 Bean 覆盖默认实现。阅读自动配置时,最值得先看的就是它的条件注解和 @Bean 方法。


自动配置类从哪里被发现

Spring Boot 需要先找到候选自动配置类,再交由条件注解筛选。

课程中提到:Spring Boot 2.7.0 以前,自动配置类主要通过 META-INF/spring.factories 声明。现代 Spring Boot 则使用更明确的文件:

META-INF/spring/
org.springframework.boot.autoconfigure.AutoConfiguration.imports

该文件每行列出一个自动配置类的全限定名。Spring Boot 导入候选配置后,再由 @Conditional 决定哪些 Bean 真正进入 IOC 容器。

因此,“Starter 一引入,所有配置都会生效”是不准确的。更准确的过程是:

引入 Starter
相关类和自动配置声明进入 classpath
Spring Boot 找到候选自动配置类
@Conditional 根据依赖、配置和已有 Bean 筛选
符合条件的 Bean 注册到 IOC 容器

自定义 Starter:把公共组件做成开箱即用的依赖

多个项目都要使用同一个组件时,可以把它封装成自己的 Starter。例如把阿里云 OSS 操作工具、配置绑定和自动装配逻辑封装起来,业务项目只需引入依赖并注入工具类。

常见结构分为两个模块:

aliyun-oss-spring-boot-starter
  └─ 负责依赖管理,依赖 autoconfigure 模块

aliyun-oss-spring-boot-autoconfigure
  ├─ AliyunOssAutoConfiguration
  ├─ @ConfigurationProperties 配置类
  └─ META-INF/spring/...AutoConfiguration.imports

自动配置模块中定义配置属性和 Bean:

@ConfigurationProperties(prefix = "aliyun.oss")
public class AliyunOssProperties {
    private String endpoint;
    private String bucketName;

    // getter、setter 省略
}
@Configuration
@EnableConfigurationProperties(AliyunOssProperties.class)
@ConditionalOnClass(AliyunOSSOperator.class)
public class AliyunOssAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public AliyunOSSOperator aliyunOSSOperator(
            AliyunOssProperties properties) {
        return new AliyunOSSOperator(properties);
    }
}

业务项目配置好 aliyun.oss 前缀的属性后,直接注入 AliyunOSSOperator 即可。这样,依赖版本、默认配置和装配规则都集中在 Starter 中,业务项目不必复制一套 @Configuration

自定义 Starter 应保持聚焦:一个 Starter 服务于一个清晰能力,并让默认行为合理、配置项可发现、用户自定义 Bean 可以覆盖默认 Bean。


小结:按这条链路排查问题

当 Spring Boot 中出现“端口没有按预期变化”“Bean 注入失败”或“引入依赖后功能没生效”时,可以按下面的顺序排查:

  1. 配置是否被更高优先级的来源覆盖。
  2. 类是自定义组件还是第三方组件,应使用组件扫描还是 @Bean
  3. Bean 的包是否落在启动类的扫描范围内。
  4. Starter 是否真的被引入,传递依赖是否存在。
  5. 自动配置的 @Conditional 是否满足,是否已有 Bean 覆盖了默认配置。
  6. 当前 Spring Boot 版本使用的是哪一种自动配置声明文件。

这条链路把“配置、Bean、依赖、自动配置”连接在一起。理解它之后,Spring Boot 的便利不再像黑盒,而是可以被观察、验证和定制的运行机制。