Java 基础体系 · 第 24/100 篇。示例统一以 Java 25 LTS 为语言和 JVM 基线;框架示例使用与其兼容的现代稳定版本。
Spring Framework 核心:IoC、Bean 生命周期、AOP、事件和事务代理
Spring 的核心不是“用注解替代 new”,而是把对象创建、依赖关系、扩展回调、横切逻辑和事务边界组织成一个可组合的运行时系统。理解这个系统,需要同时回答五个问题:
- 谁负责创建对象,以及对象依赖如何解析?
- 一个 Bean 从定义到销毁经历哪些状态?
- AOP 如何在不修改业务方法的情况下插入逻辑?
- 事件如何在 Bean 之间传播,事务事件又如何保证时机?
@Transactional为什么有时生效、有时完全不生效?
下文中的 Spring API 属于 Spring Framework 的稳定核心模型;具体 Spring Boot 版本应使用与 Java 25 兼容的版本,并以对应版本的官方文档为准。
一、IoC:把对象创建权交给容器
1.1 IoC 和 DI 的关系
IoC(Inversion of Control,控制反转)描述的是一种控制权变化:
- 传统代码控制对象的创建、装配和生命周期;
- Spring 容器控制这些过程;
- 业务对象只声明自己需要什么,不负责寻找或创建依赖。
DI(Dependency Injection,依赖注入)是实现 IoC 的主要方式。一个对象通过构造器、字段或 Setter 接收依赖,而不是主动调用容器或 new 依赖对象。
例如,下面的代码把依赖创建写死在业务类中:
public final class OrderService {
private final PaymentClient paymentClient = new PaymentClient();
}
这种写法的问题不是 new 本身,而是 OrderService 同时承担了两种职责:
- 处理订单业务;
- 决定
PaymentClient的具体实现和生命周期。
使用构造器注入后:
@Service
public class OrderService {
private final PaymentClient paymentClient;
public OrderService(PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
}
OrderService 仍然依赖 PaymentClient,但不再负责创建它。容器根据 Bean 定义解析依赖并调用构造器。
构造器注入还使依赖关系成为对象成立的必要条件:
public OrderService(PaymentClient paymentClient) {
this.paymentClient = Objects.requireNonNull(paymentClient);
}
如果容器无法提供 PaymentClient,应用通常会在启动阶段失败,而不是等到一次请求执行时才出现空指针异常。
1.2 容器中的核心对象:BeanDefinition、BeanFactory 和 ApplicationContext
Spring 容器至少可以从三个层面理解:
BeanDefinition:对象的“说明书”
BeanDefinition 描述如何创建一个 Bean,例如:
- Bean 的类型;
- 构造器或工厂方法;
- 作用域;
- 属性依赖;
- 初始化和销毁方法;
- 是否延迟初始化。
注解并不是 Bean 本身。@Component、@Service、@Configuration 等注解通常先被扫描器读取,再转换成容器内部的 BeanDefinition。
BeanFactory:Bean 的创建和获取引擎
BeanFactory 负责:
Object bean = beanFactory.getBean("orderService");
它需要完成:
- 查找 BeanDefinition;
- 解析依赖;
- 创建实例;
- 执行后处理器和初始化回调;
- 返回最终对象。
ApplicationContext:带基础设施的容器
ApplicationContext 建立在 BeanFactory 之上,额外提供:
- 应用事件发布;
- 国际化消息;
- 资源加载;
- 自动注册部分基础设施 Bean;
- 更完整的生命周期管理。
Spring Boot 启动时创建的通常是 ApplicationContext,而不是裸 BeanFactory。
1.3 依赖解析可以看成有向图
把每个 Bean 看成一个节点,把“依赖”看成一条有向边:
OrderController ──依赖──> OrderService ──依赖──> OrderRepository
记依赖图为:
其中:
- 是 Bean 集合;
- 是依赖边;
- 如果 ,表示创建 之前需要得到 。
对于构造器注入,容器必须找到一个满足依赖顺序的创建过程。若图是有向无环图,可以通过拓扑排序得到创建顺序:
OrderRepository
↓
OrderService
↓
OrderController
如果存在构造器循环:
A ──> B
↑ │
└─────┘
则不存在满足“构造 A 前先得到 B,同时构造 B 前先得到 A”的普通顺序。容器会报告循环依赖,而不是凭空构造出两个完整对象。
Setter 或字段注入有时可以通过“先实例化、后注入属性”解决单例循环,但这依赖特定的单例创建机制,并不等价于所有循环都能解决。构造器循环通常无法通过这种方式绕过。
1.4 多个候选 Bean:类型、名称和限定符
当容器中只有一个 PaymentClient 时,下面的依赖通常可以唯一解析:
public OrderService(PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
如果存在两个实现:
@Component
class AlipayClient implements PaymentClient {}
@Component
class WechatPayClient implements PaymentClient {}
则按类型注入会产生歧义。可以使用 @Primary:
@Component
@Primary
class AlipayClient implements PaymentClient {}
或者使用限定符:
@Service
public class OrderService {
private final PaymentClient paymentClient;
public OrderService(@Qualifier("wechatPayClient")
PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
}
这里的关键不是“Spring 偏好哪一个实现”,而是依赖解析必须得到唯一结果。无法唯一决定时,启动失败比随机选择更安全。
1.5 @Bean 和组件扫描的差别
组件扫描适合注册自己控制的类:
@Service
public class OrderService {
}
@Bean 适合注册第三方类或需要显式构造过程的对象:
@Configuration
public class ClientConfiguration {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
}
@Configuration 类中的 @Bean 方法由 Spring 处理。对于标准配置类,容器通常会确保同一个配置方法产生的 Bean 仍受容器作用域管理,而不是每次普通 Java 调用都创建一个新对象。
需要区分两种调用:
@Configuration
class Config {
@Bean
public A a() {
return new A();
}
@Bean
public B b() {
return new B(a()); // 通常得到容器中的 A
}
}
和:
class PlainConfig {
@Bean
public A a() {
return new A();
}
@Bean
public B b() {
return new B(a()); // 在普通 Java 配置类中可能每次直接创建 A
}
}
前者依赖 Spring 对配置类的增强,后者不能假设有同样的拦截行为。使用方法参数注入通常更直接:
@Bean
public B b(A a) {
return new B(a);
}
二、Bean 生命周期:从定义到销毁
2.1 Bean 生命周期的主要阶段
以普通单例 Bean 为例,生命周期可以抽象为:
flowchart TD
A[读取 BeanDefinition] --> B[实例化对象]
B --> C[属性依赖注入]
C --> D[Aware 回调]
D --> E[初始化前 BeanPostProcessor]
E --> F[调用初始化方法]
F --> G[初始化后 BeanPostProcessor]
G --> H[放入单例缓存并对外提供]
H --> I[容器关闭]
I --> J[销毁前回调]
J --> K[调用销毁方法]
每一步的意义不同:
- 读取 BeanDefinition:容器知道“怎么创建”。
- 实例化:调用构造器或工厂方法。
- 属性注入:设置字段、Setter 或其他属性依赖。
- Aware 回调:如果 Bean 实现了特定接口,容器向它提供上下文信息。
- 初始化前处理:
BeanPostProcessor可以修改或替换对象。 - 初始化方法:执行
@PostConstruct、InitializingBean或自定义初始化方法。 - 初始化后处理:AOP 代理通常在这个阶段生成。
- 对外提供:后续依赖拿到的可能已经不是原始对象,而是代理对象。
- 销毁:容器关闭时执行销毁回调。
“Bean 已经构造完成”不等于“Bean 已经初始化完成”,更不等于“Bean 已经可以安全对外提供服务”。
2.2 初始化回调的顺序和用途
一个 Bean 可以使用多种初始化机制:
@Component
public class CacheWarmup implements InitializingBean {
@PostConstruct
void loadConfiguration() {
System.out.println("load configuration");
}
@Override
public void afterPropertiesSet() {
System.out.println("warm up cache");
}
@Bean(initMethod = "start")
public SomeComponent component() {
return new SomeComponent();
}
}
常见的执行关系是:
@PostConstruct所对应的处理器执行;InitializingBean.afterPropertiesSet()执行;- 自定义
initMethod执行; - 后续初始化阶段的 BeanPostProcessor 执行。
具体细节由容器和处理器实现决定,但可以稳定地把这些回调理解为“依赖注入完成后、Bean 正式可用前”的初始化阶段。
初始化方法适合:
- 校验配置;
- 建立连接;
- 加载小规模本地缓存;
- 注册需要随着 Bean 创建而建立的资源。
不适合在其中执行不可控的长时间任务。因为非懒加载单例初始化发生在应用上下文刷新过程中,初始化阻塞可能直接延长启动时间或导致启动失败。
2.3 BeanPostProcessor:容器扩展机制
BeanPostProcessor 是 Spring 大量功能的基础:
public interface BeanPostProcessor {
default Object postProcessBeforeInitialization(
Object bean, String beanName) {
return bean;
}
default Object postProcessAfterInitialization(
Object bean, String beanName) {
return bean;
}
}
处理器可以:
- 修改 Bean;
- 包装 Bean;
- 返回代理;
- 检查注解;
- 注入额外行为。
AOP、@Autowired、@PostConstruct 等功能都依赖类似的后处理器机制。
关键规则是:后处理器返回的对象可能与原对象不是同一个对象。
Object raw = new OrderService();
Object exposed = postProcessor.postProcessAfterInitialization(raw, "orderService");
如果 exposed 是代理,那么其他 Bean 注入和从容器获取的通常是 exposed,而不是 raw。因此下面的日志可能显示代理类:
Object bean = context.getBean(OrderService.class);
System.out.println(bean.getClass());
这不是 Bean 被重复创建,而是 Bean 被包装了。
2.4 作用域决定生命周期
最常见的 singleton 作用域表示:在一个 ApplicationContext 内,容器通常只管理一个 Bean 实例。
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class RequestBuilder {
}
prototype 表示每次向容器请求时创建新实例,但容器通常只负责创建和初始化,不负责完整管理其销毁生命周期。
其他作用域的生命周期可能与 Web 请求、会话或线程相关。作用域越短,越不能把它无条件注入到生命周期更长的单例中。例如,把请求作用域对象直接保存进单例,可能造成上下文错误或状态污染。此时通常需要代理作用域对象或使用 ObjectProvider<T> 延迟获取。
2.5 循环依赖和早期引用
单例 Bean 创建过程中,Spring 内部会维护不同阶段的缓存,目的是在某些属性注入循环中暴露“早期引用”。早期引用可能已经是代理,也可能仍是原始对象,具体取决于创建阶段和相关后处理器。
因此循环依赖存在几个风险:
- 构造器循环通常直接失败;
- Setter 或字段循环有时能启动,但对象可能在完全初始化前被另一方使用;
- AOP 代理参与循环时,可能出现“注入的是代理,缓存的是原对象”或版本相关的循环依赖错误;
- Spring Boot 默认配置可能对循环依赖采取更严格的失败策略。
更可靠的修复方式是消除环,而不是依赖容器的早期引用:
A ──> B ──> A
可以通过提取第三个职责:
A ──> C <── B
如果两个类确实需要协作,也可以把运行时查询延迟到方法执行时,但这只是改变创建时机,不能自动消除设计上的强耦合。
三、AOP:代理如何插入横切逻辑
3.1 横切关注点和连接点
日志、权限、事务、缓存、监控等逻辑往往横跨多个业务类。若直接写进每个方法,会产生重复代码和不一致行为。
AOP(Aspect-Oriented Programming,面向切面编程)把这类横切关注点抽取为切面。
几个核心术语:
- 连接点(Join Point):可以插入额外行为的位置。
- 切点(Pointcut):用于筛选连接点的表达式。
- 通知(Advice):在匹配位置执行的额外逻辑。
- 切面(Aspect):切点和通知的组合。
- 代理(Proxy):Spring AOP 中实际承载拦截行为的对象。
Spring AOP 基于代理的方法执行连接点。它不像完整 AspectJ 那样天然拦截任意字段访问、构造器执行或静态方法调用。
3.2 JDK 动态代理和 CGLIB 子类代理
假设有接口:
public interface PaymentService {
void pay(long orderId);
}
Spring 可以创建 JDK 动态代理:
调用方
│
▼
JDK Proxy ──> PaymentServiceImpl.pay()
JDK 代理实现接口,调用方法时进入 InvocationHandler。
如果没有合适接口,或配置要求使用类代理,Spring 可以使用基于字节码生成的子类代理,常见实现是 CGLIB 机制:
PaymentServiceProxy extends PaymentServiceImpl
两种方式的边界不同:
| 目标 | JDK 代理 | 子类代理 |
|---|---|---|
| 代理接口方法 | 可以 | 可以 |
| 代理具体类方法 | 只能通过接口暴露 | 通常可以 |
final 类 |
不可以 | 不可以继承 |
final 方法 |
接口代理只能代理接口调用;具体实现的 final 方法不具备可覆写拦截点 | 不能通过覆写拦截 |
private 方法 |
不在接口调用路径上 | 不能被子类覆写 |
static 方法 |
不属于实例方法调用链 | 不能按实例覆写 |
不要通过 bean.getClass() == MyService.class 判断是否是原始 Bean。代理存在时,这个判断可能失败;更稳妥的方式是使用容器按类型获取,或使用 Spring 提供的代理判断工具。
3.3 一个切面的完整执行过程
下面的切面记录方法耗时:
@Aspect
@Component
public class TimingAspect {
@Around("execution(* com.example.order..*(..))")
public Object measure(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed();
} finally {
long elapsed = System.nanoTime() - start;
System.out.println(pjp.getSignature() + " took " + elapsed + " ns");
}
}
}
一次外部调用的过程是:
controller
│
▼
OrderService 代理
│
├─ 执行 Around 通知之前的代码
│
├─ pjp.proceed()
│ │
│ ▼
│ 目标对象的业务方法
│
└─ 执行 Around 通知之后的代码
如果通知不调用 proceed():
@Around("execution(* com.example.order..*(..))")
public Object deny(ProceedingJoinPoint pjp) {
throw new AccessDeniedException("denied");
}
目标方法就不会执行。通知不仅可以观察调用,还可以:
- 修改参数;
- 替换返回值;
- 转换异常;
- 提前返回;
- 终止调用。
这也是为什么切面错误可能改变业务语义,而不仅仅是增加日志。
3.4 自调用为什么绕过代理
下面的代码经常导致事务或日志失效:
@Service
public class OrderService {
public void outer() {
inner();
}
@Transactional
public void inner() {
// 期望开启事务
}
}
外部调用实际是:
调用方
↓
OrderService 代理.outer()
↓
目标对象内部 this.inner()
outer() 内部的 inner() 等价于 this.inner()。它没有再次经过代理,因此事务拦截器没有机会执行。
这不是 Spring 的事务特殊规则,而是代理模型的直接结果。
可行的结构调整是把事务边界放到另一个 Bean:
@Service
public class TransactionalOrderService {
@Transactional
public void inner() {
// 事务边界
}
}
@Service
public class OrderFacade {
private final TransactionalOrderService transactionalOrderService;
public OrderFacade(TransactionalOrderService transactionalOrderService) {
this.transactionalOrderService = transactionalOrderService;
}
public void outer() {
transactionalOrderService.inner(); // 跨 Bean,经过代理
}
}
AopContext.currentProxy() 可以在特定配置下取得当前代理,但它增加了对代理上下文的耦合,并且要求调用确实处于代理链中,通常不如拆分职责清晰。
3.5 通知顺序不是代码书写顺序
多个切面同时匹配时,执行顺序由优先级决定,而不是 Java 文件中注解的排列顺序。可以使用 @Order:
@Aspect
@Component
@Order(10)
class SecurityAspect {
}
@Aspect
@Component
@Order(20)
class LoggingAspect {
}
数值越小通常优先级越高。若存在事务、权限、重试和日志,顺序会改变语义。例如:
权限检查
↓
事务开启
↓
重试包装
↓
业务方法
和:
事务开启
↓
重试包装
↓
业务方法
并不等价。重试究竟应该重新开启事务,还是在同一事务中重复执行,是必须明确的设计问题。
四、应用事件:发布、监听和失败路径
4.1 事件模型
Spring 事件的基本参与者是:
ApplicationEventPublisher:发布事件;- 事件对象:携带事实数据;
@EventListener或ApplicationListener:接收事件。
现代代码通常可以直接使用普通对象作为事件:
public record OrderCreated(long orderId) {
}
发布事件:
@Service
public class OrderService {
private final ApplicationEventPublisher publisher;
public OrderService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void create(long orderId) {
// 保存订单
publisher.publishEvent(new OrderCreated(orderId));
}
}
监听事件:
@Component
public class OrderCreatedListener {
@EventListener
public void onOrderCreated(OrderCreated event) {
System.out.println("send notification for " + event.orderId());
}
}
默认情况下,事件监听器通常在发布线程中同步执行。于是调用链是:
create()
├─ 保存订单
├─ publishEvent()
│ └─ listener.onOrderCreated()
└─ create() 返回
如果监听器抛出异常,异常可能沿发布调用链返回发布方。事件不是天然的消息队列,也不自动提供持久化、重试、跨进程传输或最终一致性。
4.2 同步监听和异步监听
启用异步执行后:
@Configuration
@EnableAsync
class AsyncConfiguration {
}
@Component
class NotificationListener {
@Async
@EventListener
public void onOrderCreated(OrderCreated event) {
// 在线程池中执行
}
}
调用链变成:
publishEvent()
├─ 把任务提交到 Executor
└─ 立即返回
└─ 线程池执行 listener
这会改变三个重要事实:
- 发布方不再等待监听器完成;
- 监听器异常不会按同步调用方式直接返回给发布方;
- 事务上下文、ThreadLocal、请求上下文不会自动传播到新线程。
因此异步监听器必须独立处理:
- 线程池容量;
- 拒绝策略;
- 异常记录;
- 重试;
- 幂等;
- 关闭时仍在执行的任务。
4.3 事务事件的时机
如果事件表示“订单已经创建”,但发布发生在事务提交之前,普通监听器可能在事务回滚前就执行:
@Transactional
public void create() {
repository.save(order);
publisher.publishEvent(new OrderCreated(order.id()));
// 后续异常导致事务回滚
}
同步监听器可能已经发出了通知,但数据库记录最终不存在。这是典型的不一致。
可以使用事务事件监听器:
@Component
public class OrderNotificationListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onCommitted(OrderCreated event) {
// 只有事务成功提交后执行
}
}
常见阶段包括:
BEFORE_COMMIT:提交前;AFTER_COMMIT:提交成功后;AFTER_ROLLBACK:回滚后;AFTER_COMPLETION:提交或回滚完成后。
AFTER_COMMIT 解决的是“不要在回滚事务上发送通知”,但它不等于可靠消息投递。进程可能在数据库提交成功后、监听器执行前崩溃,导致事件丢失。
如果需要跨进程可靠传播,通常需要事务消息、Outbox 表或消息中间件等额外机制。Outbox 的基本过程是:
同一数据库事务
├─ 写业务表
└─ 写 outbox_event 表
后台投递器
├─ 读取未发送事件
├─ 投递消息
└─ 标记已发送
投递通常仍可能重复,因此消费者必须幂等。可靠性来自“可恢复的持久化状态”,而不是 @TransactionalEventListener 这一注解本身。
五、事务代理:@Transactional 究竟做了什么
5.1 事务的抽象
Spring 不直接把事务逻辑写死在业务类中,而是通过 PlatformTransactionManager 抽象数据库或其他资源的事务操作:
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition);
void commit(TransactionStatus status);
void rollback(TransactionStatus status);
}
具体实现可能是:
- JDBC 事务管理器;
- JPA 事务管理器;
- JTA 事务管理器;
- 其他资源的事务管理器。
@Transactional 只是声明事务属性,例如:
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED,
timeout = 5,
readOnly = false
)
public void createOrder() {
}
真正执行事务的是事务拦截器和事务管理器。
5.2 一次事务代理调用的步骤
假设外部调用了被 @Transactional 标记的方法:
sequenceDiagram
participant C as 调用方
participant P as 事务代理
participant TM as TransactionManager
participant T as 目标对象
participant DB as 数据库
C->>P: createOrder()
P->>TM: getTransaction()
TM->>DB: BEGIN 或加入现有事务
P->>T: 执行业务方法
T->>DB: INSERT / UPDATE
T-->>P: 正常返回或抛出异常
alt 正常返回
P->>TM: commit()
TM->>DB: COMMIT
P-->>C: 返回结果
else 满足回滚规则的异常
P->>TM: rollback()
TM->>DB: ROLLBACK
P-->>C: 抛出异常
end
对于 Propagation.REQUIRED:
- 当前线程没有事务:创建新事务;
- 当前线程已有事务:加入现有事务;
- 内层方法返回时通常不会独立提交外层事务。
事务资源通常绑定在线程上下文中。以 JDBC 为例,Spring 会把当前事务对应的连接绑定到当前线程,使同一事务内的数据访问复用正确连接。这个线程绑定模型解释了为什么事务不会自动跨越新线程。
5.3 默认回滚规则
常见默认规则是:
RuntimeException及其子类:回滚;Error:回滚;- 受检异常
Exception:默认不一定回滚。
例如:
@Transactional
public void pay() throws IOException {
savePayment();
throw new IOException("network failure");
}
如果希望受检异常也回滚:
@Transactional(rollbackFor = IOException.class)
public void pay() throws IOException {
savePayment();
throw new IOException("network failure");
}
反过来,也可以指定某类异常不回滚:
@Transactional(noRollbackFor = BusinessNoticeException.class)
public void process() {
}
异常是否回滚和异常是否被捕获是两个问题。下面的代码可能导致事务正常提交:
@Transactional
public void process() {
try {
repository.save();
callRemote();
} catch (RuntimeException ex) {
log.warn("failed", ex);
}
}
异常已经在事务代理之外不可见,代理看到的是正常返回。如果业务要求回滚,应重新抛出异常,或者显式标记回滚:
@Transactional
public void process() {
try {
repository.save();
callRemote();
} catch (RuntimeException ex) {
TransactionAspectSupport.currentTransactionStatus()
.setRollbackOnly();
throw ex;
}
}
不过更清晰的做法通常是让异常传播到事务边界,由回滚规则统一处理。
5.4 REQUIRED、REQUIRES_NEW 和 NESTED
REQUIRED
@Transactional(propagation = Propagation.REQUIRED)
public void a() {
}
当前有事务就加入,没有就创建。它适合大多数同一业务操作中的服务方法。
REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void writeAudit() {
}
它会挂起外层事务并创建独立事务:
外层事务 T1
├─ 暂停 T1
├─ 执行内层事务 T2
├─ 提交或回滚 T2
└─ 恢复 T1
这意味着审计记录可能在外层业务回滚后仍然提交。它也可能增加连接池压力:外层事务占用一个连接,内层新事务还需要另一个连接。并发较高时,连接池容量必须能够承受这种嵌套占用。
NESTED
NESTED 通常依赖底层资源支持保存点。它不是一个完全独立的事务,而是在现有事务中建立保存点:
T1
├─ savepoint S
├─ 内层失败:回滚到 S
└─ 外层仍可继续
是否支持、具体行为取决于事务管理器和底层资源,不能仅凭注解名称推断所有环境都具有相同语义。
5.5 readOnly 和隔离级别不是强制魔法
@Transactional(readOnly = true)
public Order find(long id) {
return repository.findById(id).orElseThrow();
}
readOnly = true 是事务属性提示。数据库驱动、ORM 和事务管理器可能据此进行优化或限制,但它不等于 Java 层面禁止所有写操作,也不能保证任何数据库都拒绝写入。
隔离级别控制并发事务之间的数据可见性,例如:
READ_COMMITTED:只能读到已提交数据;REPEATABLE_READ:同一事务内重复读取通常保持一致;SERIALIZABLE:更强隔离,但可能降低并发;DEFAULT:使用数据库默认级别。
实际效果取决于数据库的并发控制实现。声明更高隔离级别不一定解决业务竞态,例如“读取库存后再扣减”仍可能需要数据库锁、原子更新或乐观锁。
5.6 事务代理失效的典型情况
自调用
this.inner();
不经过代理,事务拦截器不执行。
方法不可被代理覆写
private、static、final 方法不适合作为基于子类代理的拦截目标。事务边界应放在对外暴露的、可被代理调用的方法上。
从对象内部直接调用目标实例
如果代码绕过容器:
OrderService service = new OrderService(repository);
service.create();
这个对象没有被 Spring 事务代理包装,@Transactional 不会自动生效。
异步线程中继续使用原事务
@Transactional
public void create() {
executor.execute(() -> repository.saveOtherData());
}
新线程不会自动继承当前线程绑定的事务连接。异步任务需要自己建立事务,或者重新设计事务和消息边界。
六、把 IoC、生命周期、AOP、事件和事务串起来
下面给出一个最小的订单示例。它展示:
- IoC 通过构造器注入;
- 事务由代理建立;
- 保存成功后发布领域事件;
- 事件在事务提交后处理;
- Bean 在初始化阶段校验配置。
package com.example.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import jakarta.annotation.PostConstruct;
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
record OrderCreated(long orderId) {
}
record Order(long id, String status) {
}
interface OrderRepository {
void save(Order order);
}
@Component
class InMemoryOrderRepository implements OrderRepository {
@Override
public void save(Order order) {
System.out.println("save order " + order.id());
}
}
@Service
class OrderService {
private final OrderRepository repository;
private final ApplicationEventPublisher events;
OrderService(OrderRepository repository,
ApplicationEventPublisher events) {
this.repository = repository;
this.events = events;
}
@PostConstruct
void validate() {
System.out.println("OrderService initialized");
}
@Transactional
public void create(long orderId) {
repository.save(new Order(orderId, "CREATED"));
events.publishEvent(new OrderCreated(orderId));
}
}
@Component
class OrderCreatedHandler {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handle(OrderCreated event) {
System.out.println("notify order " + event.orderId());
}
}
这个示例如果使用内存仓储,不能真实演示数据库提交和回滚;要观察真实事务行为,需要把仓储替换为 JDBC、JPA 等支持事务的实现,并注册对应的事务管理器。
调用过程是:
外部调用 OrderService.create()
│
▼
OrderService 的事务代理
│
├─ 开启或加入事务
├─ 调用目标对象 create()
│ ├─ repository.save()
│ └─ publishEvent()
│ └─ AFTER_COMMIT 监听器暂不执行
├─ 方法正常返回
├─ 提交事务
└─ 执行 OrderCreatedHandler
如果 create() 在发布事件后抛出运行时异常:
@Transactional
public void create(long orderId) {
repository.save(new Order(orderId, "CREATED"));
events.publishEvent(new OrderCreated(orderId));
throw new IllegalStateException("simulate failure");
}
事务会回滚,AFTER_COMMIT 监听器不会执行。若改成普通 @EventListener,监听器可能已经在事务提交前执行,因此不能把普通事件监听器当成提交成功通知。
七、端到端运行方式与验证
7.1 创建工程
使用 Spring Initializr 创建 Maven 或 Gradle 工程,选择:
- Java 25;
- Spring Boot 的当前稳定版本;
- Spring Web;
- Spring Context;
- Spring TX;
- 真实项目中的 JDBC 或 JPA;
- 测试依赖。
Spring Boot 负责自动配置,例如:
- 创建
ApplicationContext; - 扫描启动类所在包及其子包;
- 根据类路径上的 Starter 注册基础设施;
- 绑定配置;
- 注册事务、Web 和数据库相关 Bean。
如果只使用上面的内存仓储,事务管理器可能没有真实资源可管理,因此只能验证 IoC、生命周期和事件注册,不能验证数据库回滚。
7.2 启动并观察结果
启动:
./mvnw spring-boot:run
或:
./gradlew bootRun
预期至少能看到:
OrderService initialized
如果通过测试或控制器调用:
orderService.create(1001L);
内存仓储示例输出类似:
save order 1001
notify order 1001
但是 notify 是否严格发生在数据库提交之后,必须使用真实事务资源验证。验证方法包括:
- 在
create()中写入数据库; - 写入后抛出
RuntimeException; - 检查业务表中没有该记录;
- 检查
AFTER_COMMIT监听器没有执行; - 改为正常返回,确认记录存在且监听器执行。
测试不应只断言日志,因为日志顺序不能证明数据库提交状态。
八、常见误解和诊断方法
8.1 “加了 @Transactional 就有事务”
诊断事务是否实际生效,需要检查调用路径:
调用方 → Spring Bean 代理 → 目标方法
而不是:
调用方 → new 出来的对象
调用方 → 同一个对象内部 this.method()
还要确认:
- 事务管理器是否存在;
- 数据访问是否使用同一个事务资源;
- 方法是否匹配事务切点;
- 是否被异常捕获;
- 是否在异步线程中执行;
- 是否因
readOnly、传播级别或数据库配置产生不同效果。
8.2 “事件就是可靠消息”
ApplicationEventPublisher 默认只是进程内调用机制。应用重启、进程崩溃或监听器失败都可能造成事件丢失或重复问题。需要持久化、重试和幂等时,应使用具备相应语义的消息系统或 Outbox 方案。
8.3 “Bean 构造器执行完就能使用”
构造器执行后,字段注入、@PostConstruct、代理包装等步骤可能尚未完成。不要在构造器中调用依赖当前 Bean 完整初始化状态的方法,也不要把 this 发布给外部线程或注册到外部系统。
8.4 “代理和目标对象是同一个对象”
通常不是。可以用以下方式诊断:
Object bean = context.getBean("orderService");
System.out.println(bean.getClass());
System.out.println(org.springframework.aop.support.AopUtils.isAopProxy(bean));
System.out.println(org.springframework.aop.support.AopUtils.isJdkDynamicProxy(bean));
System.out.println(org.springframework.aop.support.AopUtils.isCglibProxy(bean));
如果代理存在,应该从代理入口调用方法,不能保存原始对象引用绕过代理。
8.5 “事务回滚后数据库一定没有外部副作用”
事务只能原子地管理其覆盖的资源。HTTP 请求、邮件发送、文件写入和消息投递不一定与数据库事务共享同一个事务边界。
数据库事务成功
↓
调用邮件服务失败
或者:
发送邮件成功
↓
数据库事务回滚
都可能发生。解决方法不是盲目扩大 @Transactional,而是明确一致性要求,使用提交后事件、Outbox、补偿或幂等机制。
九、规范保证、实现细节与工程取舍
需要区分三个层次:
规范保证
Spring 的公开抽象保证了核心语义,例如:
- Bean 由容器按定义创建;
@Transactional通过事务拦截机制应用事务属性;@TransactionalEventListener根据事务阶段决定监听时机;ApplicationEventPublisher在应用上下文中传播事件。
常见实现
以下是常见但不应当当作所有实现都固定不变的细节:
- 使用 JDK 动态代理或基于子类的代理;
- 使用多级单例缓存处理部分早期引用;
- 使用线程上下文绑定事务资源;
- 使用后处理器生成代理和处理注解。
这些实现细节可能随 Spring 版本、代理配置和基础设施类型变化。
工程建议
以下属于设计建议,而不是框架强制规则:
- 优先构造器注入;
- 事务边界放在服务层;
- 避免自调用依赖代理;
- 将外部副作用放到提交后或可靠消息流程;
- 让事件消费者具备幂等性;
- 通过结构消除循环依赖;
- 不把
prototype或请求作用域对象随意保存到单例中。
掌握这些边界后,可以把一次典型调用准确还原为:
容器创建 Bean
→ 注入依赖
→ 执行初始化回调
→ 后处理器生成代理
→ 外部调用代理
→ AOP 拦截器建立事务
→ 目标方法发布事件
→ 事务提交或回滚
→ 按事件阶段执行监听器
→ 容器关闭时销毁 Bean
IoC 负责“对象从哪里来”,生命周期负责“对象何时可用”,AOP 负责“调用如何被拦截”,事件负责“事实如何在组件间传播”,事务代理负责“哪些操作在什么边界内提交或回滚”。Spring 的核心机制正是这些过程的组合,而不是任意一个注解单独产生的魔法。
系列导航与关联阅读
- 系列入口:Java 完整学习路线:从 Java 25 语言与 JVM 到 Spring、微服务和生产交付
- 上一篇:JPA 与 Hibernate:实体状态、关联、查询、事务和 N+1 诊断
- 下一篇:Spring Boot 工程基础:自动配置、Starter、配置绑定、Actuator 和启动
- 延伸:Java 反射与注解:Class、MethodHandle、元注解、处理器和边界
官方资料
本文依据 Java、Spring 与相关项目官方文档重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论