AOP 概念的引入

第一部分:业务场景与问题引入

1.1 业务场景

要求对两个账户进行保存(模拟转账),这两个账户的保存操作必须同时成功或同时失败,构成一个最小逻辑执行单元(事务)。

业务层 AccountServiceImpl 代码如下:

复制代码
public class AccountServiceImpl implements AccountService {
    private AccountDao accountDao;

    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }

    /**
     * 对 saveAll 方法进行增强
     */
    @Override
    public void saveAll(Account account1, Account account2) throws SQLException {
        // 保存 1 账号
        accountDao.save(account1);
        // 模拟异常
        // int a = 1/0;
        // 保存 2 账号
        accountDao.save(account2);
    }
}

1.2 问题所在

  • 若只有第一个账号保存成功,第二个账号保存失败(如发生异常),数据就不一致。
  • 转账场景:一个账户扣钱,另一个没加上,钱就"转丢了"。

难点:理解为什么这两个保存操作必须绑在一起。因为它们是同一个事务,中途不能断开。


第二部分:传统解决方案的痛点

2.1 手动控制事务

做法:用 JDBC 工具类在业务层手动控制事务。

核心逻辑:startTransaction(底层 setAutoCommit(false))关闭自动提交,执行业务操作,无异常则 commit,有异常则 rollback,最后 close 释放资源。

2.2 持久层配合

持久层的 save 方法在插入数据后,不能关闭 Connection,因为事务要在业务层控制。

复制代码
public class AccountDaoImpl implements AccountDao {
    @Override
    public void save(Account account) throws SQLException {
        // 获取连接(注意 conn 不能关闭)
        Connection conn = TxUtils.getConnection();
        String sql = "insert into account values (null,?,?)";
        PreparedStatement stmt = conn.prepareStatement(sql);
        stmt.setString(1, account.getName());
        stmt.setDouble(2, account.getMoney());
        stmt.executeUpdate();
        // 关闭资源,conn 不能关闭
        stmt.close();
    }
}

2.3 痛点分析

  • 代码重复 :如果业务里有 10 套类似操作,就要在每个业务方法里写一遍 try-catch 和事务控制代码。
  • 侵入性强:业务代码里混杂了大量事务控制的非业务代码,不符合解耦原则。
  • 维护困难:改动事务逻辑需要去每个业务方法里修改。

重点:需要把事务控制逻辑向上抽取,动态地在业务方法执行前、执行后、抛出异常时织入处理,这就是 AOP(面向切面编程)的核心思想。


第三部分:JDK 动态代理的引入

既然不能在业务代码里写死,能不能找一个"中间人",帮我们完成开启事务、提交、回滚的事情?这就是动态代理。

3.1 生成代理对象的代码

编写 JdkProxy 类,实现 InvocationHandler 接口,用于对目标对象的方法进行增强。

复制代码
package com.qcbyjy.demo1;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 传入目标对象,生成该对象的代理对象,返回。对目标对象的方法进行增强
 */
public class JdkProxy {

    /**
     * 获取代理对象,返回,增强目标对象的方法
     */
    public static Object getProxy(AccountService accountService) {
        /**
         * 使用 Jdk 的动态代理生成代理对象
         */
        Object proxy = Proxy.newProxyInstance(
                JdkProxy.class.getClassLoader(),
                accountService.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * 调用代理对象的方法,invoke 方法就会去执行
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object result = null;
                        try {
                            // 开启事务
                            TxUtils.startTransaction();
                            // 对目标对象的方法进行增强
                            result = method.invoke(accountService, args);
                            // 提交事务
                            TxUtils.commit();
                        } catch (Exception e) {
                            e.printStackTrace();
                            TxUtils.rollback();
                        } finally {
                            // 关闭资源
                            TxUtils.close();
                        }
                        return result;
                    }
                });
        return proxy;
    }
}

3.2 JDK 动态代理核心 API

Proxy.newProxyInstance 需要传入三个参数:

参数 说明
ClassLoader loader 代理类的类加载器(如 JdkProxy.class.getClassLoader())
Class<?>[] interfaces 代理的目标对象的接口(如 accountService.getClass().getInterfaces())
InvocationHandler h 调用处理程序,内部实现增强逻辑

重点:JDK 动态代理基于接口实现,所以目标对象必须实现至少一个接口。

易错点:传入的 accountService 实际应为接口类型。所有实现了该接口的类都满足代理条件。

3.3 测试与执行流程

复制代码
@Test
public void run1() throws SQLException {
    ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext_demo1.xml");
    // 获取 service 对象(目标对象)
    AccountService accountService = (AccountService) ac.getBean("accountService");

    // 准备两个账户
    Account account1 = new Account();
    account1.setName("熊大");
    account1.setMoney(1000d);
    Account account2 = new Account();
    account2.setName("美羊羊");
    account2.setMoney(1000d);

    // 生成代理对象
    Object proxyobj = JdkProxy.getProxy(accountService);
    // 强转
    AccountService proxy = (AccountService) proxyobj;
    // 调用代理对象方法
    proxy.saveAll(account1, account2);
}

执行流程:

  1. 测试类调用代理对象的 saveAll。
  2. 代理对象拦截调用,进入 invoke 方法。
  3. invoke 内部先开事务,再通过反射 method.invoke(accountService, args) 调用原对象的目标方法。
  4. 如果发生除零异常,目标方法抛出,被 catch 捕获,执行 rollback 回滚。
  5. 如果没有异常,执行 commit 提交。
  6. finally 中统一关闭资源。

重点:由于异常抛出,代理类没有提交事务,导致数据添加不进去。这验证了代理对象能够拦截异常并回滚,从而实现事务控制。

3.4 静态代理与动态代理的区别

  • 静态代理:基于接口实现,需要手动编写代理类。
  • JDK 动态代理:基于接口实现,通过反射动态生成代理类。
  • CGLIB 代理:基于继承实现,通过生成目标类的子类来代理。逻辑相似但机制不同。

易错点:JDK 动态代理和静态代理都是基于接口,不要误以为静态也用继承。CGLIB 才是基于继承。


第四部分:从动态代理到 AOP 概念

4.1 为什么提出 AOP?

手动写 JDK 动态代理类仍然繁琐:每个接口都要写一套代理逻辑。AOP 底层就是封装了 JDK 动态代理(或 CGLIB 代理)。我们只需写个切面类、加注解并做简单配置,就能省去复杂的代理代码。

4.2 AOP 专业术语与动态代理的映射

AOP 术语 动态代理中的对应逻辑
前置通知 动态代理中开启事务
后置通知 动态代理中提交事务
异常通知 动态代理中回滚事务
最终通知 动态代理中关闭资源

重点:AOP 本质是用约定的术语替代了手动编写代理类的代码。开发中我们只需在配置文件中通过 AOP 配置即可生效,切面类和通知方法自己写。

易错点:别把后置通知和最终通知混淆。后者不管成败都关资源,前者仅正常提交。

4.3 AOP 概念初步引入

  • AOP 是 OOP 的延续,是软件开发中的一个热点,也是 Spring 框架中的重要内容。
  • 它采取横向抽取机制,取代了传统纵向继承体系重复性代码(如性能监视、事务管理、安全检查、缓存)。
  • 核心目的:在不修改源代码的前提下,对程序进行增强。

重点:AOP 不止做事务控制,还能做日志监控、程序增强扩展等,后续会深入学习其实现方式和概念。

相关推荐
凉茶钱1 小时前
多态:从虚函数语法到虚表寻址,一次讲透动态绑定
开发语言·c++·多态
重生之我是Java开发战士1 小时前
【Redis】Java & JavaSpring 客户端
java·redis·bootstrap
Hello.Reader1 小时前
Three.js / Cesium 卡顿怎么办?把三维渲染搬到 GPU 服务器
服务器·开发语言·javascript
Solange__1 小时前
AI世界模型、3D场景生成和游戏引擎怎么选?按5类任务比较交互、物理与导出
开发语言·人工智能·游戏·平面·3d·自然语言处理·游戏引擎
椒颜皮皮虾྅1 小时前
模型部署不再卡在环境上:DeploySharp 四大推理引擎与 Windows/Linux 离线交付实战
linux·运维·开发语言·人工智能·windows·计算机视觉·cuda
郭小李1 小时前
mybatis的 Mapper.xml语法
xml·java·mybatis
小蒜学长2 小时前
基于SpringBoot 框架的自媒体新闻系统的设计与实现(代码+数据库+LW)
java·spring boot·后端·mysql数据库·自媒体新闻管理系统
9624562 小时前
补打一条卡,为什么要从前一天重新算:人工补录与实时重算业务复盘
java·mysql·kafka
codebolt2 小时前
C++程序crash分析和内存检测
开发语言·c++