⚠️ 最重要强提醒:自动装配和自动配置不能搞混。
- 自动装配: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 |
启动过程可以简化理解为:
- 执行
main方法,调用SpringApplication.run()。 - 创建并刷新 Spring 容器。
- 扫描启动类所在包下的组件。
- 读取自动配置类,根据条件判断是否注册默认 Bean。
- 创建 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 配置优先级遵循“高优先级覆盖低优先级、不同配置互补生效”的原则。
常见优先级由高到低:
- 命令行参数。
- Java 系统属性。
- 操作系统环境变量。
- 配置中心配置,比如 Nacos 中的部分配置。
- JAR 包外
/config目录下的配置。 - JAR 包外根目录配置。
- JAR 包内
/config目录下的配置。 - JAR 包内根目录配置。
- 默认配置。
同级配置文件后缀优先级通常是:
properties > yml > yaml
配置文件位置优先级可以记成:
- 外部优先于内部:JAR 包外配置覆盖 JAR 包内配置。
- config 目录优先于根目录:
/config下优先级更高。 - profile 配置优先于通用配置:
application-prod.yml优先于application.yml。
高优先级不会让低优先级完全失效,只有相同 key 才覆盖;不同 key 会共同生效。
面试口述版
Spring Boot 配置加载可以按“外部优先、profile 优先、properties 优先”来记。命令行参数和配置中心通常优先级更高,JAR 包外配置优先于 JAR 包内配置,config 目录优先于根目录,同级里一般properties 高于yml 和yaml。相同配置项高优先级覆盖低优先级,不同配置项会互补生效。
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/classes 和 BOOT-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 方法上 |
方法是 private、final、static |
代理无法拦截或无法重写目标方法 | 避免在这些方法上直接加事务 |
| 同类内部方法调用 | this.xxx() 不经过 Spring 代理 |
拆到另一个 Service,或通过代理对象调用 |
| 当前类没有交给 Spring 管理 | 自己 new 的对象不是 Spring Bean |
通过 Spring 注入使用 Bean |
异常被 catch 后吞掉 |
方法正常返回,事务管理器会提交 | 继续抛出异常,或手动标记回滚 |
| 抛出检查异常 | 默认只回滚 RuntimeException 和 Error |
配置 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 调用最好直接移到事务边界之外。该方法中的非事务数据库写入可能立即提交,不能被外层回滚。
- MANDATORY:
deductStock()、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() {
// 容器关闭前释放资源
}
}
如果逻辑是“整个应用启动完成后执行一次”,更适合 CommandLineRunner 或 ApplicationRunner;如果只是某个 Bean 初始化后执行,优先考虑 @PostConstruct。
面试口述版
Bean 初始化后执行逻辑一般用 @PostConstruct,它会在依赖注入完成后执行;销毁前执行逻辑用 @PreDestroy。如果要在整个 Spring Boot 应用启动完成后执行一次任务,比如预热缓存或启动后检查,可以用 CommandLineRunner或 ApplicationRunner。InitializingBean和initMethod也能做初始化,BeanPostProcessor更偏底层扩展,可以拦截所有 Bean 的初始化过程。
接口幂等和防重复提交
书面笔记版
幂等性指同一个业务请求执行一次和执行多次,最终结果一致。防重复提交解决的是用户连续点击、网络超时重试、服务重试、MQ 重复投递、第三方回调重复通知等问题。
常见方案:
| 方案 | 适合场景 | 核心思路 |
|---|---|---|
| 前端防抖 | 用户连续点击 | 按钮置灰、提交中状态,只是体验优化 |
| Token 防重 | 表单提交、创建订单 | 服务端发一次性 token,提交时校验并删除 |
Redis SET NX EX |
高并发接口防重 | 用业务唯一键抢占处理资格并设置过期时间 |
| 数据库唯一索引 | 下单、支付、开户 | 用唯一约束兜底,防止重复写入 |
| 状态机 | 支付、订单流转 | 只有合法状态才能流转 |
| 分布式锁 | 同一资源串行处理 | 同一业务 key 同一时刻只允许一个线程处理 |
| MQ 消费幂等 | 消息可能重复投递 | 消费表、唯一索引、业务状态判断 |
Token 防重流程:
- 用户进入提交页面时,服务端生成 token。
- token 存入 Redis,并返回给前端。
- 前端提交表单时带上 token。
- 后端用 Lua 或原子删除校验 token。
- 校验成功才执行业务,校验失败说明重复提交。
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 消费也要做幂等,因为消息可能重复投递。