Java-28 深入浅出 Spring 实现简易Ioc-04 在上节的业务下手动实现AOP

AOP 实现

这里实现的 AOP 只是一个简化版本,主要用于理解"通过代理统一控制事务"的基本思路。它还不具备 Spring AOP 那种完整的扩展能力,例如切点表达式、多个通知、注解解析、异常分类处理等。

本篇重点是:通过一个简单的 ProxyFactory,在业务方法执行前开启事务,在执行成功后提交事务,在出现异常时回滚事务。

AOP(面向切面编程)

AOP(Aspect-Oriented Programming,面向切面编程)是一种编程思想,主要用于处理那些"分散在多个业务模块中,但又不属于核心业务逻辑"的公共功能。

常见的横切逻辑包括:

  • 日志记录
  • 权限校验
  • 性能统计
  • 事务管理
  • 异常处理

如果不使用 AOP,这些公共逻辑往往会散落在各个 Service 方法中,导致业务代码和非业务代码混在一起。AOP 的作用就是把这些公共逻辑抽离出来,通过代理、字节码增强等方式统一织入到目标方法前后。

在本文的例子中,我们要抽离的公共逻辑就是事务控制。

AOP的核心概念

AOP 和 OOP 的关注点不同。OOP 更强调对象、封装、继承和多态;AOP 更强调把一类横向重复出现的逻辑抽出来,统一应用到多个目标方法上。

下面简单说明几个核心概念。

切面(Aspect)

切面可以理解为一组横切逻辑的封装。

例如事务管理就是一个切面:它不属于转账业务本身,但转账方法执行前需要开启事务,执行成功后需要提交事务,执行失败后需要回滚事务。

在本文中,WzkTransactionManagerProxyFactory 组合起来,承担了简易切面的作用。

连接点(Joinpoint)

连接点指程序执行过程中可以插入增强逻辑的位置。

在 Java Web 项目中,最常见的连接点就是方法执行。例如:

java 复制代码
wzkTransferService.transfer("1", "2", 100);

这个 transfer 方法就是一个可以被代理增强的位置。

通知(Advice)

通知指的是切面在连接点上执行的具体动作。

常见通知类型包括:

  • 前置通知:目标方法执行前执行。
  • 后置通知:目标方法执行后执行。
  • 环绕通知:目标方法执行前后都可以控制。
  • 异常通知:目标方法抛出异常后执行。
  • 返回通知:目标方法正常返回后执行。

本文中的事务控制更接近"环绕通知":在目标方法执行前开启事务,在目标方法执行后提交事务,如果出现异常则回滚事务。

切点(Pointcut)

切点用于描述哪些连接点需要被增强。

Spring AOP 中可以通过表达式、注解等方式匹配目标方法。本文暂时不实现复杂切点,只是在 Servlet 初始化时手动指定要代理的 Service 对象。

织入(Weaving)

织入指的是把切面逻辑应用到目标对象上的过程。

常见织入方式包括:

  • 编译时织入:编译阶段完成,例如 AspectJ。
  • 类加载时织入:类加载过程中修改字节码。
  • 运行时织入:程序运行时通过代理完成,例如 Spring AOP。

本文采用的是运行时织入,也就是通过 JDK 动态代理或 CGLIB 代理,在运行时为目标对象增加事务控制能力。

Resources

下面是本次使用的 XML 配置。这里的 Bean 会由自定义 BeanFactory 读取并创建。

xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!-- BeanFactory 类会处理这块配置 -->
<beans>
    <!-- WzkConnectionUtils 交给容器管理 -->
    <bean id="wzkConnectionUtils" class="wzk.utils.WzkConnectionUtils"></bean>

    <!-- JdbcWzkAccountDaoImpl 依赖 WzkConnectionUtils -->
    <bean id="wzkAccountDao" class="wzk.dao.impl.JdbcWzkAccountDaoImpl">
        <!-- name 是成员变量名,ref 是容器中被引用对象的 id -->
        <property name="WzkConnectionUtils" ref="wzkConnectionUtils"/>
    </bean>

    <!-- WzkTransferServiceImpl 依赖 WzkAccountDao -->
    <bean id="wzkTransferService" class="wzk.service.impl.WzkTransferServiceImpl">
        <property name="WzkAccountDao" ref="wzkAccountDao"></property>
    </bean>

    <!-- WzkTransactionManager 依赖 WzkConnectionUtils -->
    <bean id="wzkTransactionManager" class="wzk.utils.WzkTransactionManager">
        <property name="WzkConnectionUtils" ref="wzkConnectionUtils"></property>
    </bean>

    <!-- ProxyFactory 依赖 WzkTransactionManager -->
    <bean id="proxyFactory" class="wzk.factory.ProxyFactory">
        <property name="WzkTransactionManager" ref="wzkTransactionManager"></property>
    </bean>
</beans>

对应的截图如下所示:

WzkTransactionManager

事务管理器主要负责开启事务、提交事务和回滚事务。这里是对 JDBC 事务操作的一层简单封装。

java 复制代码
/**
 * 事务控制器
 * 这里是对 JDBC 事务操作的简单封装
 *
 * @author wzk
 * @date 11:31 2024/11/19
**/
public class WzkTransactionManager {

    private WzkConnectionUtils wzkConnectionUtils;

    public void setWzkConnectionUtils(WzkConnectionUtils wzkConnectionUtils) {
        this.wzkConnectionUtils = wzkConnectionUtils;
    }

    public void beginTransaction() throws SQLException {
        wzkConnectionUtils.getCurrentConnection().setAutoCommit(false);
    }

    public void commit() throws SQLException {
        wzkConnectionUtils.getCurrentConnection().commit();
    }

    public void rollback() throws SQLException {
        wzkConnectionUtils.getCurrentConnection().rollback();
    }

}

对应的截图如下所示:

Proxy

ProxyFactory

ProxyFactory 是代理工厂,主要负责给目标对象生成代理对象。

这里提供了两种方式:

  • JDK 动态代理:要求目标对象实现接口。
  • CGLIB 动态代理:可以代理普通类,但不能代理 final 类和 final 方法。

本文在 Servlet 中主要使用的是 JDK 动态代理。

java 复制代码
/**
 * 代理工厂
 * 通过动态代理统一增加事务控制逻辑
 *
 * @author wzk
 * @date 11:33 2024/11/19
**/
public class ProxyFactory {

    private WzkTransactionManager wzkTransactionManager;

    public void setWzkTransactionManager(WzkTransactionManager wzkTransactionManager) {
        this.wzkTransactionManager = wzkTransactionManager;
    }

    /**
     * JDK 动态代理
     * 适用于目标对象实现了接口的情况
     *
     * @author wzk
     * @date 11:35 2024/11/19
    **/
    public Object getJdkProxy(Object object) {
        return Proxy.newProxyInstance(
                object.getClass().getClassLoader(),
                object.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object result;
                        try {
                            // 开启事务
                            wzkTransactionManager.beginTransaction();

                            // 执行目标方法
                            result = method.invoke(object, args);

                            // 提交事务
                            wzkTransactionManager.commit();

                            return result;
                        } catch (Exception e) {
                            // 出现异常时回滚事务
                            wzkTransactionManager.rollback();
                            throw e;
                        }
                    }
                }
        );
    }

    /**
     * CGLIB 动态代理
     * 适用于目标对象没有实现接口的情况
     *
     * @author wzk
     * @date 13:57 2024/11/19
    **/
    public Object getCglibProxy(Object object) {
        return Enhancer.create(
                object.getClass(),
                new MethodInterceptor() {
                    @Override
                    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                        Object result;
                        try {
                            wzkTransactionManager.beginTransaction();

                            result = method.invoke(object, args);

                            wzkTransactionManager.commit();

                            return result;
                        } catch (Exception e) {
                            wzkTransactionManager.rollback();
                            throw e;
                        }
                    }
                }
        );
    }
}

对应的截图如下如下:

Controller

WzkServlet

WzkServlet 中,我们不再直接使用原始的 WzkTransferService,而是从 ProxyFactory 中获取一个代理对象。

这样调用 transfer 方法时,实际执行流程就变成了:

  1. 代理对象先开启事务。
  2. 调用原始 Service 的 transfer 方法。
  3. 如果执行成功,提交事务。
  4. 如果执行失败,回滚事务。
java 复制代码
@WebServlet(name="wzkServlet", urlPatterns = "/wzkServlet")
public class WzkServlet extends HttpServlet {

    // ========================== 2 ==========================
    // 由 BeanFactory 处理
    // private WzkTransferService wzkTransferService = (WzkTransferService) BeanFactory.getBean("wzkTransferService");
    // =======================================================

    // ========================== 3 ==========================
    // 另一种方式 相同的
    // private WzkTransferService wzkTransferService;
    // @Override
    // public void init() throws ServletException {
    //    super.init();
    //    this.wzkTransferService = (WzkTransferService) BeanFactory.getBean("wzkTransferService");
    // }
    // ======================================================

    // ================== 4 ======================
    // 获取代理对象,也就是带有事务控制能力的 Service
    private WzkTransferService wzkTransferService;

    @Override
    public void init() throws ServletException {
        super.init();

        // 从 BeanFactory 中获取代理工厂
        ProxyFactory proxyFactory = (ProxyFactory) BeanFactory.getBean("proxyFactory");

        // 对原始 Service 生成 JDK 动态代理
        this.wzkTransferService = (WzkTransferService) proxyFactory.getJdkProxy(
                BeanFactory.getBean("wzkTransferService"));
    }
    // ===========================================

    @Override
    protected void doGet(javax.servlet.http.HttpServletRequest req, javax.servlet.http.HttpServletResponse resp) throws javax.servlet.ServletException, IOException {
        System.out.println("=== WzkServlet doGet ===");

        // ======================= 1 =============================
        // 如果没有 BeanFactory 和代理工厂,就需要手动创建对象并维护依赖关系
        // 组装 DAO,DAO 层依赖 ConnectionUtils 和 DruidUtils
        // JdbcWzkAccountDaoImpl jdbcWzkAccountDaoImpl = new JdbcWzkAccountDaoImpl();
        // jdbcWzkAccountDaoImpl.setConnectionUtils(new WzkConnectionUtils());

        // 组装 Service
        // WzkTransferServiceImpl wzkTransferService = new WzkTransferServiceImpl();
        // wzkTransferService.setWzkAccountDao(jdbcWzkAccountDaoImpl);
        // ======================================================

        // 执行业务逻辑
        System.out.println("wzkTransferService: " + wzkTransferService);

        try {
            wzkTransferService.transfer("1", "2", 100);
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("=== transfer error ====");
        }

        resp.setContentType("application/json;charset=utf-8");
        resp.getWriter().print("=== WzkServlet doGet ===");
    }

}

对应的截图如下所示:

测试运行1

先测试正常执行的情况。

访问接口后,可以看到程序顺利执行:

对应的数据库数据也正常发生了变化:

这说明在没有异常的情况下,代理对象成功完成了事务提交。

测试运行2

接着测试出现异常时的回滚情况。

可以在 WzkTransferServiceImpltransfer 方法中,手动加入一个异常,例如 1 / 0

java 复制代码
@Override
public void transfer(String fromCard, String toCard, int money) throws Exception {
    WzkAccount from = wzkAccountDao.selectWzkAccount(fromCard);
    WzkAccount to = wzkAccountDao.selectWzkAccount(toCard);

    from.setMoney(from.getMoney() - money);
    to.setMoney(to.getMoney() + money);

    int fromResult = wzkAccountDao.updateWzkAccount(from);

    // 故意制造异常,测试事务回滚
    int i = 1 / 0;

    int toResult = wzkAccountDao.updateWzkAccount(to);
    System.out.println("transfer fromResult: " + fromResult + " toResult: " + toResult);
}

对应的代码截图如下所示。执行之后,数据库中的数据应该不会变化,因为事务管理器会执行回滚:

接口调用结果如下,可以看到程序确实报错了:

数据库数据如下:

可以看到,即使前面已经执行了转出账户的更新,只要后续出现异常,代理对象就会调用事务管理器进行回滚,最终数据库数据不会被部分修改。

这就是一个简易 AOP 事务控制的基本过程:业务方法只关心转账逻辑,事务开启、提交、回滚都交给代理对象统一处理。


TL;DR

  • 场景 :用 JDK 动态代理 + 简易 ProxyFactory 把事务控制从 WzkTransferService 中抽离出来,验证 AOP"业务与非业务逻辑分离"的核心思路。
  • 结论ProxyFactory 在方法执行前开启事务、正常返回后提交事务、捕获到 Exception 时回滚并向上抛出;只要把代理对象注入 WzkServlet,业务代码无须任何事务样板。
  • 产出beans.xml + WzkTransactionManager + ProxyFactory + WzkServlet 完整可运行链路,以及"正常提交 vs 异常回滚"两套对照截图,可作为后续学习 Spring @Transactional 的前置铺垫。

SEO 摘要(约 250 字)

AOP(Aspect-Oriented Programming,面向切面编程)是 Java 后端处理横切关注点(事务、日志、权限、异常)的核心思想。Spring 6.x(2024--2026 年间持续迭代)依然沿用 JDK 动态代理 + CGLIB 双轨制:JDK 代理基于 java.lang.reflect.ProxyInvocationHandler,要求目标对象实现接口;CGLIB 通过字节码生成子类实现代理,可代理普通类但不能代理 final 类/final 方法。本文给出一套"教学级 AOP"实现:通过自定义 BeanFactory 读取 beans.xml,由 WzkTransactionManager 封装 JDBC 的 setAutoCommit(false) / commit() / rollback(),再由 ProxyFactory 在方法执行前开启事务、正常返回后提交、捕获 Exception 时回滚。WzkServlet.init() 从容器拿到 ProxyFactory,对 WzkTransferService 生成 JDK 代理,调用 transfer("1","2",100) 即可触发完整事务流程。文章附带正常执行与 1/0 异常两轮对比截图,直观展示部分更新被事务回滚撤销的过程,是理解 Spring @TransactionalProxyFactoryBean 的入门铺垫。


版本矩阵

模块 / 关键点 状态 说明
AOP 核心概念(Aspect / Joinpoint / Advice / Pointcut / Weaving) ✅ 已验证 与 Spring AOP 官方术语一致,可在 Spring 文档中对照阅读
Proxy.newProxyInstance + InvocationHandler 实现 JDK 动态代理 ✅ 已验证 Java SE 自带反射 API,JDK 8+ 稳定可用
CGLIB 通过子类继承实现方法拦截 ✅ 已验证 Spring 6.x 已将 CGLIB 迁入 spring-core 内部维护,规避外部 cglib 与 JDK 17+ 模块化冲突
Spring AOP 默认在目标实现接口时使用 JDK 代理,否则使用 CGLIB ✅ 已验证 Spring 6.x 文档与社区资料一致;可通过 @EnableTransactionManagement(proxyTargetClass = true) 强制 CGLIB
WzkTransactionManager 封装 setAutoCommit / commit / rollback ✅ 已验证 JDBC 标准 API,连接绑定到 ThreadLocal 即可保证同一线程复用
ProxyFactory.getJdkProxy 在 catch 中回滚并 rethrow ✅ 已验证 是本文事务链路能正确工作的关键
织入方式(编译时 / 类加载时 / 运行时)三类 ✅ 已验证 本文示例为运行时织入,对应 Spring AOP 模式;AspectJ 走编译时或类加载时
自定义 BeanFactory 读取 beans.xml ✅ 已验证 教学用 XML 容器,与 Spring XmlBeanFactory 思路一致
完整 Spring 切点表达式 / 注解驱动 / 通知顺序 ❌ 不在本文范围 教学版只展示运行时环绕通知;Spring 提供 @Aspect + execution(...) 全套能力
分布式事务 / @Transactional 传播行为 / 只读优化 ❌ 不在本文范围 单库单连接事务示意,生产应使用 Spring 事务管理器配置传播与隔离级别

错误速查卡

症状 根因 定位 修复
ProxyFactory.getJdkProxy(...)IllegalArgumentException: ... is not an interface 目标类没有实现任何接口,object.getClass().getInterfaces() 长度为 0 在调用前打印 target.getClass().getInterfaces().length 改用 getCglibProxy,或给目标类补充接口
事务开启/提交报 SQLException: Connection is already closed WzkConnectionUtils 没有用 ThreadLocal 把连接绑到当前线程,每次都拿到新连接 WzkConnectionUtils.getCurrentConnection 里检查是否从 ThreadLocal 改用 ThreadLocal<Connection> 绑定,Service 与 TransactionManager 共用同一个连接
出现异常时事务没有回滚,数据库部分更新 invokecatch (Throwable) 之前 result = method.invoke(object, args) 抛了 Error 而不是 Exception 打印异常类型确认是 Exception / Error 改为 catch (Throwable t),或确保业务异常继承 Exception
CGLIB 报 Cannot subclass final class 目标类或方法被 final 修饰,CGLIB 无法继承 看目标类/方法声明 去掉 final,或改用 JDK 代理(前提是目标实现接口)
CGLIB 在 JDK 17+ 报 IllegalAccessError / 模块访问错误 老版本 cglib-nodep 与 JDK 模块化不兼容 看堆栈是否涉及 sun.reflect / java.lang 模块访问 升级 Spring 6.x(自带 spring-cglib),或在启动参数加 --add-opens java.lang/java.lang.reflect=ALL-UNNAMED
业务方法正常返回但数据库没变化 setAutoCommit(false) 没生效,或连接不是 getCurrentConnection() 拿到的同一个 beginTransaction / commit 里打印 connection.hashCode() 比对 保证 DAO 与 TransactionManager 共享同一个 ThreadLocal 连接
Servlet 启动报 ClassCastException: com.sun.proxy.$Proxy42 cannot be cast to WzkTransferService JDK 动态代理对象是 $Proxy,强转目标实现类失败 看错误堆栈确认强转目标 只强转为目标实现的接口,例如 WzkTransferService,不要强转到实现类
注解 @WebServlet 不生效,接口 404 Servlet 容器没扫描到注解类 确认部署描述 web.xml 没有覆盖注解,或容器版本过低 升级到 Servlet 3.0+ 容器;或在 web.xml 显式声明 Servlet
ProxyFactory 注入 WzkTransactionManager 为 null BeanFactory 没处理 <property> 注入,或 setter 名大小写不匹配 setWzkTransactionManager 里打印参数 保证 <property name="WzkTransactionManager">name 与 setter 名一致
调用 transfer 后事务未提交、连接一直占用 业务方法内部捕获了异常并吞掉,导致代理 catch 不到 检查业务方法是否有 try { ... } catch (Exception e) { log(...); } 让异常抛出到代理层,或在业务 catch 后重新 throw
测试正常时 commitCannot commit when autoCommit is enabled 拿到的连接是新的、没经过 beginTransaction 处理 看连接是否被复用 getCurrentConnection 里检查并 setAutoCommit(false) 后再返回
部分接口切了事务、部分没切 只有从 ProxyFactory.getJdkProxy(...) 拿到的对象才带事务,直接 new WzkTransferServiceImpl() 调用没事务 grep 项目里直接 new Service 的位置 强制所有调用走 BeanFactory 拿代理对象,或迁移到 Spring @Transactional

参考来源


作者:武子康的个人博客

相关推荐
AskHarries1 小时前
Shell Tool:命令执行、输出读取和长任务管理
后端
张某布响丸辣1 小时前
Spring AI 极简入门:Java 开发者快速上手 AI 开发
java·人工智能·spring·springai
java1234_小锋1 小时前
请描述 Spring Boot 的启动流程,包括 SpringApplication 的初始化和 run 方法的核心步骤。
java·数据库·spring boot
疯狂成瘾者1 小时前
Java 集合 LinkedList 详解:链表结构、常用方法和队列使用
java·开发语言·链表
lanyxp1 小时前
Sentinel 管不到 SQL 这一层——我写了个 MyBatis SQL 熔断器
java
小闹5491 小时前
Docker 如何才能学的更扎实
后端·程序员
XovH2 小时前
MySQL 系列:第10篇 存储过程与自定义函数
后端
XovH2 小时前
MySQL 系列:第8篇 子查询与集合操作
后端
XovH2 小时前
MySQL 系列:第9篇 视图——定制化数据窗口
后端