揭秘 @Transactional:徒手实现一个AOP事务管理器
我们都写过这样的代码:在 Service 方法上加一个 @Transactional
注解,这个方法就神奇地拥有了事务管理能力——成功则提交,异常则回滚。这背后没有 try-catch-finally
,没有 connection.commit()
,代码干净得令人愉悦。
这背后的魔法,就是 AOP (Aspect-Oriented Programming) 和动态代理 (Dynamic Proxy)。今天,我们的目标就是亲手实现这个魔法。
Chapter 1: The AOP/Proxy Paradigm - AOP与动态代理思想
在开始之前,我们必须理解一个颠覆性的概念:当你在一个 Bean 上使用 AOP 相关的功能(如 @Transactional
)时,Spring 容器最终返回给你的,已经不是你写的那个原版对象了!
它返回的是一个在运行时动态创建的代理对象 (Proxy)。
这个代理对象就像一个“经纪人”,它看起来和你写的原版对象(“明星”)一模一样(实现了相同的接口),但它内部却包裹着你的原版对象。当外界调用代理对象的方法时:
- 代理拦截:代理对象的同名方法会先被执行。
- 前置增强:代理可以在调用原版方法之前,执行一些额外的逻辑(例如:
开启事务
)。 - 调用目标:代理接着调用原版对象(目标 Target)的真实方法,执行核心业务逻辑。
- 后置增强:代理可以在调用原版方法之后,根据执行结果,再执行一些逻辑(例如:
提交事务
或回滚事务
)。
我们的任务,就是去实现这样一个能够自动为带有 @MiniTransactional
注解的 Bean 创建代理,并实现事务逻辑的机制。
Chapter 2: The Building Blocks - 定义注解与工具
首先,准备好我们的“建筑材料”。
@MiniTransactional
: 我们的事务注解。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD) // 注意:事务注解通常作用于方法上
@Retention(RetentionPolicy.RUNTIME)
public @interface MiniTransactional {
}
DataSourceUtils.java
: 一个模拟数据库连接管理的工具类。这里我们用 ThreadLocal
来模拟“一个线程持有一个连接”的场景,这与 Spring 管理事务连接的方式非常相似。
import java.sql.Connection;
/**
* 模拟数据源和连接管理
*/
public class DataSourceUtils {
// 使用 ThreadLocal 确保每个线程有自己独立的连接,互相不干扰
private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
// 模拟获取连接
public static Connection getConnection() {
System.out.println(" [DB] 模拟:从连接池获取连接...");
// 这里简单地返回一个 null 作为示意
connectionHolder.set(null);
return connectionHolder.get();
}
// 模拟提交
public static void commit() {
System.out.println(" [DB] 模拟:提交事务...");
}
// 模拟回滚
public static void rollback() {
System.out.println(" [DB] 模拟:回滚事务...");
}
// 模拟关闭连接
public static void closeConnection() {
System.out.println(" [DB] 模拟:关闭连接,归还到连接池...");
connectionHolder.remove();
}
}
Chapter 3: The Star of the Show - 代理创建器
现在,我们需要升级我们的 MiniApplicationContext
。在 Spring 中,这个功能由 BeanPostProcessor
的一个特殊实现(如 AnnotationAwareAspectJAutoProxyCreator
)完成。它会在 Bean 初始化之后,检查 Bean 是否需要被代理,如果需要,就用代理对象替换掉容器中的原始对象。
我们来模拟这个过程。在 MiniApplicationContext
中,我们在所有 Bean 初始化并完成依赖注入后,增加一个“代理包装”的步骤。
MiniApplicationContext.java
(新增 wrapInProxy
步骤)
// ... (保留上一篇博客中的所有代码)
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class MiniApplicationContext {
// ... (beanMap, metadataCache 和构造函数等都保留)
public MiniApplicationContext(Class<?>... componentClasses) {
try {
// 步骤一:实例化 Beans
instantiate(componentClasses);
// 步骤二:执行依赖注入
populateProperties();
// 新增!步骤三:对需要的 Bean 进行 AOP 代理包装
wrapBeansInProxy();
} catch (Exception e) {
e.printStackTrace();
}
}
// ... (instantiate, populateProperties, findAutowiringMetadata 等方法不变)
/**
* 新增:模仿 BeanPostProcessor 的 postProcessAfterInitialization 阶段
* 检查 Bean 是否有方法被 @MiniTransactional 注解,如果有,则为其创建代理对象
*/
private void wrapBeansInProxy() {
System.out.println("\n--- 步骤 3: AOP 代理包装 ---");
// 遍历容器中的每一个 Bean
for (Map.Entry<Class<?>, Object> entry : beanMap.entrySet()) {
Class<?> beanClass = entry.getKey();
Object originalBean = entry.getValue();
// 检查是否有任何方法被 @MiniTransactional 标记
boolean needsProxy = false;
for (Method method : beanClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(MiniTransactional.class)) {
needsProxy = true;
break;
}
}
if (needsProxy) {
System.out.printf(" [AOP] 发现 [%s] 需要事务代理。\n", beanClass.getSimpleName());
// 创建一个 InvocationHandler,我们的事务逻辑就在这里
InvocationHandler handler = new TransactionalInvocationHandler(originalBean);
// 使用 JDK 动态代理 API 创建代理对象
// 注意:JDK 动态代理要求目标类必须实现接口,这里为了简化,我们假设所有业务类都实现了接口
Object proxy = Proxy.newProxyInstance(
beanClass.getClassLoader(),
beanClass.getInterfaces(), // 关键:获取该类实现的所有接口
handler
);
// **关键一步**:用创建的代理对象,替换掉 IoC 容器中原始的 Bean 对象
entry.setValue(proxy);
System.out.printf(" [AOP] 成功创建代理,并将 [%s] 替换为代理对象。\n", beanClass.getSimpleName());
}
}
}
// ... (getBean 方法不变)
}
注意: 上述代码使用了 JDK 动态代理,它有一个限制:目标类(如我们的 UserService
)必须实现一个接口。Spring 在这种情况下也会优先使用 JDK 动态代理。如果目标类没有实现接口,Spring 会转而使用 CGLIB 来创建代理,CGLIB 通过继承目标类的方式来创建代理,更为强大。为了保持简单,我们这里遵循 JDK 代理的约定。
Chapter 4: The Transactional Logic - TransactionalInvocationHandler
这个 InvocationHandler
是我们 AOP 逻辑的核心,代理对象的所有方法调用都会被转发到它的 invoke
方法中。
TransactionalInvocationHandler.java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class TransactionalInvocationHandler implements InvocationHandler {
// 被代理的原始对象(目标对象)
private final Object target;
public TransactionalInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 关键:通过反射从【原始对象】中获取真正要被调用的方法
// 因为代理对象的方法和原始对象的方法是1:1的,但是注解在原始对象的方法上
Method originalMethod = target.getClass().getMethod(method.getName(), method.getParameterTypes());
// 1. 判断调用的方法是否被 @MiniTransactional 注解
if (!originalMethod.isAnnotationPresent(MiniTransactional.class)) {
// 如果没有,直接调用原始方法,不进行任何事务处理
return method.invoke(target, args);
}
// 2. 如果有,执行事务切面逻辑
System.out.println("---@MiniTransactional AOP---");
System.out.println("开启事务...");
DataSourceUtils.getConnection();
Object result;
try {
// 2.1 调用原始业务方法
result = method.invoke(target, args);
// 2.2 如果没有异常,提交事务
DataSourceUtils.commit();
System.out.println("事务已提交。");
} catch (Exception e) {
// 2.3 如果出现异常,回滚事务
DataSourceUtils.rollback();
System.out.println("事务已回滚。");
// 向上抛出异常,让调用方知道
throw e.getCause();
} finally {
// 2.4 无论如何,最后关闭连接
DataSourceUtils.closeConnection();
System.out.println("--------------------------");
}
return result;
}
}
Chapter 5: Putting It All Together - 组装与验证
最后,我们来改造一下业务类,并启动验证。
IUserService.java
(创建接口,因为 JDK 动态代理需要)
public interface IUserService {
void updateUser(String username, boolean shouldThrowException);
void findUser();
}
UserService.java
(实现接口并使用注解)
// 继承了 MiniComponent,但现在要实现接口
public class UserService implements IUserService {
@Override
@MiniTransactional // 在需要事务的方法上加上注解
public void updateUser(String username, boolean shouldThrowException) {
System.out.println(String.format(" [Service] 准备更新用户: %s", username));
// 模拟数据库操作
if (shouldThrowException) {
System.out.println(" [Service] 操作失败,抛出异常!");
throw new RuntimeException("数据库更新异常!");
}
System.out.println(String.format(" [Service] 成功更新用户: %s", username));
}
@Override
public void findUser() {
System.out.println(" [Service] 这是一个普通方法,不带事务。");
}
}
OrderService
也需要做类似修改,依赖 IUserService
接口而非实现类,这里从略。
MainApplication.java
public class MainApplication {
public static void main(String[] args) {
// ... 创建上下文的代码不变,但传入的类需要是 UserService.class
// 从容器中获取 Bean
// 注意:我们请求的是接口类型,容器返回的是代理对象
IUserService userService = context.getBean(UserService.class);
System.out.println("\n--- 验证场景1: 事务成功 ---");
userService.updateUser("Jack", false);
System.out.println("\n--- 验证场景2: 事务因异常而回滚 ---");
try {
userService.updateUser("Tom", true);
} catch (Exception e) {
System.out.println("捕获到业务层抛出的异常: " + e.getMessage());
}
System.out.println("\n--- 验证场景3: 调用普通方法 ---");
userService.findUser();
}
}
运行 MainApplication
,你将看到清晰的 AOP 执行轨迹:
... (实例化和注入的日志) ...
--- 步骤 3: AOP 代理包装 ---
[AOP] 发现 [UserService] 需要事务代理。
[AOP] 成功创建代理,并将 [UserService] 替换为代理对象。
--- 验证场景1: 事务成功 ---
---@MiniTransactional AOP---
开启事务...
[DB] 模拟:从连接池获取连接...
[Service] 准备更新用户: Jack
[Service] 成功更新用户: Jack
[DB] 模拟:提交事务...
事务已提交。
[DB] 模拟:关闭连接,归还到连接池...
--------------------------
--- 验证场景2: 事务因异常而回滚 ---
---@MiniTransactional AOP---
开启事务...
[DB] 模拟:从连接池获取连接...
[Service] 准备更新用户: Tom
[Service] 操作失败,抛出异常!
[DB] 模拟:回滚事务...
事务已回滚。
[DB] 模拟:关闭连接,归还到连接池...
--------------------------
捕获到业务层抛出的异常: 数据库更新异常!
--- 验证场景3: 调用普通方法 ---
[Service] 这是一个普通方法,不带事务。
Conclusion: Beyond Injection - The Power of Proxies
这次的旅程让我们看到了 Spring 框架的另一面。如果说 @Autowired
是对 Bean 内部状态的修改,那么 @Transactional
就是对 Bean 外部行为的增强。
核心思想总结:
- AOP 是面向切面:它将通用逻辑(如事务、日志、安全)从业务代码中剥离,形成“切面”,在不侵入代码的情况下应用这些逻辑。
- 动态代理是实现机制:Spring 通过在运行时创建一个“代理”对象,包裹住你的原始对象,从而获得了在你的方法执行前后“动手脚”的机会。
BeanPostProcessor
是“偷梁换柱”的钩子:这个 Spring 的扩展点,允许我们在 Bean 初始化的最后阶段,神不知鬼不觉地将原始 Bean 替换成它的代理。ThreadLocal
是线程状态的守护者:在 Web 环境中,每个请求是一个线程,ThreadLocal
确保了事务连接等状态在线程内部的隔离性。
现在,你已经掌握了 Spring 两大核心——DI 和 AOP 的基本实现原理。当你再次写下 @Autowired
和 @Transactional
时,它们在你眼中,应该已经不再是魔法,而是两套设计精妙、分工明确的工程蓝图。