主题
IoC、DI、AOP:Spring 到底解决什么问题
本文是 Spring 系统学习系列的 L1 入门篇。前置:无。这一篇不讲配置怎么写,只回答一个问题——没有 Spring 的时候代码哪里疼,Spring 拿什么止疼。学完可以配合面试题食用:不依赖 Spring 手写 IoC 和 AOP
先看不用 Spring 的疼:一个转账服务
假设要写一个转账功能,最小依赖是账户 DAO 和事务管理器。不用任何框架,代码大概长这样:
java
public class TransferService {
private final AccountDao accountDao;
private final TransactionManager txManager;
public TransferService() {
// 依赖在这里写死:选了 JDBC 就换不掉
this.accountDao = new JdbcAccountDao(dataSource());
this.txManager = new JdbcTransactionManager(dataSource());
}
public void transfer(String from, String to, long amount) {
txManager.begin();
try {
accountDao.debit(from, amount);
accountDao.credit(to, amount);
txManager.commit();
} catch (Exception e) {
txManager.rollback();
throw e;
}
}
}这段代码有三个毛病,项目一大就藏不住:
- 对象自己 new 依赖。
new JdbcAccountDao(...)写在构造器里,将来想换 MyBatis 实现,得回来改这个类的源码。上层依赖下层,耦合点全在业务代码里。 - 对象图靠手工装配。真实项目一个 Service 可能依赖五六个组件,每个组件又依赖别的组件,初始化顺序错一个就 NPE。几十个类的装配代码全堆在 main 方法里,越堆越长。
- 横切逻辑散落在每个方法里。事务的 begin/commit/rollback、日志、权限校验,每个业务方法都要抄一遍。转账要事务,下单要事务,改密码还要事务——同一套模板代码复制了三十遍,哪天事务边界规则变了,就得改三十个地方。
Spring 的三个核心概念,就是对准这三个毛病来的:IoC 干掉"自己 new",DI 提供装配手段,AOP 收编横切逻辑。
IoC:反转的是"对象创建与装配权"
IoC(Inversion of Control)经常被说得玄乎,其实反转的东西很具体:对象不自己创建和装配依赖了,这个权力交给容器。
上面的 TransferService 里,accountDao 是谁、怎么造出来,是 TransferService 自己说了算(它自己 new 的)。用了 IoC 之后,这个决定权移交给容器:类只声明"我需要一个 AccountDao",容器负责找到实现类、造好实例、递进来。
要注意 IoC 是思想,不是某项具体技术。实现这个思想的手段不止一种——DI(依赖注入)是最主流的一种,还有依赖查找(Dependency Lookup,早期 EJB 风格,容器里 JNDI 查一下)。日常说 Spring 的 IoC,默认就是指 DI 这条路线。
mermaid
flowchart LR
subgraph 传统方式
A[TransferService] --"自己 new"--> B[JdbcAccountDao]
end
subgraph IoC 方式
C[TransferService] --"只声明需要"--> D[IoC 容器]
D --"查注册表/装配后注入"--> E[AccountDao 实现]
end控制权反转后有个直接后果:业务类对具体实现零依赖,JdbcAccountDao 换成 MybatisAccountDao,业务类一行不用改——这是后面单元测试能 mock、模块能替换的前提。
DI:IoC 的落地手段
DI(Dependency Injection)说的是同一件事的另一半:容器怎么把依赖递给对象。三种注入方式,Spring 都支持:
java
// 1. 构造器注入(推荐)
@Service
public class TransferService {
private final AccountDao accountDao;
public TransferService(AccountDao accountDao) {
this.accountDao = accountDao;
}
}
// 2. Setter 注入:适合可选依赖
// 3. 字段注入(@Autowired 直接打在字段上):代码最少,但离不开容器,单测不好写推荐构造器注入有三个实际理由:依赖可以声明成 final;缺依赖时容器启动阶段就报错,而不是等到运行时 NPE;不启动容器也能 new TransferService(mockDao) 做单元测试。字段注入省两行代码,代价是测试和复用都受限——团队规范里通常直接禁掉。
AOP:把散落的横切逻辑收拢到一处
横切关注点(cross-cutting concern)指那些不属于业务、却要在每个业务方法里出现的逻辑:事务、日志、权限、限流。OOP 按业务域划分类,天然放不下这类逻辑,只能到处复制。
AOP(Aspect-Oriented Programming)的解法是:把横切逻辑写成独立的切面(Aspect),再由框架在指定位置(切点)织入(Weave)。业务代码里看不到事务代码,事务却生效:
java
@Aspect
@Component
public class TxAspect {
// 切点:所有 @Transactional 注解的方法
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
TransactionStatus tx = txManager.getTransaction(new DefaultTransactionDefinition());
try {
Object result = pjp.proceed(); // 执行业务方法
txManager.commit(tx);
return result;
} catch (Throwable t) {
txManager.rollback(tx);
throw t;
}
}
}pjp.proceed() 前后包一层事务,效果等价于手写版里 begin/commit/rollback 的模板,但只写一次,所有命中切点的方法自动获得。织入靠动态代理实现,运行时给业务对象套一个代理对象,调用先进代理里的切面逻辑,再进目标方法。代理有 JDK 动态代理和 CGLIB 两条路线,细节留到 AOP 系列展开,这里记住一句话:你拿到的 Bean 不是裸对象,是代理——这也是自调用事务失效的根源,面试题系列里会反复遇到。
动手实操:10 行代码跑起一个容器
光看概念没用,跑一遍才有体感。下面是不依赖 Spring Boot、纯 Spring Framework 的最小可运行例子,同一个转账业务:
java
// 1. 业务类:只声明依赖,不自己 new
@Service
public class TransferService {
private final AccountDao accountDao;
public TransferService(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Transactional
public void transfer(String from, String to, long amount) {
accountDao.debit(from, amount);
accountDao.credit(to, amount);
}
}
@Repository
public class JdbcAccountDao implements AccountDao { /* ... */ }
// 2. 配置类:告诉容器扫哪里
@Configuration
@ComponentScan("com.example")
@EnableTransactionManagement
public class AppConfig { /* 事务管理器等 Bean 定义 */ }
// 3. 启动容器,拿 Bean 用
public class Main {
public static void main(String[] args) {
ApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
TransferService svc = ctx.getBean(TransferService.class);
svc.transfer("a", "b", 100);
}
}对比一下前后:手写版里构造器那两行 new 没了,换成构造器参数;事务模板那六行没了,换成一个 @Transactional;main 方法里的手工装配没了,换成 @ComponentScan 扫包注册。业务类从"什么都管"退化成"只写业务",这就是 Spring 止疼的方式。
常见误区与小结
几个入门常踩的坑:
- 把 IoC 和 DI 当两个东西并列背。IoC 是思想,DI 是实现手段,依赖查找是另一种实现,Spring 用的是 DI。
- 以为 @Autowired 注入的对象是裸对象。开启事务/AOP 后容器给的是代理对象,同类内部方法自调用走不到代理,事务会失效。
- 用字段注入图省事。单测没法脱离容器构造对象,依赖还可能被注入 null 也无感知,规范做法是构造器注入。
- 把 Spring 等同于 Spring Boot。Framework 是容器 + MVC + 事务这些基础能力;Boot 在其上做自动装配和内嵌容器,解决"配置多"的问题;Cloud 再往上做微服务组件集成。三者边界清晰,不是一个东西的三个别名。
小结:IoC 反转对象创建装配权,让业务类不再自己 new 依赖;DI 是容器把依赖递进来的具体手段;AOP 用代理织入收拢事务、日志这类横切逻辑。这三件事合起来,把"对象怎么来、依赖怎么接、横切逻辑放哪"从业务代码里剥离出去。这是整个 Spring 的地基,下一篇从 Spring Boot 快速上手切入,看它在这套地基上解决了什么新问题。
参考
参考:Spring 官方文档 Core Technologies 章节(docs.spring.io/spring-framework/reference/core.html);《Spring 技术内幕》第 2 章 IoC 容器设计