Appearance
IoC、DI 与 AOP
如果说 Spring 有哪几个概念最核心,那通常就是:
IoCDIAOP
很多 Spring 能力最后都会回到这三条主线上。
这篇文章重点讲:
IoC到底是什么DI和IoC是什么关系Bean、容器、生命周期分别是什么意思AOP到底在解决什么问题- Spring 为什么能用代理机制把事务、日志这些能力织进去
1. IoC 到底是什么
IoC 是 Inversion of Control,即控制反转。
它可以看成 把对象的创建和管理控制权,从业务代码手里交给 Spring 容器。
最直观的变化是:
- 以前自己
new - 现在交给容器创建和维护
所以 IoC 解决的不是“少写几行代码”,而是:对象创建、依赖管理和生命周期控制不要散落在业务逻辑里。
2. DI 和 IoC 到底是什么关系
DI 是 Dependency Injection,即依赖注入。
它可以理解成 由 Spring 容器把对象所依赖的其他对象自动注入进去。
两者的关系可以直接记成:
IoC是思想DI是最常见的落地方式
前者强调“控制权交给容器”,后者强调“依赖由容器注入”。
3. 为什么 IoC / DI 很重要
如果没有这套机制,代码经常会是这样:
- 一个类自己创建很多依赖
- 实现类被写死
- 测试时很难替换依赖
- 模块之间耦合很重
而 IoC / DI 带来的变化是:
- 对象创建交给容器
- 依赖关系由容器组织
- 替换实现更容易
- 模块更容易解耦
- 测试更方便
这就是为什么 Spring 在企业应用里很容易成为底座。
4. Spring 容器到底是什么
Spring 容器 可以理解成 负责创建、管理、装配和维护 Bean 的运行时环境。
它解决的是 应用里那么多对象,谁来创建、谁来维护、谁依赖谁,这些事情不要让业务代码各自处理。
更常见的两个名字是:
BeanFactoryApplicationContext
4.1 BeanFactory
它更偏基础容器。
4.2 ApplicationContext
它是在 BeanFactory 基础上扩展出来的更完整容器。
它通常还提供:
- 事件机制
- 资源加载
- 国际化
- 更完整的应用上下文能力
所以实际开发里,更常接触的是 ApplicationContext。
5. Bean 是什么
在 Spring 语境里,Bean 可以简单理解成:被 Spring 容器管理的对象。
要特别注意:不是所有 Java 对象都是 Bean。
只有那些:
- 被扫描到
- 被配置注册
- 或显式声明进入容器
的对象,才会变成 Bean。
常见方式包括:
@Component@Service@Repository@Controller@Bean
5.1 常见 Bean 注册注解怎么理解
| 注解 | 作用 | 常见场景 |
|---|---|---|
@Component | 最通用的组件标记 | 工具类、通用组件 |
@Service | 语义化表示业务服务层 | Service 层 |
@Repository | 语义化表示数据访问层 | DAO、Repository 层 |
@Controller | 语义化表示 MVC 控制层 | 页面控制器 |
@RestController | 组合注解,表示接口控制层 | REST API |
@Configuration | 表示配置类 | Bean 装配配置 |
@Bean | 把方法返回值注册为 Bean | 第三方对象、显式装配 |
@Service、@Repository、@Controller 本质上都还是组件扫描这条主线,只是语义更明确。
5.2 @Component 和 @Bean 有什么区别
区别可以直接记成:
@Component是把“类”交给 Spring 扫描注册@Bean是把“方法返回对象”交给 Spring 注册
例如:
java
@Service
public class OrderService {
}java
@Configuration
public class AppConfig {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
}6. 依赖注入有哪些常见方式
常见方式通常有:
- 构造器注入
- Setter 注入
- 字段注入
6.1 构造器注入
它更适合 把依赖显式表达出来,并且让对象在创建时就处于完整可用状态。
工程上通常更推荐这种方式。
例如:
java
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
public OrderService(OrderRepository orderRepository, PaymentService paymentService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
}
}6.2 Setter 注入
它更适合 某些可选依赖或需要后续调整的依赖。
例如:
java
@Component
public class ExportService {
private MessageSender messageSender;
@Autowired
public void setMessageSender(MessageSender messageSender) {
this.messageSender = messageSender;
}
}6.3 字段注入
它写起来最省事,但在依赖显式性、测试友好性方面通常没那么理想。
例如:
java
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
}所以更稳妥的工程习惯通常是:优先构造器注入。
6.4 这 3 种注入方式到底有什么不同
这 3 种写法的核心区别,不只是“代码写在哪”,而是:依赖是在对象创建时注入,还是创建后再补进去;依赖关系是否显式;以及对象能不能从一开始就保持完整可用。
看一张总表:
| 方式 | 注入时机 | 依赖是否显式 | 对象是否容易保持完整状态 | 测试友好性 |
|---|---|---|---|---|
| 构造器注入 | 创建对象时注入 | 最显式 | 是 | 最好 |
| Setter 注入 | 创建后再注入 | 中等 | 不一定 | 较好 |
| 字段注入 | 创建后由容器直接注入字段 | 最弱 | 不一定 | 最差 |
把差别先拉开:
- 构造器注入强调“必要依赖必须在创建时给齐”
- Setter 注入强调“对象可以建出来,依赖之后再补”
- 字段注入强调“写法最短,但依赖关系最容易被隐藏”
6.5 它们各自的优缺点是什么
6.5.1 构造器注入
优点通常包括:
- 依赖最显式,代码一眼就能看出类依赖了什么
- 对象一创建就处于完整可用状态
- 适合把依赖声明为
final - 最利于单元测试和重构
缺点通常包括:
- 当依赖很多时,构造器参数会变长
- 更容易暴露“类职责过多”这种设计问题
这里第二点从工程角度看不一定是坏事,因为它能更早提醒你这个类可能已经太重了。
6.5.2 Setter 注入
优点通常包括:
- 写法直观
- 对可选依赖更灵活
- 某些场景下方便后续替换依赖
缺点通常包括:
- 依赖不如构造器注入那么显式
- 对象在注入完成前,可能处于不完整状态
- 如果遗漏注入,问题更容易延迟到运行时暴露
所以它更适合 那些并不是对象创建所必需的依赖。
6.5.3 字段注入
优点通常包括:
- 写法最短
- 初学时看起来最省事
缺点通常包括:
- 依赖最不显式
- 不利于单元测试
- 很难做不可变设计
- 更依赖 Spring 容器本身
- 容易隐藏一个类真实依赖过多的问题
所以它虽然方便,但工程上通常不是最推荐的选择。
6.6 工程上更常见的选择建议
如果把这三种方式压缩成最实用的结论,通常就是:
- 必要依赖优先使用构造器注入
- 可选依赖可以考虑 Setter 注入
- 字段注入尽量少用
可以用一句话记住:构造器注入最稳,Setter 注入更灵活,字段注入最省事但也最容易埋下维护问题。
6.7 常用依赖注入注解
| 注解 | 作用 | 要点 |
|---|---|---|
@Autowired | 按类型注入依赖 | Spring 最常见的注入注解 |
@Qualifier | 指定注入哪个 Bean | 多实现类场景很常见 |
@Primary | 指定默认优先 Bean | 解决同类型 Bean 冲突 |
@Resource | 按名称优先注入 | 来自 Jakarta 规范 |
下面用同一组接口和实现,分别演示这几个注解最常见的使用方式。
看公共前置代码:
java
public interface MessageSender {
void send(String message);
}
@Component("smsSender")
public class SmsSender implements MessageSender {
@Override
public void send(String message) {
}
}
@Component("emailSender")
public class EmailSender implements MessageSender {
@Override
public void send(String message) {
}
}6.7.1 @Autowired
@Autowired 最常见的用法,是按类型注入依赖。
例如只有一个实现时,可以直接注入:
java
@Service
public class AuditService {
private final MessageSender messageSender;
public AuditService(@Autowired MessageSender messageSender) {
this.messageSender = messageSender;
}
}这里真正的要点是:Spring 会先按类型找 Bean。
如果同一类型只有一个实现,这种写法最直接。
6.7.2 @Qualifier
当同一个接口有多个实现时,只按类型注入就不够了,这时通常要配合 @Qualifier 明确指定 Bean 名称。
java
@Service
public class NotifyService {
private final MessageSender messageSender;
public NotifyService(@Qualifier("smsSender") MessageSender messageSender) {
this.messageSender = messageSender;
}
}这个例子表达的是:同类型 Bean 有多个时,用 @Qualifier 指定到底注入哪一个。
6.7.3 @Primary
如果你希望某个实现成为“默认首选实现”,可以在实现类上标记 @Primary。
java
@Primary
@Component("smsSender")
public class SmsSender implements MessageSender {
@Override
public void send(String message) {
}
}
@Component("emailSender")
public class EmailSender implements MessageSender {
@Override
public void send(String message) {
}
}
@Service
public class DefaultNotifyService {
private final MessageSender messageSender;
public DefaultNotifyService(MessageSender messageSender) {
this.messageSender = messageSender;
}
}这里没有写 @Qualifier,但仍然能注入成功,是因为:Spring 会优先选择被 @Primary 标记的那个实现。
6.7.4 @Resource
@Resource 更常见的理解是:按名称优先注入,名称找不到时再看类型。
java
@Service
public class ResourceNotifyService {
@Resource(name = "emailSender")
private MessageSender messageSender;
}这个例子最适合说明:当你希望明确按 Bean 名称注入时,@Resource 会很直接。
6.8 Spring 是如何实现依赖注入的
前面讲的是:依赖注入有哪些写法、常见注解怎么用。
但如果再往里追一层,真正的问题其实是:Spring 容器内部到底是怎么把依赖“注进去”的。
可以把整个过程压缩成一条主线:
- 容器先收集并注册
BeanDefinition - 容器刷新时,开始创建 Bean
- 创建 Bean 时先实例化对象
- 再解析依赖并完成属性填充
- 依赖注入完成后,再进入初始化和后置处理
也就是说:依赖注入不是一个孤立动作,而是 Bean 创建流程中的一个关键阶段。
一张总流程图,会更容易把后面的细节串起来:
mermaid
flowchart TD
A[启动 Spring 应用] --> B[扫描配置类与组件]
B --> C[注册 BeanDefinition]
C --> D[刷新 ApplicationContext]
D --> E[创建单例 Bean]
E --> F{选择实例化方式}
F -->|构造器注入| G[先解析构造参数]
F -->|字段或 Setter 注入| H[先实例化对象]
G --> I[创建 Bean 实例]
H --> I
I --> J[属性填充与依赖注入]
J --> K[执行 Aware 与前置处理]
K --> L[执行初始化方法]
L --> M[执行 BeanPostProcessor 后置处理]
M --> N[Bean 进入可用状态]
N --> O[需要时可能被 AOP 代理包装]6.8.1 第一步:把 Bean 定义收进容器
Spring 并不是看到 @Autowired 才临时决定“去哪里找对象”。更早之前,容器就在启动阶段把 Bean 的定义信息收集好了。
这些定义通常来自:
@Component、@Service、@Repository等组件扫描@Configuration+@Bean- XML 或其他配置方式
这些信息最终会变成 BeanDefinition。它可以理解成“Bean 的说明书”,里面描述了:
- 这个 Bean 的类型是什么
- 它怎么创建
- 它的作用域是什么
- 它依赖哪些对象
所以容器先管理的是对象的定义信息,后面才按需创建实例。
6.8.2 第二步:容器刷新时,开始创建 Bean
以 ApplicationContext 为主线看,Spring 启动时会触发容器刷新。
在这个过程中,容器会把前面注册好的 Bean 定义交给 BeanFactory 管理,并逐步创建需要提前初始化的单例 Bean。
可以粗略理解成:容器开始按照 BeanDefinition,把“说明书”变成真正可用的对象。
6.8.3 第三步:先实例化,再注入依赖
这一步非常关键,因为不同注入方式,发生时机并不一样。
可以看最核心的区别:
- 构造器注入:创建对象之前,就要把构造参数解析出来
- Setter 注入 / 字段注入:对象先创建出来,再进行属性填充
例如下面这个类:
java
@Service
public class OrderService {
private final OrderRepository orderRepository;
@Autowired
private PaymentService paymentService;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}Spring 大致会按这个思路处理:
- 先发现构造器需要
OrderRepository - 先去容器里找
OrderRepository,找到后才能创建OrderService OrderService实例创建出来以后- 再处理字段上的
@Autowired,把PaymentService注进去
这就是为什么:构造器注入更早发生,字段 / Setter 注入更偏向实例化之后的属性填充阶段。
6.8.4 @Autowired 和 @Resource 到底是谁处理的
很多人会以为,@Autowired 是 Bean 自己“长出来的能力”。其实不是,它们是容器在 Bean 创建过程中,借助后置处理器完成的。
最常见的两个处理器是 AutowiredAnnotationBeanPostProcessor 和 CommonAnnotationBeanPostProcessor。
AutowiredAnnotationBeanPostProcessor负责处理@Autowired、@Value等注解CommonAnnotationBeanPostProcessor负责处理@Resource、@PostConstruct、@PreDestroy等 Jakarta / JSR 规范相关注解
不是注解自己会注入依赖,而是 Spring 在创建 Bean 的过程中识别这些注解,再按规则完成注入。
6.8.5 Spring 按什么规则找到要注入的 Bean
依赖注入真正难的地方,不是“塞进去”,而是:同类型 Bean 有多个时,到底该选谁。
Spring 常见的解析顺序大致就是:
- 看类型能不能唯一匹配
- 如果同类型有多个,再看名称、
@Qualifier、@Primary - 如果还是无法确定,就抛出注入歧义异常
例如:
java
public interface MessageSender {
void send(String message);
}
@Primary
@Component("smsSender")
public class SmsSender implements MessageSender {
@Override
public void send(String message) {
}
}
@Component("emailSender")
public class EmailSender implements MessageSender {
@Override
public void send(String message) {
}
}
@Service
public class NotifyService {
private final MessageSender messageSender;
public NotifyService(@Qualifier("emailSender") MessageSender messageSender) {
this.messageSender = messageSender;
}
}这个例子里:
- 如果没有
@Qualifier,Spring 会优先考虑@Primary - 写了
@Qualifier("emailSender")之后,就明确指定注入emailSender
而 @Resource(name = "emailSender") 更强调优先按名字去找 Bean。
6.8.6 为什么字段循环依赖有时能解,构造器循环依赖通常不行
这件事其实也和依赖注入的时机强相关。
字段注入 / Setter 注入的 Bean,往往可以把对象实例“提前暴露”出来,然后再补属性,所以 Spring 在一部分单例场景里还能继续推进。
但构造器注入不一样,因为构造器参数在对象创建前就必须准备好。
如果 A 构造器依赖 B,B 构造器又依赖 A,那么两个对象都还没创建出来,就已经卡住了。
- Spring 不是“天然能解决循环依赖”
- 它只是能在一部分单例 Bean 的属性注入场景里,通过提前暴露对象引用继续完成装配
- 构造器循环依赖通常没有这个回旋空间
6.8.7 把整个依赖注入流程串起来
把 Spring 依赖注入的实现压缩成一条完整链路,可以记成:
- 扫描和注册 Bean,生成
BeanDefinition - 容器刷新,开始创建 Bean
- 先实例化对象
- 再由容器解析
@Autowired、@Resource等注解 - 按类型、名称、
@Qualifier、@Primary等规则查找依赖 - 完成依赖注入后,进入初始化、后置处理和可能的代理增强
Spring 依赖注入真正解决的,不只是“自动帮你 new 对象”,而是把对象定义、创建时机、依赖解析、生命周期扩展和后续增强放进同一套容器机制里统一处理。
7. Bean 生命周期到底在说什么
Bean 生命周期不是简单的“new 一个对象”。
更完整一点的理解通常包括:
- 实例化
- 依赖注入
- 初始化前处理
- 初始化
- 初始化后处理
- Bean 可用
- 容器关闭时销毁
这里最关键的理解是:Spring 能在 Bean 创建前后插入很多扩展点,所以很多增强能力才有落点。
这也是为什么 BeanPostProcessor 这类机制很重要。
7.1 生命周期相关常见注解和接口
| 注解 / 接口 | 作用 |
|---|---|
@PostConstruct | 依赖注入完成后执行初始化逻辑 |
@PreDestroy | Bean 销毁前执行清理逻辑 |
InitializingBean | 初始化回调接口 |
DisposableBean | 销毁回调接口 |
看更常见的注解方式:
java
@Component
public class CacheManager {
@PostConstruct
public void init() {
System.out.println("load cache");
}
@PreDestroy
public void destroy() {
System.out.println("clear cache");
}
}如果你想用接口回调方式,也可以这样写:
java
@Component
public class CacheManager implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() {
System.out.println("Bean 初始化完成,开始加载缓存");
}
@Override
public void destroy() {
System.out.println("Bean 即将销毁,开始释放资源");
}
}要注意的是:
InitializingBean对应的方法是afterPropertiesSet()DisposableBean对应的方法是destroy()
它们最常见的用途通常包括:
- 初始化缓存
- 校验关键配置
- 预热连接或资源
- 容器关闭时释放资源
7.2 接口方式和注解方式怎么选
把这两种方式分开:
InitializingBean / DisposableBean:接口方式@PostConstruct / @PreDestroy:注解方式
接口方式的特点是:
- 语义很直接
- 生命周期时机明确
- 但类会直接依赖 Spring 接口
注解方式的特点是:
- 写法更轻
- 侵入性更低
- 在大多数业务代码里更常见
所以更常见的工程习惯通常是:优先使用 @PostConstruct / @PreDestroy;只有在明确需要接口回调风格时,再考虑 InitializingBean / DisposableBean。
8. AOP 到底是什么
AOP 是 Aspect-Oriented Programming,即面向切面编程。
它可以看成 把分散在多个业务方法里的通用逻辑抽出来,不直接写死在业务主流程里。
这些通用逻辑通常包括:
- 日志
- 事务
- 权限校验
- 性能统计
- 审计
如果没有 AOP,业务方法就容易变成:
- 先写主流程
- 再插日志
- 再插事务
- 再插鉴权
最后业务代码会越来越乱。
8.1 一个最直观的 AOP 示例
如果不用 AOP,日志可能会分散在很多业务方法里:
java
public void createOrder() {
long start = System.currentTimeMillis();
try {
// 业务逻辑
} finally {
System.out.println("cost=" + (System.currentTimeMillis() - start));
}
}用了 AOP 之后,可以把通用逻辑抽出去:
java
@Aspect
@Component
public class LogAspect {
@Around("execution(* com.example.service..*(..))")
public Object logCost(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
try {
return joinPoint.proceed();
} finally {
System.out.println(joinPoint.getSignature() + " cost=" + (System.currentTimeMillis() - start));
}
}
}9. AOP 具体解决什么问题
它解决的核心问题是:横切逻辑不该和主业务逻辑强耦合。
这里的“横切逻辑”,可以理解成 很多模块都要用,但它本身又不是主业务目标的那类逻辑。
所以 AOP 的价值是:
- 让业务方法更聚焦主流程
- 让通用逻辑集中治理
- 让日志、事务、安全等能力更容易复用
10. Spring AOP 为什么能工作
Spring AOP 的核心不是“偷偷改源码”,而是:在运行时为目标对象创建代理对象,由代理对象在合适时机织入增强逻辑。
- 业务对象负责主流程
- 代理对象包在外面
- 请求先进入代理
- 代理决定是否在前后插入日志、事务、安全等增强逻辑
所以很多 Spring 能力背后都离不开:容器 + 代理。
10.1 Spring AOP 常见注解
| 注解 | 作用 |
|---|---|
@Aspect | 标记这是一个切面类 |
@Pointcut | 抽取切点表达式 |
@Before | 方法执行前增强 |
@After | 方法执行后增强 |
@AfterReturning | 方法正常返回后增强 |
@AfterThrowing | 方法抛异常后增强 |
@Around | 环绕增强,控制能力最强 |
例如把切点单独抽出来:
java
@Aspect
@Component
public class AuditAspect {
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
public void transactionalMethod() {
}
@Before("transactionalMethod()")
public void beforeTransactionalMethod() {
System.out.println("before transactional method");
}
}10.2 Spring 里如何实现自定义注解
很多人第一次说“Spring 自定义注解”时,脑子里容易把两件事混在一起:
- 定义一个 Java 注解
- 让 Spring 识别这个注解,并在运行时做点事情
只有把这两步都做了,自定义注解才算真正“在 Spring 里跑起来”。
也就是说,注解本身只是元数据,Spring 还需要一套处理机制去识别它。
在工程里,最常见的实现方式通常是:自定义注解 + AOP 切面
例如,我们希望给某些业务方法加一个 @AuditLog,自动记录审计日志。
10.2.1 第一步:先定义注解
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuditLog {
String action();
}这个定义里最关键的几件事是:
@Target(ElementType.METHOD):表示这个注解放在方法上@Retention(RetentionPolicy.RUNTIME):表示运行时仍然保留,Spring 才有机会读取到它String action():给注解增加业务属性,表示当前操作的名称
如果没有 RUNTIME 级别保留策略,运行时拿不到注解信息,AOP 也就无从处理。
10.2.2 第二步:写切面去识别这个注解
java
@Aspect
@Component
public class AuditLogAspect {
@Around("@annotation(auditLog)")
public Object recordAuditLog(ProceedingJoinPoint joinPoint, AuditLog auditLog) throws Throwable {
long start = System.currentTimeMillis();
try {
return joinPoint.proceed();
} finally {
String methodName = joinPoint.getSignature().toShortString();
System.out.println("action=" + auditLog.action()
+ ", method=" + methodName
+ ", cost=" + (System.currentTimeMillis() - start));
}
}
}这段代码里最关键的是:
@Around("@annotation(auditLog)"):表示只拦截那些标了@AuditLog的方法AuditLog auditLog:Spring 会把当前方法上的注解对象直接传进来joinPoint.proceed():真正执行目标方法
这条链路直接看成:
- 业务方法被 Spring 代理包装
- 调用先进入切面
- 切面发现方法上有
@AuditLog - 先执行增强逻辑,再放行目标方法
- 最后再做日志记录或收尾动作
10.2.3 第三步:在业务方法上使用
java
@Service
public class OrderService {
@AuditLog(action = "createOrder")
public void createOrder(Long orderId) {
System.out.println("create order: " + orderId);
}
}这样一来,调用 createOrder() 时,除了业务本身执行,还会自动触发切面里的审计逻辑。
这就是 Spring 里最常见的“自定义注解落地”方式:注解负责表达业务语义,AOP 负责在运行时接住这个语义并织入对应逻辑。
10.2.4 为什么只定义注解还不够
很多人会写出这样的代码:
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {
}然后期待加在方法上就自动生效,其实不会。注解本身只是在类或方法上贴了一个标记,真正让它“有功能”的,是 Spring 后续如何处理这个标记。
这个处理动作常见可能来自:
AOP切面BeanPostProcessorImportSelectorSpring MVC参数解析或拦截器扩展
但在大多数业务项目里,最常见、最容易理解的仍然是:自定义注解 + AOP
10.2.5 再看一个组合注解示例
Spring 里除了“注解 + AOP”这种玩法,还有一种很常见的自定义方式,就是组合注解。
例如:
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestController
@RequestMapping("/admin")
public @interface AdminRestController {
}然后就可以这样用:
java
@AdminRestController
public class AdminUserController {
@GetMapping("/users")
public String listUsers() {
return "ok";
}
}这个例子想表达的是:你可以把多个 Spring 注解打包成一个更符合业务语义的组合注解。
它的价值是:
- 减少重复配置
- 让注解表达更贴近业务语义
- 统一约束一类组件的使用方式
10.2.6 一句话总结 Spring 自定义注解
一句话总结就是:自定义注解本身只负责表达语义,真正让它生效的,是 Spring 在运行时通过 AOP、后置处理器或其他扩展机制去识别并处理它。
11. JDK 动态代理和 CGLIB 有什么区别
这是 Spring AOP 里最常见的问题之一。
11.1 JDK 动态代理
它更偏:基于接口生成代理。
适合目标对象有接口的场景。
11.2 CGLIB
它更偏:通过继承目标类并生成子类来做代理。
它不要求目标类一定有接口。
一句话压缩:
- JDK 动态代理偏接口
- CGLIB 偏子类继承
12. 为什么事务也和 AOP 有关
很多人会把事务只看成数据库问题,但在 Spring 里,声明式事务之所以能工作,常常也依赖 AOP 代理。
例如:
java
@Transactional
public void createOrder() {
}这背后的关键不是注解本身“有魔法”,而是:Spring 通过代理对象在方法调用前后插入事务开启、提交、回滚这些逻辑。
所以事务、AOP、代理机制其实是连在一起的。
13. @Transactional 为什么有时候不生效
这是典型高频问题。
常见原因包括:
- 方法没有经过 Spring 代理对象调用
- 同类内部自调用
- 方法可见性不合适
- 异常被吞掉,没有触发预期回滚
这里最容易忽略的一点是:同类内部直接调用方法,往往不会经过代理对象,所以增强逻辑可能根本没机会执行。
14. 工程上最常见的几个误区
14.1 误区一:IoC 就是自动 new 对象
这太窄了。
IoC 的重点是控制权转移和统一管理,不只是少写 new。
14.2 误区二:DI 和 IoC 是同义词
它们密切相关,但不完全一样。
更稳妥的说法仍然是:
IoC 是思想,DI 是落地手段。
14.3 误区三:AOP 就是“高级注解”
注解只是表达形式。
真正让 AOP 工作的是代理和增强机制。
14.4 误区四:只要加了 @Transactional 就一定安全
事务是否生效,仍然要看:
- 调用路径
- 代理是否参与
- 异常是否正确传播
15. 一句话总结
IoC / DI 解决的是对象创建和依赖组织问题,AOP 解决的是横切逻辑抽离问题,而 Spring 真正强大的地方,正是在容器和代理机制之上把这两条主线统一了起来。