返回八股知识点
八股知识点 / 发布 2026-02-28 15:12 / 更新 2026-07-14 00:00

Spring Boot

整理 Spring Boot 启动核心注解、自动配置、可执行 JAR 结构、配置加载顺序、与 SSM 和 Spring Cloud 的区别、事务、Bean 生命周期、接口幂等等高频问题。

SpringSpring Boot

⚠️ 最重要强提醒:自动装配和自动配置不能搞混。

  • 自动装配:Spring Framework 的依赖注入能力,指容器把已有 Bean 注入到当前 Bean,例如 @Autowired@Resource、构造器注入。
  • 自动配置:Spring Boot 的能力,指 Boot 根据 classpath、配置项和条件注解自动向 IoC 容器注册默认 Bean,例如 @EnableAutoConfiguration
  • 一句话记忆:自动装配 = 注入依赖;自动配置 = 注册 Bean。

Spring Boot 的启动核心注解

书面笔记版

Spring Boot 启动类上最核心的注解是 @SpringBootApplication,它是一个组合注解。

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

@SpringBootApplication 主要包含三个核心注解:

注解 作用
@SpringBootConfiguration 标识当前类是 Spring Boot 配置类,本质上包含 @Configuration
@EnableAutoConfiguration 开启自动配置(很多资料口语会叫自动装配,但准确说是自动配置),根据依赖、配置项和条件注解注册 Bean
@ComponentScan 开启组件扫描,默认扫描启动类所在包及其子包

其中 @ComponentScan 负责扫描自己项目里的组件。它不是扫描整个项目磁盘,而是从启动类所在包开始,扫描这个包及其子包下带有 @Component@Service@Controller@Repository@Configuration 等注解的类,然后注册成 BeanDefinition

例如启动类在 com.example 包下:

com.example
├─ DemoApplication
├─ controller
├─ service
└─ mapper

默认扫描范围就是 com.example 及其子包。如果 UserService 放在 com.other.service,默认就扫不到,可能出现找不到 Bean 的问题。解决方式通常是把启动类放到更上层根包,或者显式配置 @ComponentScan(basePackages = "com.example")

@ComponentScan@EnableAutoConfiguration 关注点不同:

注解 主要负责
@ComponentScan 扫描业务代码里的组件 Bean
@EnableAutoConfiguration 根据依赖、配置项和条件注解注册框架默认 Bean

启动过程可以简化理解为:

  1. 执行 main 方法,调用 SpringApplication.run()
  2. 创建并刷新 Spring 容器。
  3. 扫描启动类所在包下的组件。
  4. 读取自动配置类,根据条件判断是否注册默认 Bean。
  5. 创建 Bean、注入依赖、启动内嵌 Web 容器。

常见注意点:

  • 启动类位置一般放在项目根包下,否则 @ComponentScan 可能扫不到其他包。
  • @Bean 方法本身不是被 @ComponentScan 直接扫描出来的;通常是先扫描到 @Configuration 配置类,再解析配置类中的 @Bean 方法。
  • 自动配置不是无脑创建 Bean,而是结合 classpath、配置项和 @Conditional 条件判断。
  • 自定义 Bean 通常可以覆盖框架默认 Bean。

面试口述版

Spring Boot 启动类核心注解是 @SpringBootApplication,它是组合注解,主要包含 @SpringBootConfiguration@EnableAutoConfiguration@ComponentScan@SpringBootConfiguration 表示这是配置类,@ComponentScan 默认扫描启动类所在包及子包下的业务组件,@EnableAutoConfiguration 根据依赖、配置项和条件注解注册框架默认 Bean。启动时通过 SpringApplication.run() 创建容器,扫描业务组件并读取自动配置,先注册 BeanDefinition,再创建 Bean、注入依赖,并启动内嵌 Web 容器。

Spring Boot 配置文件加载顺序

书面笔记版

Spring Boot 配置优先级遵循“高优先级覆盖低优先级、不同配置互补生效”的原则。

常见优先级由高到低:

  1. 命令行参数。
  2. Java 系统属性。
  3. 操作系统环境变量。
  4. 配置中心配置,比如 Nacos 中的部分配置。
  5. JAR 包外 /config 目录下的配置。
  6. JAR 包外根目录配置。
  7. JAR 包内 /config 目录下的配置。
  8. JAR 包内根目录配置。
  9. 默认配置。

同级配置文件后缀优先级通常是:

properties > yml > yaml

配置文件位置优先级可以记成:

  • 外部优先于内部:JAR 包外配置覆盖 JAR 包内配置。
  • config 目录优先于根目录/config 下优先级更高。
  • profile 配置优先于通用配置application-prod.yml 优先于 application.yml

高优先级不会让低优先级完全失效,只有相同 key 才覆盖;不同 key 会共同生效。

面试口述版

Spring Boot 配置加载可以按“外部优先、profile 优先、properties 优先”来记。命令行参数和配置中心通常优先级更高,JAR 包外配置优先于 JAR 包内配置,config 目录优先于根目录,同级里一般properties 高于ymlyaml。相同配置项高优先级覆盖低优先级,不同配置项会互补生效。

Spring Boot 和传统 SSM 区别

书面笔记版

Spring Boot 不是替代 Spring、Spring MVC、MyBatis,而是在它们之上做工程化封装,让项目更容易启动、配置和部署。

传统 SSM 一般指:

  • Spring:管理 Bean、事务、AOP。
  • Spring MVC:处理 Web 请求。
  • MyBatis:操作数据库。
对比点 传统 SSM Spring Boot
配置方式 XML 和手动配置较多 注解、配置文件和自动配置为主
依赖管理 需要手动组合依赖版本 Starter 统一封装依赖组合
Web 容器 常打 WAR 包部署到外部 Tomcat 默认内嵌 Tomcat/Jetty/Undertow,可直接运行 JAR
启动方式 依赖外部容器 main 方法启动
开发效率 初始化和配置成本较高 开箱即用,约定大于配置
运维部署 环境依赖更多 交付 JAR 或镜像即可

Spring Boot 的核心能力:

  • 自动配置:根据依赖、配置项和条件注解自动注册默认 Bean。
  • Starter:把常用依赖打包成场景化依赖。
  • 内嵌容器:应用自带 Web 容器。
  • 约定大于配置:默认配置覆盖大多数场景。
  • 生产能力:可结合 Actuator 做健康检查、指标和运维监控。

面试口述版

传统 SSM 是 Spring、Spring MVC、MyBatis 的组合,功能完整但配置比较重,很多 MVC、数据源、事务和扫描路径都要手动配置。Spring Boot 不是替代它们,而是在 SSM 基础上做自动配置和工程化封装,通过 Starter 管理依赖,通过内嵌 Tomcat 直接运行 JAR,通过约定大于配置减少 XML。简单说,SSM 更像手动组装,Spring Boot 是把常见组合提前封装好。

Spring Boot 可执行 JAR 的结构和运行方式

书面笔记版

JAR 本质上是一个 ZIP 压缩包,里面可以放 .class 文件、配置文件、资源文件和 META-INF/MANIFEST.MF。JVM 运行时不会一次性把所有 class 都加载进内存,而是通过 ClassLoader 按需查找和加载 class。

常见打包方式有三种:

类型 结构 依赖位置 常见运行方式
普通 JAR 通常只包含当前项目的 class 和资源 依赖放在外部 classpath java -cp app.jar;lib/* com.example.Main
Spring Boot fat JAR 外层一个可执行 JAR,内部包含业务 class 和依赖 JAR BOOT-INF/lib java -jar app.jar
Shade / Uber JAR 把当前项目和依赖的 class 展平合并进一个 JAR 依赖 class 被解压合并 java -jar app-all.jar

Spring Boot 默认通过 spring-boot-maven-plugin 或 Gradle Boot 插件重新打包成可执行 fat JAR。它通常是“JAR 套 JAR”的结构:

app.jar
├─ META-INF/
│  └─ MANIFEST.MF
├─ org/springframework/boot/loader/
│  └─ ...
└─ BOOT-INF/
   ├─ classes/
   │  ├─ application.yml
   │  └─ com/example/DemoApplication.class
   └─ lib/
      ├─ spring-core-xxx.jar
      ├─ spring-context-xxx.jar
      ├─ spring-boot-autoconfigure-xxx.jar
      └─ jackson-databind-xxx.jar

各目录含义:

目录 作用
BOOT-INF/classes 当前业务项目编译后的 .class、配置文件和资源
BOOT-INF/lib Maven / Gradle 引入的第三方依赖 JAR
org/springframework/boot/loader Spring Boot 启动加载器
META-INF/MANIFEST.MF 记录入口类、启动器等元信息

标准 JVM 的 classpath 机制主要识别普通目录和普通 JAR,不能直接把“内层 JAR”当成普通 classpath 入口递归加载。所以 Spring Boot 可执行 JAR 里会带一层 spring-boot-loader。执行 java -jar app.jar 时,先启动 Boot 的 JarLauncher,再由它创建专门的 ClassLoader,把 BOOT-INF/classesBOOT-INF/lib/*.jar 加入应用 classpath。

可以这样理解启动过程:

java -jar app.jar
  -> 读取 MANIFEST.MF
  -> 启动 spring-boot-loader
  -> 加载 BOOT-INF/classes 和 BOOT-INF/lib
  -> 调用业务启动类 main 方法
  -> SpringApplication.run()

如果解压 Spring Boot JAR,看到的不是所有依赖 class 都散落在外层,而是业务 class 在 BOOT-INF/classes,依赖 JAR 在 BOOT-INF/lib。如果是 Shade / Uber JAR,才更像“把很多依赖 JAR 解压后,所有 class 展平合并到一个 JAR 里”。

面试口述版

JAR 本质是 ZIP 包,JVM 通过 ClassLoader 按需加载里面的 class。普通 JAR 通常只放自己项目的 class,依赖靠外部 classpath。Spring Boot 默认打出来的是可执行 fat JAR,外层还是一个 JAR,但里面的 BOOT-INF/classes 放业务 class,BOOT-INF/lib 放依赖 JAR,所以更像 JAR 套 JAR。标准 JVM 不能直接递归加载内层 JAR,Spring Boot 通过自带的 spring-boot-loader 和专门的 ClassLoader 把这些路径加载起来。如果用 Shade 插件,则是另一种方式,会把依赖 class 解压后展平合并进一个 JAR。

Spring Boot 和 Spring Cloud 有什么区别

书面笔记版

Spring Boot 和 Spring Cloud 不是同一层面的东西。

对比点 Spring Boot Spring Cloud
定位 快速开发单个 Spring 应用 构建和治理分布式微服务系统
解决问题 自动配置、依赖管理、内嵌容器、快速启动 服务注册发现、配置中心、网关、负载均衡、熔断限流、链路追踪
使用范围 单体应用和微服务都能用 主要用于微服务架构
依赖关系 基础工程框架 通常基于 Spring Boot 应用扩展

可以这样理解:

  • Spring Boot 解决“一个服务怎么快速开发、配置和运行”。
  • Spring Cloud 解决“多个服务之间怎么发现、调用、治理和容错”。
  • 微服务项目里通常每个服务本身是一个 Spring Boot 应用,再引入 Spring Cloud 组件做注册、配置、网关和熔断等能力。

面试口述版

Spring Boot 主要是为了快速开发单个 Spring 应用,提供自动配置、Starter、内嵌 Tomcat 和约定大于配置。Spring Cloud 是微服务治理体系,解决多个服务之间的注册发现、配置中心、网关、负载均衡、熔断限流、链路追踪等问题。简单说,Boot 解决一个服务怎么快速跑起来,Cloud 解决一堆服务之间怎么协作和治理;实际微服务里通常每个服务都是 Boot 应用,再接入 Cloud 组件。

Spring Boot 怎么保证业务代码事务的

书面笔记版

Spring Boot 主要通过 Spring 的声明式事务 @Transactional 保证业务代码事务。它底层基于 AOP 代理和事务管理器,在业务方法执行前开启事务,方法正常执行完成后提交,方法执行过程中抛出异常时回滚。

常见事务管理器:

场景 事务管理器
JDBC / MyBatis DataSourceTransactionManager
JPA JpaTransactionManager

事务一般加在 Service 层的 public 方法上,因为一次业务操作通常会包含多个数据库操作。

@Service
public class OrderService {

    @Transactional(rollbackFor = Exception.class)
    public void createOrder(CreateOrderCmd cmd) {
        orderMapper.insert(cmd);
        stockMapper.deduct(cmd.getSkuId(), cmd.getCount());
        accountMapper.freeze(cmd.getUserId(), cmd.getAmount());

        if (cmd.getAmount().signum() <= 0) {
            throw new IllegalArgumentException("amount invalid");
        }
    }
}

上面代码里,创建订单、扣减库存、冻结账户金额在同一个事务中执行。只要方法抛出异常,数据库操作就会整体回滚。

最容易被问的是 @Transactional 失效场景:

失效场景 原因 处理方式
方法不是 public Spring AOP 默认主要代理 public 方法 事务方法放在 Service 层 public 方法上
方法是 privatefinalstatic 代理无法拦截或无法重写目标方法 避免在这些方法上直接加事务
同类内部方法调用 this.xxx() 不经过 Spring 代理 拆到另一个 Service,或通过代理对象调用
当前类没有交给 Spring 管理 自己 new 的对象不是 Spring Bean 通过 Spring 注入使用 Bean
异常被 catch 后吞掉 方法正常返回,事务管理器会提交 继续抛出异常,或手动标记回滚
抛出检查异常 默认只回滚 RuntimeExceptionError 配置 rollbackFor = Exception.class
开启新线程或使用 @Async 事务上下文基于线程绑定,新线程拿不到原事务 异步逻辑单独加事务,或通过消息/事件解耦
传播行为配置不当 比如 NOT_SUPPORTED 会挂起事务 普通业务优先使用默认 REQUIRED
多数据源事务管理器用错 当前事务管理器只能管理绑定的数据源 指定正确的 transactionManager
数据库或表不支持事务 例如 MySQL MyISAM 引擎不支持事务 使用支持事务的引擎,比如 InnoDB
操作 Redis、MQ、远程接口 本地事务只能回滚数据库连接内的操作 结合幂等、补偿、事务消息或 Outbox 模式

异常被 catch 后吞掉是高频考点。rollbackFor = Exception.class 只是在异常抛到事务代理层时扩大回滚范围,如果方法内部把异常吞掉,代理看到的是正常返回,事务会提交。

@Service
public class OrderService {

    @Transactional(rollbackFor = Exception.class)
    public void createOrder(CreateOrderCmd cmd) {
        try {
            orderMapper.insert(cmd);
            stockMapper.deduct(cmd.getSkuId(), cmd.getCount());
            throw new RuntimeException("mock exception");
        } catch (Exception e) {
            log.error("create order failed", e);
            // 反例:异常没有继续抛出,也没有标记回滚,方法正常返回后事务会提交
        }
    }
}

常见修正方式有两种:

@Transactional(rollbackFor = Exception.class)
public void createOrder(CreateOrderCmd cmd) {
    try {
        orderMapper.insert(cmd);
        stockMapper.deduct(cmd.getSkuId(), cmd.getCount());
    } catch (Exception e) {
        throw new RuntimeException("create order failed", e);
    }
}
@Transactional(rollbackFor = Exception.class)
public void createOrder(CreateOrderCmd cmd) {
    try {
        orderMapper.insert(cmd);
        stockMapper.deduct(cmd.getSkuId(), cmd.getCount());
    } catch (Exception e) {
        log.error("create order failed", e);
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

Spring 事务传播机制

事务传播机制解决的是:一个带事务的方法调用另一个带事务的方法时,内部方法应该加入当前事务、创建新事务,还是暂时脱离事务执行。

先区分两个概念:

  • 逻辑事务:每个 @Transactional 方法形成的事务边界。
  • 物理事务:数据库连接上真正执行并最终提交或回滚的事务。

多个 REQUIRED 方法可以有多个逻辑事务边界,但最终共享同一个物理事务;REQUIRES_NEW 则会创建另一个独立物理事务。

传播机制 已存在外层事务 不存在外层事务 常用场景
REQUIRED 加入当前事务 新建事务 默认行为,订单、库存、余额等必须整体成功
REQUIRES_NEW 挂起外层事务,创建独立事务 新建事务 审计日志、独立流水、失败记录
NESTED 在当前事务中建立 Savepoint 新建事务,行为类似 REQUIRED 批处理允许单项回滚,外层仍可继续
SUPPORTS 加入当前事务 以非事务方式执行 可同时被事务和非事务流程复用的只读查询
NOT_SUPPORTED 挂起外层事务,以非事务方式执行 以非事务方式执行 报表、非事务查询、明确不应加入事务的操作
MANDATORY 加入当前事务 抛出异常 余额、库存等必须由上层事务统一编排的底层能力
NEVER 抛出异常 以非事务方式执行 明确禁止在事务中运行的长任务或非事务流程
1. REQUIRED:加入现有事务,没有就新建

REQUIRED 是默认传播行为,适合一组数据库操作必须同时成功或同时失败的场景。

@Service
public class UserApplicationService {

    private final UserService userService;
    private final OrderService orderService;

    @Transactional(rollbackFor = Exception.class)
    public void register(RegisterCommand command) {
        userService.saveUser(command);
        orderService.createInitialOrder(command);
    }
}

@Service
public class OrderService {

    @Transactional(propagation = Propagation.REQUIRED,
                   rollbackFor = Exception.class)
    public void createInitialOrder(RegisterCommand command) {
        // 写入初始订单
    }
}

执行过程:

事务 A:register
|
+-- saveUser()                 加入事务 A
|
+-- createInitialOrder()      REQUIRED,加入事务 A
|
+-- register 正常结束         提交事务 A

如果创建订单抛出异常并继续向外传播,事务 A 整体回滚,用户和订单都不会提交。

REQUIRED 有一个高频陷阱:内层方法把共享事务标记为 rollback-only 后,外层即使 catch 住异常继续执行,也不能再真正提交。

@Transactional
public void register(RegisterCommand command) {
    userService.saveUser(command);
    try {
        orderService.createInitialOrder(command);
    } catch (RuntimeException e) {
        log.warn("ignore order failure", e);
    }
    // 代码可以继续运行,但共享的事务 A 已经被标记为 rollback-only
}

外层方法最后尝试提交时,Spring 会回滚事务并抛出 UnexpectedRollbackException,避免调用方误以为已经提交成功。

2. REQUIRES_NEW:挂起旧事务,创建独立事务

REQUIRES_NEW 永远使用独立物理事务。存在外层事务时,Spring 先挂起外层事务及其资源,内层事务完成后再恢复外层事务。

@Service
public class PaymentService {

    private final PaymentRecordRepository paymentRecordRepository;
    private final AuditLogService auditLogService;

    @Transactional(rollbackFor = Exception.class)
    public void pay(PayCommand command) {
        try {
            paymentRecordRepository.insert(command);
            deductBalance(command); // 假设这里失败
            updatePaymentSuccess(command);
        } catch (RuntimeException e) {
            auditLogService.saveFailureLog(command, e.getMessage());
            throw e;
        }
    }
}

@Service
public class AuditLogService {

    @Transactional(propagation = Propagation.REQUIRES_NEW,
                   rollbackFor = Exception.class)
    public void saveFailureLog(PayCommand command, String reason) {
        // 独立写入失败审计记录
    }
}

执行过程:

事务 A:pay
|
+-- insert payment record
|
+-- deductBalance 抛异常
|
+-- catch 异常,挂起事务 A
|   |
|   +-- 开启事务 B:saveFailureLog
|   +-- 提交事务 B 并释放事务 B 的锁和连接
|
+-- 恢复事务 A并重新抛出异常
|
+-- 回滚事务 A

最终:事务 A 的支付记录回滚,事务 B 的审计日志仍然存在。

内外层失败组合:

情况 结果
内层 B 提交,外层 A 后续失败 B 保留,A 回滚
内层 B 回滚,外层捕获异常并正常结束 B 回滚,A 可以提交
内层 B 抛异常且继续传播到外层 B 回滚;A 通常也会因收到异常而回滚
外层 A 在调用内层前已经写入数据 A 的数据仍未提交,B 无法看到 A 未提交的数据

常用场景包括审计日志、独立流水、失败原因记录等。但要注意:

  • 必须通过另一个 Spring Bean 调用,this.saveLog() 不经过代理,无法创建新事务。
  • 外层事务占用一个连接,内层事务还要再申请连接。高并发下连接池过小可能耗尽甚至形成等待。
  • REQUIRES_NEW 只能保证本数据库事务独立,不等于能保证 MQ、远程日志服务等跨系统操作原子成功。
  • 高可靠业务事件通常还要结合事务同步、Outbox 或事务消息,不能滥用独立事务替代一致性设计。
3. NESTED:同一物理事务中的保存点

NESTED 不会创建独立物理事务,而是在外层事务中建立 Savepoint。内层失败时可以回滚到保存点,外层决定是否继续;如果外层最终回滚,整个物理事务仍会全部回滚。

@Service
public class BatchImportService {

    private final ItemImportService itemImportService;

    @Transactional
    public void importAll(List<ItemCommand> items) {
        for (ItemCommand item : items) {
            try {
                itemImportService.importOne(item);
            } catch (RuntimeException e) {
                // 当前 item 回滚到保存点,继续导入其他 item
                log.warn("item import failed: {}", item.getId(), e);
            }
        }
    }
}

@Service
public class ItemImportService {

    @Transactional(propagation = Propagation.NESTED)
    public void importOne(ItemCommand item) {
        // 写入一组与当前 item 有关的数据
    }
}
事务 A:importAll
|
+-- Savepoint S1 -> item1 成功
+-- Savepoint S2 -> item2 失败,回滚到 S2
+-- Savepoint S3 -> item3 成功
|
+-- 外层提交:item1、item3 提交

如果外层事务 A 最后失败:item1、item3 也会一起回滚。

NESTED 通常依赖 JDBC Savepoint,常见于 DataSourceTransactionManager。具体事务管理器和数据库是否支持必须确认,不能默认认为 JPA、JTA 或所有数据源都支持相同行为。

4. 其余传播机制的具体场景
  • SUPPORTS:商品详情查询既可能被普通接口直接调用,也可能被下单事务调用。直接调用时不创建事务;在事务中调用时加入当前事务。没有事务时,多次查询不保证处于同一一致性快照。
  • NOT_SUPPORTED:报表统计或非事务查询可以明确不加入当前事务;存在外层事务时,Spring 会先挂起外层事务。但挂起不等于提交,外层连接和锁通常仍要等外层结束才释放,所以耗时 HTTP 调用最好直接移到事务边界之外。该方法中的非事务数据库写入可能立即提交,不能被外层回滚。
  • MANDATORYdeductStock()changeBalance() 等底层能力不允许单独调用,要求订单或支付服务必须先开启事务;没有事务时立即抛出 IllegalTransactionStateException,便于尽早发现错误调用。
  • NEVER:某个批量导出或长时间扫描任务明确要求不在事务中执行;如果上层带事务调用,就抛出 IllegalTransactionStateException,防止无意中形成超长事务。

传播机制生效的前提是调用经过 Spring 事务代理。下面这种同类自调用不会触发内层传播配置:

@Transactional
public void outer() {
    this.inner(); // 没经过代理,inner 上的 REQUIRES_NEW 或 NESTED 不生效
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void inner() {
}

通常应把内层事务方法拆到另一个 Service,并通过 Spring 注入的 Bean 调用。

面试口述版

Spring Boot 通过 Spring 声明式事务 @Transactional 保证业务事务,底层是 AOP 代理加事务管理器。传播机制决定事务方法互相调用时如何处理事务:REQUIRED 有事务就加入、没有就创建,是默认选择;REQUIRES_NEW 会挂起外层并创建独立物理事务,适合需要独立提交的审计或流水,但会额外占用数据库连接;NESTED 在同一物理事务中建立 Savepoint,适合批处理局部回滚,但外层回滚时仍会全部回滚。SUPPORTS 可有可无,NOT_SUPPORTED 暂停事务,MANDATORY 强制必须有事务,NEVER 强制不能有事务。实际使用还要注意:传播配置必须经过 Spring 代理,同类 this.xxx() 调用不会生效;REQUIRED 内层把共享事务标记为 rollback-only 后,外层即使 catch 异常,提交时仍可能收到 UnexpectedRollbackException。此外,方法不是 public、对象不是 Spring Bean、异常被吞掉、检查异常没配置 rollbackFor、新线程或 @Async、事务管理器用错等也会导致事务失效或结果不符合预期。对于 Redis、MQ、远程接口这类数据库事务管不到的操作,还要结合幂等、补偿、Outbox 或事务消息保证最终一致性。

Spring Bean 生命周期和初始化扩展点

书面笔记版

Spring 容器负责创建 Bean、注入依赖、执行初始化回调,最后在容器关闭时销毁 Bean。

常见扩展点:

扩展点 执行时机 适合场景
@PostConstruct 当前 Bean 依赖注入完成后 单个 Bean 的轻量初始化
InitializingBean#afterPropertiesSet() 属性设置完成后 Spring 专用初始化接口
@Bean(initMethod = "...") @Bean 方法创建对象后 第三方对象初始化
@PreDestroy Bean 销毁前 释放资源
BeanPostProcessor 所有 Bean 初始化前后 框架扩展、统一代理、统一增强
CommandLineRunner Spring Boot 应用启动完成后 启动后执行一次任务
ApplicationRunner Spring Boot 应用启动完成后 需要解析启动参数的启动任务

@PostConstruct 适合做轻量初始化,比如加载本地缓存、检查必要配置、初始化内存结构。

@Component
public class CacheLoader {

    @PostConstruct
    public void init() {
        // Bean 依赖注入完成后执行
    }
}

@PreDestroy 适合释放资源,比如关闭线程池、断开连接、刷新缓冲区。

@Component
public class ResourceHolder {

    @PreDestroy
    public void destroy() {
        // 容器关闭前释放资源
    }
}

如果逻辑是“整个应用启动完成后执行一次”,更适合 CommandLineRunnerApplicationRunner;如果只是某个 Bean 初始化后执行,优先考虑 @PostConstruct

面试口述版

Bean 初始化后执行逻辑一般用 @PostConstruct,它会在依赖注入完成后执行;销毁前执行逻辑用 @PreDestroy。如果要在整个 Spring Boot 应用启动完成后执行一次任务,比如预热缓存或启动后检查,可以用 CommandLineRunnerApplicationRunnerInitializingBeaninitMethod也能做初始化,BeanPostProcessor更偏底层扩展,可以拦截所有 Bean 的初始化过程。

接口幂等和防重复提交

书面笔记版

幂等性指同一个业务请求执行一次和执行多次,最终结果一致。防重复提交解决的是用户连续点击、网络超时重试、服务重试、MQ 重复投递、第三方回调重复通知等问题。

常见方案:

方案 适合场景 核心思路
前端防抖 用户连续点击 按钮置灰、提交中状态,只是体验优化
Token 防重 表单提交、创建订单 服务端发一次性 token,提交时校验并删除
Redis SET NX EX 高并发接口防重 用业务唯一键抢占处理资格并设置过期时间
数据库唯一索引 下单、支付、开户 用唯一约束兜底,防止重复写入
状态机 支付、订单流转 只有合法状态才能流转
分布式锁 同一资源串行处理 同一业务 key 同一时刻只允许一个线程处理
MQ 消费幂等 消息可能重复投递 消费表、唯一索引、业务状态判断

Token 防重流程:

  1. 用户进入提交页面时,服务端生成 token。
  2. token 存入 Redis,并返回给前端。
  3. 前端提交表单时带上 token。
  4. 后端用 Lua 或原子删除校验 token。
  5. 校验成功才执行业务,校验失败说明重复提交。

Redis 幂等 key 示例:

idem:order:{userId}:{requestNo}
idem:pay:{payNo}
idem:callback:{thirdTradeNo}

处理流程是先执行 SET key value NX EX 300。设置成功说明第一次请求,可以继续处理;设置失败说明正在处理或已经处理过,可以返回处理中或历史结果。

核心业务必须有数据库兜底,例如订单表加 (user_id, request_no) 唯一索引,支付流水表加 pay_no 唯一索引。订单、支付、退款等状态流转还要带状态条件更新,避免重复回调或重复消费。

面试口述版

幂等就是同一个业务请求执行一次和多次,最终结果一致。防重复提交不能只靠前端按钮置灰,后端必须兜底。常见做法是页面提交用一次性 token,接口请求用业务唯一号配合 Redis SET NX EX,数据库用唯一索引兜底,订单支付类业务再加状态机判断。MQ 消费也要做幂等,因为消息可能重复投递。