My App

Spring Boot 原理

1. 配置优先级

在 Spring Boot 中,常见的属性配置来源有 5 种(从低到高):

  1. application.yaml(很少使用,可忽略)
  2. application.yml
  3. application.properties
  4. Java 系统属性-Dxxx=xxx
  5. 命令行参数--xxx=xxx

当同一个配置键在多种位置都出现时,优先级更高的来源会覆盖优先级更低的配置
例如同时存在:

  • application.yml 中配置 server.port: 8080
  • 命令行参数 --server.port=10010

最终生效的端口是 10010(命令行参数优先级最高)。

虽然 Spring Boot 支持多种格式的配置文件,但在实际项目开发中,推荐统一使用一种配置文件格式,以便团队协作和维护;目前主流做法是统一使用 application.yml

2. 外部化配置:Java 系统属性与命令行参数

除了项目内部的 3 种配置文件外,Spring Boot 为了增强扩展性,还支持两种常见的外部配置方式

  • Java 系统属性-Dkey=value(如 -Dserver.port=9000
  • 命令行参数--key=value(如 --server.port=10010

即使项目已经打成 JAR 包上线,我们仍然可以在启动命令中通过这两种方式覆盖配置文件中的值:

# 同时设置 Java 系统属性 和 命令行参数
java -Dserver.port=9000 -jar app.jar --server.port=10010

在这种情况下:

  • application.yml / application.properties 中的 server.port 会被 Java 系统属性 覆盖;
  • Java 系统属性中的 server.port=9000 又会被 命令行参数 --server.port=10010 覆盖,最终端口为 10010。

注意:Spring Boot 项目打包成可执行 JAR 时,需要在 pom.xml 中引入(或保留)spring-boot-maven-plugin 插件;使用官网骨架创建项目时会自动添加该插件。

3. Bean 的作用域

在前面讲 IOC 容器时提到:默认情况下,Spring 中的 Bean 是单例的(同名 Bean 在容器中只有一个实例)。
实际上,Spring 支持 五种作用域,其中后三种只在 Web 环境 中生效:

作用域说明
singleton容器内同名称的 Bean 只有一个实例(单例),默认作用域
prototype每次获取该 Bean 时都会创建一个新的实例(非单例)。
request每个 HTTP 请求范围内创建新的实例(Web 环境中,了解即可)。
session每个会话范围内创建新的实例(Web 环境中,了解即可)。
application每个应用范围(ServletContext)内创建新的实例(Web 环境中,了解)。

3.1 使用 @Scope 指定作用域

如果我们希望修改某个 Bean 的作用域,可以借助 Spring 提供的 @Scope 注解 进行配置,例如将某个 Controller 配置为原型(prototype):

使用 @Scope 指定 Bean 作用域

等价代码示例:

@Scope("prototype")          // 每次注入或获取时都创建新的 DeptController 实例
@RequestMapping("/depts")
@RestController
public class DeptController {
}

实际项目中,大多数 Bean(尤其是 Service / Repository)仍建议使用默认的 singleton;只有在确有需求时,才调整为 prototype 或 Web 相关作用域。

3.2 @Lazy:延迟初始化单例 Bean

默认情况下,singleton 作用域的 Bean 会在容器启动时就被创建(饿汉式加载)。
如果某些 Bean 比较“重”(初始化成本高)或并不是每次启动都一定会用到,可以使用 @Lazy 注解 将其改为延迟初始化 —— 第一次真正被使用时才创建实例

@Lazy                  // 延迟到第一次注入 / 调用时再创建该 Bean
@Service
public class HeavyService {
    // ...
}

小结:@Scope("singleton") + 无 @Lazy → 容器启动时立即创建;
加上 @Lazy → 延迟到首次使用时再创建,有助于缩短启动时间、按需加载资源。

4. 面试常问:Bean 单例与线程安全

4.1 Spring 容器中的 Bean 是单例还是多例?单例 Bean 什么时候实例化?

  • 默认是单例(singleton:同名 Bean 在容器中只有一个实例。
  • 默认情况下,单例 Bean 会在容器启动时就完成实例化(可以通过 @Lazy 改为延迟初始化)。

4.2 Spring 容器中的 Bean 是线程安全的吗?

Bean 的线程安全与是否有状态、以及作用域有关:

  • 若是 无状态的 Bean(内部不保存任何与请求相关的可变状态,例如只依赖方法参数,不持有可变成员字段),即使是单例,一般也是线程安全的
  • 若是 有状态的 Bean(内部保存可变状态信息,如把用户数据、计数器等放在成员变量里),在多线程同时访问 / 修改时,可能出现数据不一致等问题,这样的单例 Bean 就是线程不安全的

实战建议:

  • Service / Repository 等 Bean 通常设计为无状态单例,每次请求的数据通过方法参数传入。
  • 如确实需要保存会话级 / 请求级状态,应考虑使用请求参数、ThreadLocal 或 Web 作用域 Bean,而不是把状态直接放在单例 Bean 的成员变量中。

5. 第三方 Bean 的配置(@Bean

前面我们声明的 Bean(Controller / Service / DAO 等)都是自己项目中编写的类,可以直接在类上使用 @Component 及其衍生注解(@Controller@Service@Repository)让 Spring 扫描并注册。

但在实际开发中,还有一类 “第三方 Bean”

  • 这些类来自引入的第三方依赖(JAR 包),不是我们自己写的;
  • 无法在源码上添加 @Component@Service 等注解。

这时就需要使用 @Bean 注解来声明 Bean。

小结:自定义类优先用 @Component / @Service 等;第三方类无法改源码时,用 @Bean 声明到容器中。

5.1 演示 1:在启动类中直接声明 @Bean

最简单的方式是在 启动类 中直接声明第三方 Bean,例如将阿里云 OSS 操作工具类注册为 Bean:

import com.itheima.utils.AliyunOSSOperator;
import com.itheima.utils.AliyunOSSProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling;

@ServletComponentScan
@EnableScheduling
@SpringBootApplication
public class TliasWebManagementApplication {

    public static void main(String[] args) {
        SpringApplication.run(TliasWebManagementApplication.class, args);
    }

    @Bean
    public AliyunOSSOperator aliyunOSSOperator(AliyunOSSProperties ossProperties) {
        // 如果第三方 Bean 需要依赖其他 Bean,可直接通过方法形参注入(按类型自动装配)
        return new AliyunOSSOperator(ossProperties);
    }
}

使用方式示例(单元测试中注入使用):

@SpringBootTest
class TliasWebManagementApplicationTests {

    @Autowired
    private AliyunOSSOperator aliyunOSSOperator;

    @Test
    void testUploadFiles() throws Exception {
        byte[] content = FileUtil.readBytes(new File("C:\\\\Users\\\\deng\\\\Pictures\\\\6.jpg"));
        String url = aliyunOSSOperator.upload(content, "6.jpg");
        System.out.println(url);
    }

    @Test
    void testListFiles() throws Exception {
        List<String> objectNameList = aliyunOSSOperator.listFiles();
        objectNameList.forEach(System.out::println);
    }

    @Test
    void testDelFiles() throws Exception {
        aliyunOSSOperator.deleteFile("2024/06/43b4....84.jpg");
    }
}

5.2 演示 2:使用配置类集中管理第三方 Bean(推荐)

如果项目中有多种第三方 Bean,建议通过 @Configuration 配置类进行集中管理,结构更清晰、职责更单一:

package com.itheima.config;

import com.itheima.utils.AliyunOSSOperator;
import com.itheima.utils.AliyunOSSProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OSSConfig {

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

5.3 @Bean 的一些细节

  • Bean 名称
    • 默认情况下,@Bean 方法名就是 Bean 的名称,例如上例中的 aliyunOSSOperator
    • 也可以通过 @Bean(name = "ossOperator")@Bean("ossOperator") 显式指定 Bean 名称。
  • 依赖注入
    • 第三方 Bean 若依赖其他 Bean,只需在 @Bean 方法的形参中声明相应类型,Spring 会按类型自动注入(类似构造器注入)。
  • 适用场景
    • 只要是无法直接加 @Component 的类(第三方库、工厂方法返回的对象等),都可以通过 @Bean 注解把它们交给 Spring 容器管理。

On this page