IOC / AOP + Bean 生命周期
IOC控制反转
生活化类比:
- 传统开发(没有 IOC):你做饭,米、菜、锅全部自己去超市买(自己 new 对象)
- IOC 思想:有个食堂(Spring 容器),所有食材厨具都由食堂统一管理
- DI 依赖注入:你要做饭,食堂直接把锅和菜送到你手上(容器注入依赖)
核心考点:解耦。业务代码不需要关心对象怎么创建,只负责业务逻辑,便于单元测试、替换实现类。
IOC 全称控制反转(Inversion of Control)
原本是程序员自己 new 对象、管理对象生命周期;反转后,对象的创建、销毁、依赖管理交给 Spring 容器负责,控制权从业务代码反转到容器。
DI 全称依赖注入(Dependency Injection)
是 IOC 的具体实现手段。容器主动把需要的依赖对象注入到目标 Bean 中,不需要自己手动 new 依赖。
一句话总结关系: IOC 是思想,DI 是实现这个思想的方式。
- IOC:思想(控制权移交容器)
- DI:技术手段(注入依赖)
- Bean 容器:承载所有 Bean,落地 IOC 思想的载体
请口述:IOC 解决了开发中的什么问题?带来了什么好处?
IOC 核心解决了传统开发中,手动 new 对象造成的代码高耦合问题。 好处:
- 解耦:业务代码不用自己创建依赖,依赖交给 Spring 容器统一管理;
- 便于灵活替换实现类,面向接口编程;
- 方便单元测试,可以轻松注入 Mock 对象;
- 容器统一管控 Bean 的创建、销毁、生命周期。
AOP(面向切面编程)
大白话类比
正常业务主线:短信下发核心逻辑(主线业务)
切面:日志记录、接口耗时统计、权限校验、事务管理、异常通知
AOP:不改动主线业务代码,横向植入通用公共逻辑
标准定义
AOP 面向切面编程,在不修改原有业务代码的前提下,对方法进行增强,抽取通用横切逻辑。
常见使用场景:
- 事务管理(@Transactional 底层就是 AOP 动态代理)
- 接口日志、请求耗时埋点
- 权限校验、接口限流
- 全局异常处理
两种动态代理
- JDK 动态代理:要求目标类实现接口,代理对象实现和目标相同的接口,只能拦截接口里的方法;
- CGLIB 动态代理:通过继承目标类生成子类作为代理,不需要实现接口;无法代理 final 类、final 方法。
SpringBoot2.x 之后,默认使用 CGLIB
@Transactional 事务失效很大一个原因:AOP 动态代理失效(内部方法调用,this 调用,不走代理对象)
Bean 的完整生命周期
大白话类比:
Bean 就像盖房子
定义图纸(BeanDefinition)→ 建房实例化 → 装修填充依赖(DI)→ 各种改造回调(初始化)→ 入住使用 → 到期拆除(销毁)
完整流程
- 实例化:调用构造方法,new 出 Bean 原始对象(此时对象存在,属性还没赋值)
- 属性填充 DI:完成依赖注入,给对象的成员变量赋值
- 初始化
- BeanPostProcessor 前置处理
- 执行 InitializingBean 接口 afterPropertiesSet
- 执行 @PostConstruct 标注方法
- 指定 init-method 自定义初始化方法
- BeanPostProcessor 后置处理(AOP 代理就是在这里生成代理对象!重点!)
- 就绪阶段:Bean 完成,放入单例池,可供业务使用
- 销毁:容器关闭时,执行 @PreDestroy、DisposableBean、destroy-method
通俗大白话对照
- 实例化 = 毛坯房建好
- 属性填充 DI = 水电、家具搬进房子
- 初始化各种回调 = 全屋改造、定制装修
- BeanPostProcessor 后置 = 加装监控切面(AOP 代理)
- 就绪阶段 = 可以入住使用
- 销毁 = 房子拆除
对照表格
| 执行顺序 | 标准阶段 | 标准含义 | 大白话房子比喻 |
|---|---|---|---|
| 0 | BeanDefinition 解析 | 扫描注解 / XML,加载 Bean 的元数据定义 | 拿到房子的设计图纸 |
| 1 | 实例化 | 调用构造方法,new 出原始对象(属性还没赋值) | 毛坯房主体建好 |
| 2 | 属性填充 DI | 依赖注入,给成员变量赋值 | 水电、家具搬进房子 |
| 3 | BeanPostProcessor 前置 | 初始化前的统一增强处理 | 装修前的全屋检查 |
| 4 | @PostConstruct | 执行注解标注的初始化方法 | 业主自己做的首次布置 |
| 5 | InitializingBean 回调 | 执行 afterPropertiesSet 接口方法 | 物业统一验收配置 |
| 6 | init-method | 执行 XML / 注解指定的自定义初始化方法 | 业主定制的特殊装修 |
| 7 | BeanPostProcessor 后置 | 初始化后的增强处理,AOP 代理对象在此生成 | 加装监控 / 门禁(切面代理) |
| 8 | 就绪阶段 | 单例 Bean 放入单例池,对外提供使用 | 可以正式入住使用 |
| 9 | @PreDestroy | 容器关闭时执行销毁前回调 | 搬家前清点物品 |
| 10 | DisposableBean 回调 | 执行 destroy 接口方法 | 物业统一断水断电 |
| 11 | destroy-method | 执行自定义销毁方法 | 业主自行拆除私人物品 |
- AOP 代理生成位置:第 7 步 BeanPostProcessor 后置处理,不是实例化阶段。
- 单例 Bean :默认容器启动时就走完 1~8 全部流程;加
@Lazy才延迟到第一次使用。 - 多例 Bean(prototype) :每次 getBean 才实例化,Spring 不管理销毁(9~11 不执行),由 JVM GC 回收。
- 执行顺序口诀:实例 → 填属性 → 前置 → 三个初始化 → 后置(生成代理)→ 使用 → 三个销毁。
关键
- 单例 Bean:容器启动时就完成实例化、初始化;
- 多例 Bean prototype :每次 getBean 才实例化,Spring 容器不管理销毁;
- AOP 代理生成在初始化后置处理器这一步;
- 顺序记忆口诀:实例 → 填属性 → 前置 → 初始化回调 → 后置(生成代理)→ 使用 → 销毁
SpringBoot 自动装配原理
大白话理解
传统 SSM:需要自己大量写 XML、手动配置 Bean(数据源、Mybatis、线程池等),自己 import 各种配置类,配置繁琐、容易冲突。
自动装配:SpringBoot 根据项目引入的依赖、配置文件,自动帮我们导入、创建、装配所需 Bean,不用手写大量配置。
类比: SSM = 组装电脑,主板、显卡、驱动全部自己挑选安装
SpringBoot 自动装配 = 品牌整机,你选好配件(引入 starter 依赖),出厂预装驱动(自动装配 Bean)
核心启动注解:@SpringBootApplication
拆开这个复合注解,由 3 个核心注解组成:
1.@SpringBootConfiguration
本质就是 @Configuration,代表当前类是配置类,可以定义 @Bean
2.@EnableAutoConfiguration(重中之重,自动装配开关)
开启自动装配功能
3.@ComponentScan
默认扫描当前启动类所在包及其子包下所有 @Component、@Service、@Repository、@Controller,注册为 Bean
坑点:如果业务类放在启动类包外层,默认扫描不到,不会注入 Bean!
@EnableAutoConfiguration 底层流程
@EnableAutoConfiguration 里面导入了 AutoConfigurationImportSelector(选择导入哪些自动配置类) 完整流程:
- 项目启动,执行 selectImports 方法
- 读取
META-INF/spring.factories文件(springboot 内置在各个 starter 包里面) - 文件里存放大量自动配置类全限定名:MybatisAutoConfiguration、DataSourceAutoConfiguration 等
- 拿到所有候选配置类后,进行条件注解过滤 (核心:@ConditionalOnXXX)
- @ConditionalOnClass:类存在才装配
- @ConditionalOnMissingBean:容器不存在该 Bean,才自动创建
- 满足条件的配置类,导入容器,里面定义好各种 Bean,完成自动装配
- 我们 yml 配置文件(spring.datasource 等),用来修改自动装配 Bean 的默认属性
Starter 是什么?
starter = 依赖 jar 包 + 配套自动配置类
例如 mybatis-spring-boot-starter
- 引入 mybatis、jdbc 相关依赖
- 内置 MybatisAutoConfiguration 自动配置类,满足条件自动装配 SqlSessionFactory、MapperScannerConfigurer
核心扩展
1.自动装配不是全部加载所有配置类!依靠条件注解按需装配
2.自定义覆盖自动装配 Bean:自己定义同名 Bean,@ConditionalOnMissingBean 会让 SpringBoot 不再自动创建,用户自定义 Bean 优先级更高
举例:SpringBoot 自动装配线程池,我们自己写 @Bean ThreadPoolExecutor,就会覆盖默认线程池(短信项目批量下发场景常用)
3.SpringBoot2.7 之后,不再使用 spring.factories,改用 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
精简版
SpringBoot 自动装配由 @EnableAutoConfiguration 开启,底层通过 AutoConfigurationImportSelector 读取配置文件获取候选自动配置类,配合 @ConditionalOnXXX 条件注解,根据引入的 starter 依赖按需自动创建 Bean;开发者可自定义 Bean 覆盖默认自动装配,减少手写配置。
| 组件 | 作用 |
|---|---|
| @SpringBootApplication | 启动复合注解,整合三大功能 |
| @EnableAutoConfiguration | 开启自动装配核心开关 |
| AutoConfigurationImportSelector | 读取配置文件,获取候选自动配置类 |
| spring.factories / AutoConfiguration.imports | 存放所有自动配置类全类名 |
| @ConditionalOnXXX 条件注解 | 按需过滤,满足条件才装配 Bean |
Spring 事务
核心目标:掌握 7 种事务传播机制 + 事务隔离级别 + 事务失效高频场景
基础铺垫:@Transactional 底层原理
大白话: @Transactional 本质依靠 AOP 动态代理。
- 调用目标方法时,先走代理对象
- 方法执行前:开启事务
- 正常执行完毕:提交事务
- 抛出指定异常:事务回滚
⚠️ 关键点:必须通过代理对象调用方法,事务才会生效,这是大部分事务失效的根源。
两大基础概念
事务隔离级别(和 MySQL InnoDB 隔离级别一一对应)
@Transactional(isolation = Isolation.XXX)
- DEFAULT:默认,使用数据库本身隔离级别(InnoDB 默认 RR 可重复读)
- READ_UNCOMMITTED:读未提交
- READ_COMMITTED:读已提交
- REPEATABLE_READ:可重复读
- SERIALIZABLE:串行化
7 种事务传播行为
传播行为解决:一个带事务的方法,调用另一个带事务的方法,事务怎么合并 / 创建 @Transactional(propagation = Propagation.XXX)
| 传播属性 | 含义(大白话) | 常用场景 |
|---|---|---|
| REQUIRED(默认) | 有事务就加入当前事务;没有就新建事务 | 绝大多数业务,短信创建、保存日志 |
| REQUIRES_NEW | 不管有没有事务,新建独立事务,各自互不影响 | 记录操作日志,主事务回滚,日志依然可以提交保存 |
| SUPPORTS | 有事务就加入,没有就非事务运行 | 查询类方法,可有可无事务 |
| NOT_SUPPORTED | 永远非事务执行,挂起已有事务 | 异步通知、第三方调用,不参与事务 |
| MANDATORY | 必须在已有事务中运行,没有直接抛异常 | 内部核心业务,禁止无事务调用 |
| NEVER | 禁止存在事务,如果已有事务直接抛异常 | 纯查询,严禁事务包裹 |
| NESTED | 嵌套事务,依赖外层事务;外层回滚,内层全部回滚;内层回滚不影响外层 | 批量操作,支持局部回滚(保存点) |
高频区分:REQUIRES_NEW vs NESTED
REQUIRES_NEW:两个完全独立事务,互不影响提交回滚
NESTED:内层是外层事务的保存点,外层回滚内层一起回滚
Spring 事务失效五大类场景
1.方法不是 public 修饰
@Transactional 只作用于 public 方法,private、protected 不会生成代理,事务失效。
2.同类中方法内部调用(this 调用)
例如 A 方法(无注解)this 调用本类带 @Transactional 的 B 方法,不走代理对象,事务失效。
✅ 解决方案:注入自身代理对象,或者拆分到不同类
3.异常类型不对,不会触发回滚
默认只捕获 RuntimeException / Error;如果抛出受检 Exception,不回滚。
解决:@Transactional (rollbackFor = Exception.class)
4.异常被 try-catch 手动捕获吃掉
方法内部 try catch 捕获异常,没有对外抛出,代理感知不到异常,不会回滚。
5.多线程场景
事务是绑定当前线程,新开启子线程执行数据库操作,无法共用主线程事务,事务失效。
补充额外坑:数据库引擎不支持事务(MyISAM),不管怎么加注解都无效。
业务举例(短信批量创建业务)
批量创建多条短信记录,希望整体要么全部入库,要么全部回滚。 方法上加 @Transactional (rollbackFor = Exception.class),默认 REQUIRED。
如果中间某条短信参数校验失败抛异常,整体事务回滚,不会出现一半数据入库脏数据。
缓存专项全套(三大缓存问题:穿透、击穿、雪崩)
重点:每个问题都记住 现象 → 产生原因 → 工业级解决方案,结合短信业务理解
前置基础:缓存使用流程
先查 Redis 缓存 ✅ 命中直接返回;缓存没命中,查询 MySQL,查到数据写入 Redis,再返回结果
缓存穿透
现象
大量请求查询数据库不存在的数据,缓存永远不会写入数据,请求全部绕过缓存,直接打到数据库,压垮 DB。
短信业务例子:前端传入不存在的手机号,查询短信下发记录,Redis 查不到,每次都查 MySQL
原因
查询的 key 对应数据在数据库本身就不存在,缓存没有兜底数据,每次都穿透到数据库
解决方案(工业常用)
- 缓存空值 / 空对象:数据库查不到,向 Redis 存入空值,设置较短过期时间(5 分钟),拦截后续相同请求
- 布隆过滤器(高并发推荐):前置存放所有合法 key,非法 key 直接拦截,不访问缓存和数据库
- 接口参数校验:手机号格式校验,非法参数直接拒绝
缓存击穿
现象
热点 key 过期瞬间,大量并发请求同时进来,缓存失效,所有请求瞬间打到数据库。
短信业务例子:营销活动热门手机号的下发统计缓存,刚好过期,瞬时大量请求访问 DB
原因
某个访问极高的热点 key 到期删除,并发请求同时到来,缓存缺失
解决方案
- 互斥锁(分布式锁,Redisson):缓存失效时,只放行一个线程去查 DB 并回写缓存,其他线程等待重试
- 热点 key 永不过期(逻辑过期):不设置物理 expire,后台异步线程定时更新缓存数据,避免集中过期
注意:互斥锁会有等待,存在性能损耗;逻辑过期适合高并发热点数据
缓存雪崩
现象
大量 key 在同一时间段集体过期,大量请求同时绕过缓存,数据库压力瞬间飙升,引发服务雪崩。
区分:击穿是单个热点 key 过期;雪崩是大批量 key 集中过期
原因
- 大量缓存 key 设置相同过期时间,同一时刻全部失效
- Redis 集群宕机,整体缓存服务不可用
解决方案
- 过期时间加随机值:基础过期时间 + 随机偏移量,打散过期时间,避免集体失效
- Redis 高可用部署:主从、哨兵、Redis Cluster 集群,防止单点宕机
- 服务层限流熔断(Sentinel):缓存失效后,限制访问数据库的流量,保护 DB
- 多级缓存:本地 Caffeine 缓存 + Redis 远程缓存,双层兜底
| 问题 | 核心特征 | 根本原因 | 主流解决方案 |
|---|---|---|---|
| 缓存穿透 | 查询不存在的数据,永远无缓存 | DB 无对应数据 | 缓存空值、布隆过滤器、参数校验 |
| 缓存击穿 | 单个热点 key 过期,并发打 DB | 高访问热点 key 到期 | 分布式互斥锁、逻辑永不过期 |
| 缓存雪崩 | 大批量 key 同时失效,整体压力突增 | 大量 key 同一时间过期 / Redis 宕机 | 过期加随机值、Redis 集群、限流熔断、多级缓存 |
缓存更新策略
三种常用策略
- 先更新数据库,再删除缓存(工业主流方案,短信项目推荐)
- 先删缓存,再更新数据库(存在短期脏数据风险)
- 更新数据库同时更新缓存(不推荐,并发下数据不一致)
配套问题:数据一致性如何保障?
最终方案:定时任务校对缓存与数据库数据;监听 binlog(Canal)异步更新缓存,保证最终一致性。