揭秘 @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)

这个代理对象就像一个“经纪人”,它看起来和你写的原版对象(“明星”)一模一样(实现了相同的接口),但它内部却包裹着你的原版对象。当外界调用代理对象的方法时:

  1. 代理拦截:代理对象的同名方法会先被执行。
  2. 前置增强:代理可以在调用原版方法之前,执行一些额外的逻辑(例如:开启事务)。
  3. 调用目标:代理接着调用原版对象(目标 Target)的真实方法,执行核心业务逻辑。
  4. 后置增强:代理可以在调用原版方法之后,根据执行结果,再执行一些逻辑(例如:提交事务回滚事务)。

我们的任务,就是去实现这样一个能够自动为带有 @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 外部行为的增强。

核心思想总结:

  1. AOP 是面向切面:它将通用逻辑(如事务、日志、安全)从业务代码中剥离,形成“切面”,在不侵入代码的情况下应用这些逻辑。
  2. 动态代理是实现机制:Spring 通过在运行时创建一个“代理”对象,包裹住你的原始对象,从而获得了在你的方法执行前后“动手脚”的机会。
  3. BeanPostProcessor 是“偷梁换柱”的钩子:这个 Spring 的扩展点,允许我们在 Bean 初始化的最后阶段,神不知鬼不觉地将原始 Bean 替换成它的代理。
  4. ThreadLocal 是线程状态的守护者:在 Web 环境中,每个请求是一个线程,ThreadLocal 确保了事务连接等状态在线程内部的隔离性。

现在,你已经掌握了 Spring 两大核心——DI 和 AOP 的基本实现原理。当你再次写下 @Autowired@Transactional 时,它们在你眼中,应该已经不再是魔法,而是两套设计精妙、分工明确的工程蓝图。