Skip to content

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;
        }
    }
}

这段代码有三个毛病,项目一大就藏不住:

  1. 对象自己 new 依赖new JdbcAccountDao(...) 写在构造器里,将来想换 MyBatis 实现,得回来改这个类的源码。上层依赖下层,耦合点全在业务代码里。
  2. 对象图靠手工装配。真实项目一个 Service 可能依赖五六个组件,每个组件又依赖别的组件,初始化顺序错一个就 NPE。几十个类的装配代码全堆在 main 方法里,越堆越长。
  3. 横切逻辑散落在每个方法里。事务的 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 容器设计

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1