Skip to content

Spring 全栈硬核教程 · 01:Spring 核心(IoC / AOP / 事务 / 事件) ​

这一篇是整套教程的"物理定律"。后面所有篇章——Boot 的自动配置、Web 的请求处理、Security 的过滤器链、Cloud 的服务治理——底层全都跑在这一层的机制上。跳过这篇,后面所有内容对你来说都是黑魔法。


1. IoC:把"谁创建对象"这件事从你手里抢走 ​

1.1 为什么需要 IoC(不要背定义,看这个例子) ​

没有 IoC 的世界长这样:

java
public class OrderService {
    // 每个依赖都自己 new,依赖的依赖也要自己 new
    private UserService userService = new UserService(new UserDao(new DataSource(...)));
    private PaymentService paymentService = new PaymentService(new AlipayClient("appId", "key"));
}

问题不在"麻烦",在于三件事:① 换实现要改源码(想把 AlipayClient 换成 WechatPayClient?改代码、重新编译);② 没法单元测试(new 死的依赖没法 mock);③ 对象生命周期无人管理(谁负责关闭连接池?谁保证单例?)。

IoC(控制反转)的解法是:你只声明"我需要什么",由容器负责"怎么给你"。

java
@Service
public class OrderService {
    private final UserService userService;
    private final PaymentService paymentService;

    // 构造器注入:我只声明需求,不关心怎么来的
    public OrderService(UserService userService, PaymentService paymentService) {
        this.userService = userService;
        this.paymentService = paymentService;
    }
}

给 Go 开发者的类比:这本质上是把 Go 里"手写 NewOrderService(userSvc, paySvc) 并在 main 里层层组装"这件事,交给了一个自动化的容器去做。代价是"魔法感"增强、排查链路变长,收益是大型项目里的依赖组装不再是手工活。这个权衡是 Java 和 Go 生态哲学差异的核心体现之一。

1.2 Bean 的完整生命周期(⭐ 必须能徒手画出来) ​

① 实例化 Instantiation
   反射调用构造器,得到一个"属性全是 null 的半成品"
        ↓
② 属性填充 Populate
   依赖注入发生在这里,@Autowired 字段/setter 被赋值
   (循环依赖的问题就出在这一步,见 1.3)
        ↓
③ Aware 接口回调
   BeanNameAware / BeanFactoryAware / ApplicationContextAware
   作用:让 Bean 能感知到容器本身(框架代码常用,业务代码少用)
        ↓
④ BeanPostProcessor.postProcessBeforeInitialization
        ↓
⑤ 初始化 Initialization
   @PostConstruct → InitializingBean.afterPropertiesSet() → 自定义 init-method
   (三者按此顺序执行,用来做"依赖都就绪后的初始化工作",比如预热缓存)
        ↓
⑥ BeanPostProcessor.postProcessAfterInitialization
   ⭐⭐⭐ AOP 代理在这里生成!这是整个 Spring 最重要的一个时间点
   (AbstractAutoProxyCreator 就是一个 BeanPostProcessor)
        ↓
⑦ Bean 就绪,放入单例池,可被使用
        ↓
⑧ 销毁
   @PreDestroy → DisposableBean.destroy() → 自定义 destroy-method

为什么第⑥步是整个 Spring 最重要的时间点? 因为它解释了 Spring 里一大半的"诡异现象":

容器里真正存着的、注入给别人的,不是你写的那个类的实例,而是一个包着它的代理对象。@Transactional、@Async、@Cacheable、@Retryable 这些注解全都是靠这层代理外壳实现的。

由此直接推出那个经典问题的答案:

❓ 为什么 @Transactional 在同类方法内部调用时会失效?

java
@Service
public class OrderService {

    public void createOrder(Order order) {
        this.saveOrder(order);   // ❌ 走的是 this(原始对象),绕过了代理外壳,事务不生效!
    }

    @Transactional
    public void saveOrder(Order order) { ... }
}

调用链是 代理对象.createOrder() → 进入代理 → 转发给原始对象的 createOrder() → 内部 this.saveOrder() 直接在原始对象上执行,代理外壳完全没参与,@Transactional 自然形同虚设。

三种解法(按推荐度排序):

java
// 方案1(最推荐):拆到不同的类,让调用真正跨越代理边界
@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderTransactionalService txService;   // 注入的是代理对象 ✅
    public void createOrder(Order order) { txService.saveOrder(order); }
}

// 方案2:自我注入(Spring 4.3+ 支持,但可读性一般)
@Service
public class OrderService {
    @Lazy @Autowired private OrderService self;
    public void createOrder(Order order) { self.saveOrder(order); }
}

// 方案3:AopContext(需要开启 @EnableAspectJAutoProxy(exposeProxy = true),侵入性最强)
public void createOrder(Order order) {
    ((OrderService) AopContext.currentProxy()).saveOrder(order);
}

1.3 循环依赖与三级缓存(源码级,面试终极题) ​

问题:A 依赖 B,B 又依赖 A,容器创建 A 时要先创建 B,创建 B 时又要先创建 A……死循环怎么破?

Spring 的解法:三级缓存 + "提前暴露半成品"。

缓存字段名存什么
一级singletonObjects完全创建好的成品 Bean
二级earlySingletonObjects提前暴露的半成品(已实例化,未完成属性填充)
三级singletonFactoriesObjectFactory 工厂,按需生成早期引用

完整流程:

创建 A:
  1. 实例化 A(半成品,属性全 null)
  2. 【关键】立刻把 A 的 ObjectFactory 放进三级缓存
  3. 开始填充 A 的属性 → 发现需要 B
       ↓
     创建 B:
       4. 实例化 B
       5. B 的 ObjectFactory 放进三级缓存
       6. 填充 B 的属性 → 发现需要 A
            ↓
          7. 去缓存里找 A:一级没有 → 二级没有 → 三级命中!
          8. 调用 ObjectFactory.getObject() 拿到 A 的早期引用
             ⭐ 如果 A 需要 AOP 代理,代理就在这一刻提前生成
          9. 把结果放进二级缓存,并从三级缓存删除
             (因为"到底给原始对象还是代理对象"已经定了,不用再算第二次)
       10. B 拿到 A 的早期引用,B 创建完成,进入一级缓存
       ↓
  11. A 继续填充完属性,初始化,进入一级缓存

❓ 为什么非要三级?两级不够吗?

这是区分"背过答案"和"真懂"的分水岭。假设只有两级(直接在实例化后把半成品放二级缓存):

  • 如果 A 不需要 AOP:两级完全够用。
  • 如果 A 需要 AOP:B 拿到的是原始对象,而 A 最终初始化完成后放进一级缓存的是代理对象——于是 B 里持有的 A 和容器里的 A 是两个不同的对象,B 调用 A 的方法时事务/缓存注解全部失效,而且这个 bug 极其隐蔽。

三级缓存的 ObjectFactory 通过"延迟到真正需要时才决定给原始对象还是代理对象"解决了这个一致性问题。

❓ 构造器注入的循环依赖为什么解决不了?

因为三级缓存的前提是"先实例化,再填充属性"——半成品必须先存在才能被提前暴露。而构造器注入要求"依赖必须在实例化那一刻就准备好",根本没有"半成品"这个中间状态可以暴露,所以 Spring 直接抛 BeanCurrentlyInCreationException。

这其实是好事:构造器注入让循环依赖在启动期就暴露,而不是埋在运行时。循环依赖通常意味着职责划分有问题,倒逼你重构才是正解,而不是庆幸 Spring 帮你兜住了。

1.4 依赖注入方式对比:一张表定胜负 ​

方式能否 final能否脱离容器测试循环依赖表现官方态度
字段注入 @Autowired private X x;❌❌ 必须起容器或用反射悄悄允许,掩盖设计问题❌ 不推荐
Setter 注入❌✅允许用于可选依赖
构造器注入✅✅ 直接 new 即可启动期报错,暴露问题✅ 推荐

实战写法(Lombok 一行搞定):

java
@Service
@RequiredArgsConstructor   // 为所有 final 字段生成构造器
public class OrderService {
    private final UserService userService;
    private final PaymentService paymentService;
    // 不用手写构造器,同时享受构造器注入的全部好处
}

老司机盲区提醒:很多人知道"该用构造器注入",但不知道为什么。真正的理由不是"官方说的",而是——构造器参数列表是一个类依赖数量的可视化仪表盘。当你发现某个类的构造器有 12 个参数时,那个类已经烂了,构造器注入让这件事藏不住;字段注入则让你可以无限制地往类里塞 @Autowired,直到它变成一个三千行的上帝类还没人察觉。

1.5 Bean 作用域与常见陷阱 ​

Scope说明典型陷阱
singleton(默认)容器内唯一实例单例 Bean 里放可变成员变量 = 线程安全事故,这是新人最常见的生产事故来源
prototype每次获取都新建容器不管理销毁,@PreDestroy 不会被调用,资源要自己释放
request / sessionWeb 环境,每请求/每会话一个注入到单例 Bean 时需要代理(proxyMode),否则拿到的永远是第一次那个
application / websocketServletContext / WebSocket 会话级使用场景较少
java
// ❌ 生产事故写法
@Service
public class CounterService {
    private int count = 0;   // 单例 + 可变状态 = 并发下数据错乱
    public void increment() { count++; }
}

// ✅ 正确做法:无状态,或用线程安全容器
@Service
public class CounterService {
    private final AtomicInteger count = new AtomicInteger();
    public void increment() { count.incrementAndGet(); }
}

2. AOP:把横切关注点从业务代码里剥离 ​

2.1 核心概念速通 ​

术语人话解释
切面 Aspect一个"横切逻辑"的封装单元(比如"日志切面")
连接点 JoinPoint可以被织入的点(Spring AOP 里只支持方法执行)
切点 Pointcut用表达式圈定"哪些连接点要被织入"
通知 Advice织入的具体逻辑(前置/后置/环绕/异常/最终)
织入 Weaving把切面应用到目标对象的过程(Spring 在运行时通过动态代理完成)

2.2 实战:一个接口耗时统计切面 ​

java
@Aspect
@Component
@Slf4j
public class PerformanceAspect {

    // 切点:所有 controller 包下的 public 方法
    @Pointcut("execution(public * com.example..controller..*.*(..))")
    public void controllerMethods() {}

    @Around("controllerMethods()")
    public Object measure(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        String method = pjp.getSignature().toShortString();
        try {
            return pjp.proceed();
        } finally {
            long costMs = (System.nanoTime() - start) / 1_000_000;
            if (costMs > 500) {
                log.warn("慢接口告警 {} 耗时 {}ms", method, costMs);
            }
        }
    }
}

2.3 JDK 动态代理 vs CGLIB:硬核对比 ​

维度JDK 动态代理CGLIB
实现原理运行时生成实现了目标接口的代理类运行时生成目标类的子类(字节码增强)
前提条件目标类必须实现接口目标类不能是 final,方法不能是 final/private
性能创建快,调用略慢(反射)创建慢,调用快(Spring 4+ 后差距已很小)
Spring Boot 默认——✅ 默认强制用 CGLIB(proxyTargetClass=true)

为什么 Spring Boot 默认选 CGLIB? 因为 JDK 代理有个反直觉的坑:如果 OrderServiceImpl implements OrderService,JDK 代理生成的对象只能转型成 OrderService 接口,不能转成 OrderServiceImpl。当有人写 @Autowired private OrderServiceImpl impl; 时就会启动失败报"类型不匹配"。CGLIB 生成的是子类,没这个问题,所以 Boot 干脆全局默认 CGLIB 省心。

老司机盲区:CGLIB 代理的构造器会被调用两次(一次是父类即目标类的、一次是代理子类的),而且代理子类的字段是空的(它只转发方法调用,不复制字段值)。所以如果你在切面里通过反射直接读代理对象的字段,读到的是 null——必须通过 getter 方法读。这个坑每年都有人踩。

2.4 Spring AOP vs AspectJ ​

维度Spring AOPAspectJ
织入时机运行时(动态代理)编译期 / 类加载期(字节码织入)
能切什么只能切 Spring 管理的 Bean 的 public 方法任意类、任意方法,甚至字段访问、构造器
性能有代理调用开销几乎零运行时开销
复杂度开箱即用需要编译插件/Agent,配置复杂
何时用99% 的业务场景需要切非 Spring 对象、切 private 方法、或对性能极度敏感

3. 事务:最容易出事故的一块 ​

3.1 事务传播行为七种模式(⭐ 配决策表) ​

传播行为有事务时无事务时典型场景
REQUIRED(默认)加入当前事务新建事务绝大多数业务方法
REQUIRES_NEW挂起当前,新建独立事务新建事务记录操作日志(主业务失败也要留下日志)
SUPPORTS加入非事务执行只读查询
NOT_SUPPORTED挂起当前,非事务执行非事务执行耗时的报表查询,不想占着事务连接
MANDATORY加入抛异常强制要求调用方必须开事务的内部方法
NEVER抛异常非事务执行明确禁止在事务中调用的方法
NESTED创建保存点(嵌套事务)等同 REQUIRED部分回滚:子操作失败只回滚子操作

REQUIRES_NEW 的生产陷阱(很多人不知道):它会挂起外层事务、从连接池再拿一个连接。在高并发下,如果一个请求链路上有多层 REQUIRES_NEW,单个请求可能同时占用 3-4 个数据库连接,连接池很容易被打满并引发自我死锁(外层事务持有连接等待内层,内层在等待池里没有的连接)。

实战建议:REQUIRES_NEW 只在真正需要"独立提交"的场景用(日志、审计),且务必评估连接池容量。

NESTED vs REQUIRES_NEW 的关键区别:NESTED 是同一个物理事务里的保存点,外层回滚会连带内层一起回滚;REQUIRES_NEW 是两个完全独立的物理事务,互不影响。

3.2 @Transactional 失效的 7 种场景(打印贴墙上) ​

#场景根因
1同类内部方法调用走 this,绕过代理(见 1.2)
2方法非 publicSpring AOP 只代理 public 方法
3异常被 catch 吞掉没异常抛到代理层,代理不知道要回滚
4抛受检异常但没配 rollbackFor默认只对 RuntimeException/Error 回滚
5类没被 Spring 管理(手动 new)压根不在容器里,没有代理
6数据库引擎不支持事务MySQL 的 MyISAM 引擎
7多线程调用事务靠 ThreadLocal 绑定连接,跨线程后不是同一个连接

第 3 条的正确写法:

java
@Transactional(rollbackFor = Exception.class)
public void transfer(Long from, Long to, BigDecimal amount) {
    try {
        accountService.debit(from, amount);
        accountService.credit(to, amount);
    } catch (Exception e) {
        log.error("转账失败", e);
        // ❌ 只 catch 不抛 → 事务正常提交,钱丢了
        // ✅ 必须重新抛出,或手动标记回滚:
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        throw new BusinessException("转账失败", e);
    }
}

3.3 事务隔离级别与并发问题 ​

隔离级别脏读不可重复读幻读说明
READ_UNCOMMITTED✅会✅会✅会几乎不用
READ_COMMITTED❌✅会✅会Oracle/PostgreSQL 默认
REPEATABLE_READ❌❌⚠️ MySQL InnoDB 通过 MVCC+间隙锁基本解决MySQL 默认
SERIALIZABLE❌❌❌串行化,性能代价极大

实用建议:99% 的业务用数据库默认隔离级别即可(Isolation.DEFAULT)。真正需要强一致的场景(库存扣减、余额变更),靠乐观锁(版本号)或悲观锁(SELECT ... FOR UPDATE) 解决,而不是调高隔离级别——调高隔离级别的性能代价往往超出预期。


4. 常被忽略但很好用的核心能力 ​

4.1 事件机制:优雅的应用内解耦 ​

java
// 1. 定义事件
public record OrderCreatedEvent(Long orderId, Long userId, BigDecimal amount) {}

// 2. 发布
@Service
@RequiredArgsConstructor
public class OrderService {
    private final ApplicationEventPublisher publisher;

    @Transactional
    public void createOrder(CreateOrderCmd cmd) {
        Order order = orderRepository.save(...);
        publisher.publishEvent(new OrderCreatedEvent(order.getId(), cmd.userId(), cmd.amount()));
    }
}

// 3. 监听(可以有任意多个监听器,互不感知)
@Component
public class OrderEventListener {

    // ⭐ @TransactionalEventListener:等事务真正提交后才执行
    // 避免"发短信说下单成功了,结果事务回滚了"的经典事故
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    @Async
    public void onOrderCreated(OrderCreatedEvent event) {
        smsService.send(event.userId(), "下单成功");
    }
}

为什么这个特性被低估了:它让你在不引入消息队列的前提下实现应用内的发布订阅解耦。很多中小项目为了"解耦"直接上 Kafka/RabbitMQ,运维复杂度陡增,其实 ApplicationEvent 就够了。当然它是进程内的、不持久化的——跨服务或需要可靠投递时才该上 MQ。

4.2 Spring Framework 7 新增的弹性注解 ​

Spring Framework 7 把过去要引 spring-retry 才能做的事收进了核心:

java
@Service
public class ExternalApiService {

    // 自动重试:3次,指数退避,带抖动
    @Retryable(maxAttempts = 3, delay = 1000, multiplier = 2, jitter = 200)
    public String callThirdParty() { ... }

    // 并发限流:同时最多 10 个线程能进这个方法,防止打垮下游
    @ConcurrencyLimit(10)
    public void heavyOperation() { ... }
}

这两个注解对做"防御性编程"很实用——过去要么自己写切面,要么引第三方库,现在是框架原生能力。

4.3 SpEL:配置文件里的表达式语言 ​

java
@Value("#{systemProperties['user.region'] ?: 'cn-north'}")
private String region;

@Cacheable(value = "users", key = "#userId + ':' + #type")
public User getUser(Long userId, String type) { ... }

@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public void updateUser(Long userId, UserDTO dto) { ... }

SpEL 在 @Cacheable 的 key 生成、@PreAuthorize 的权限表达式、@ConditionalOnExpression 的条件判断里无处不在,值得专门花半小时熟悉语法。


5. 本篇自测:能答出来才算过关 ​

  1. 画出 Bean 的完整生命周期,并指出 AOP 代理在哪一步生成。
  2. 解释三级缓存,并说明"为什么两级不够"。
  3. 列举 @Transactional 的 7 种失效场景及各自根因。
  4. REQUIRES_NEW 和 NESTED 的本质区别是什么?前者在高并发下有什么陷阱?
  5. JDK 动态代理和 CGLIB 各自的前提条件是什么?Spring Boot 为什么默认选 CGLIB?
  6. 为什么单例 Bean 里不能随便放可变成员变量?
  7. @TransactionalEventListener 解决了 @EventListener 的什么问题?

下一篇(02 · Spring Boot):自动配置的魔法到底怎么实现的?.imports 文件替代 spring.factories 背后藏着什么战略意图?如何手写一个能给全公司用的 Starter?以及——配置优先级那十几层,到底谁覆盖谁?

基于 Vite 强力驱动 | 纯静态轻量托管