news 2026/4/2 2:10:25

【Spring框架】通过JDK动态代理实现AOP

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Spring框架】通过JDK动态代理实现AOP

首先需要创建 maven java 项目,引入开发的坐标

<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.12</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.0.2.RELEASE</version> </dependency> </dependencies>
Account 类:存储账户信息(姓名、金额),对应数据库中的account表
public class Account { private int id; private String name; private double money; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getMoney() { return money; } public void setMoney(double money) { this.money = money; } @Override public String toString() { return "Account{" + "id=" + id + ", name='" + name + '\'' + ", money=" + money + '}'; } }
统一管理数据库连接和事务,确保同一事务中的所有数据库操作使用同一个连接,并提供事务的开启、提交、回滚等操作,最终保证要么所有操作都成功,要么都失败
这是控制事务的方式代码
事务管理工具类 PxUtils,每次直接调用就可以,不用再写事务管理得逻辑代码了
/** * 事务的工具类 */ public class PxUtils { private static DruidDataSource ds = null; // 使用ThreadLocal存储当前线程中的Connection对象 private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>(); // 在静态代码块中创建数据库连接池 static { try { // 通过代码创建C3P0数据库连接池 ds = new DruidDataSource(); ds.setDriverClassName("com.mysql.jdbc.Driver"); ds.setUrl("jdbc:mysql:///ssm"); ds.setUsername("root"); ds.setPassword("root"); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } /** * @Method: getConnection * @Description: 从数据源中获取数据库连接 */ public static Connection getConnection() throws SQLException { // 从当前线程中获取Connection Connection conn = threadLocal.get(); if (conn == null) { // 从数据源中获取数据库连接 conn = getDataSource().getConnection(); // 将conn绑定到当前线程 threadLocal.set(conn); } return conn; } /** * @Method: startTransaction * @Description: 开启事务 */ public static void startTransaction() { try { Connection conn = threadLocal.get(); if (conn == null) { conn = getConnection(); // 把 conn绑定到当前线程上 threadLocal.set(conn); } // 开启事务 conn.setAutoCommit(false); } catch (Exception e) { throw new RuntimeException(e); } } /** * @Method: rollback * @Description:回滚事务 */ public static void rollback() { try { // 从当前线程中获取Connection Connection conn = threadLocal.get(); if (conn != null) { // 回滚事务 conn.rollback(); } } catch (Exception e) { throw new RuntimeException(e); } } /** * @Method: commit * @Description:提交事务 */ public static void commit() { try { // 从当前线程中获取Connection Connection conn = threadLocal.get(); if (conn != null) { // 提交事务 conn.commit(); } } catch (Exception e) { throw new RuntimeException(e); } } /** * @Method: close * @Description:关闭数据库连接(注意,并不是真的关闭,而是把连接还给数据库连接池) */ public static void close() { try { // 从当前线程中获取Connection Connection conn = threadLocal.get(); if (conn != null) { conn.close(); // 解除当前线程上绑定conn threadLocal.remove(); } } catch (Exception e) { throw new RuntimeException(e); } } /** * @Method: getDataSource * @Description: 获取数据源 */ public static DataSource getDataSource() { // 从数据源中获取数据库连接 return ds; } }
为什么需要 ThreadLocal?
事务的核心要求是同一事务中的所有 SQL 必须用同一个数据库连接,否则无法保证原子性,ThreadLocal确保了一个线程中多次获取的连接是同一个,从而保证了事务的正确性。
事务的核心逻辑
close():释放连接(归还给连接池),并清除 ThreadLocal 中的连接
startTransaction():获取连接并关闭自动提交
若全部成功,commit():一次性提交所有 SQL,生效
若有失败,rollback():撤销所有 SQL,恢复到事务开始前的状态
持久层接口和实现类:定义数据操作方法save,用于将单个账户存入数据库
public interface AccountDao_px { void save(Account account) throws SQLException; }
public class AccountDaoPxImpl implements AccountDao_px { /** * 保存 * @param account */ public void save(Account account) throws SQLException { System.out.println("持久层:保存账户..."); // 把数据存储到数据库中 // 先获取到连接 Connection conn = PxUtils.getConnection(); // 编写 sql 语句 String sql = "insert into account values (null,?,?)"; // 预编译 SQL 语句 PreparedStatement stmt = conn.prepareStatement(sql); // 设置值 stmt.setString(1,account.getName()); stmt.setDouble(2,account.getMoney()); // 执行操作 stmt.executeUpdate(); // 关闭资源 ,conn 不能关闭 stmt.close(); } }
通过 PxUtils.getConnection() 获取数据库连接,这个连接由事务工具类统一管理,确保同一事务中使用同一个连接,从而让事务控制生效。
业务层接口和实现类:定义业务方法savaAll,用于保存两个账户
/** * 业务层接口 */ public interface AccountService_px { //保存两个账户 public void savaAll(Account account1, Account account2) throws SQLException; }
/** * 业务层实现类 */ public class AccountServicePxImpl implements AccountService_px { //DI依赖注入 private AccountDao_px accountDao_px; public void setAccountDao(AccountDao_px accountDao_px) { this.accountDao_px = accountDao_px; } public void savaAll(Account account1, Account account2) throws SQLException { //保存账号1 accountDao_px.save(account1); //显示除零错误 //int a=1/0; //保存账号2 accountDao_px.save(account2); System.out.println("保存成功"); } }
先保存第一个账户,再保存第二个账户,如果中间出现异常(打开int a=1/0),理论上第二个账户不应该保存成功,这就是事务要解决的问题。
动态代理工具类 JdkPoxy,通过 JDK 动态代理技术,在不修改原业务代码的前提下,为业务方法自动添加事务控制逻辑
/** * 生成代理对象 * 传入目标对象,生成该对象的代理对象,返回,在对目标对象的方法进行增强 */ public class JdkPoxy { /** * 获取代理对象的方法 返回代理对象 对目标对象的方法进行增强 */ public static Object getPoxy(final AccountService_px accountServicePx){ /** * 使用JDK动态代理生成代理对象 *第一个参数:类的加载器 *第二个参数:当前传入的对象实现了哪些接口要字节码对象 *第三个参数:回调函数 */ Object proxy = Proxy.newProxyInstance(JdkPoxy.class.getClassLoader(), accountServicePx.getClass().getInterfaces(), new InvocationHandler() { /** * 调用代理对象的方法invoke方法就会执行 */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //System.out.println("你调用了代理对象的invoke方法了。。。。。。。"); //对目标对象的方法进行增强 Object result = null; try { //开启事务 PxUtils.startTransaction(); result = method.invoke(accountServicePx, args); //提交事务 PxUtils.commit(); } catch (Exception e) { e.printStackTrace(); //回滚事务 PxUtils.rollback(); }finally { PxUtils.close(); } return result; } }); return proxy; } }
为什么用动态代理?
动态代理的本质是创建一个和目标对象实现相同接口的代理对象,当调用代理对象的方法时,会先执行定义的增强逻辑再执行原方法,这样避免了在每个业务方法中重复写事务代码。
JDK 动态代理的要求:必须基于接口,代理对象只能转换为接口类型(如AccountService_px proxy = (AccountService_px) proxyobj)。
invoke 方法的执行时机:当通过代理对象调用任何方法(如proxy.savaAll(...))时,invoke方法会被自动触发,其中method是当前调用的方法,args是方法的参数,method.invoke(accountServicePx, args)即调用目标对象的原方法执行真正的保存逻辑。
与事务的关联:代理类本身不直接管理事务,而是通过调用PxUtils的方法(startTransaction/commit/rollback)来实现事务控制,是事务逻辑的触发者。
applicationContext_px.xml 配置文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="accountDao_px" class="com.qcby.dao.impl.AccountDaoPxImpl"/> <bean id="accountServicePx" class="com.qcby.service.impl.AccountServicePxImpl"> <property name="accountDao" ref="accountDao_px"/> </bean> </beans>
测试方法
public class Text_px { @Test public void run1() throws SQLException { ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext_px.xml"); // 获取 service 对象 AccountService_px accountService_px = (AccountService_px) ac.getBean("accountServicePx"); Account account1 = new Account(); account1.setName("熊大"); account1.setMoney(1000d); Account account2 = new Account(); account2.setName("美羊羊"); account2.setMoney(1000d); // 生成代理对象 Object proxyobj = JdkPoxy.getPoxy(accountService_px); // 强转 AccountService_px proxy = (AccountService_px) proxyobj; // 调用代理对象方法 proxy.savaAll(account1,account2); } }
整个执行逻辑是:测试类Text_px通过Spring容器加载配置文件applicationContext_px.xml,获取由Spring管理的AccountService_px实现类实例;接着调用JdkPoxy的getPoxy方法,基于JDK动态代理为该Service实例生成代理对象,代理对象会拦截目标方法的调用;当通过代理对象调用savaAll方法时,代理的invoke方法先触发PxUtils开启事务(绑定连接到当前线程),再执行目标Service的savaAll方法——该方法调用AccountDao_px的save方法,DAO层通过PxUtils获取同一线程连接执行SQL插入两个账户数据;若执行中无异常,PxUtils提交事务,若有异常则回滚事务,最终无论成败都会关闭连接(归还连接池并解除线程绑定),从而通过动态代理实现了事务的统一管理,确保两个账户的保存操作要么同时成功,要么同时失败。
实现效果:
AccountServicePxImpl 打开int a=1/0,即中间出现异常的执行效果

总结

  • JDK 动态代理是实现手段:代码中JdkPoxy类通过Proxy.newProxyInstance()方法,基于AccountService_px接口生成代理对象,这是 JDK 动态代理的典型实现,依赖接口,通过反射生成代理类。
  • 与 AOP 核心概念的精准对应:代码将事务管理这一横切关注点从业务代码中分离出来,代理对象的invoke()方法中,通过PxUtils实现了事务的开启、提交、回滚、关闭等操作(这相当于AOP中的通知 Advice);被代理的AccountService_px的saveAll()方法是核心业务逻辑(相当于AOP中的连接点 Joinpoint);最终通过代理机制,在不修改业务代码的前提下,将事务管理逻辑织入到业务方法的执行流程中。AOP 的本质是将横切关注点与业务逻辑分离,并通过特定方式织入业务流程,这段代码完全符合这一思想。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/26 23:10:04

Langchain-Chatchat实现繁体字与简体字互转问答

Langchain-Chatchat 实现简繁体字互转问答 在企业级智能问答系统日益普及的今天&#xff0c;如何在保障数据安全的前提下&#xff0c;提升系统的语言适应能力&#xff0c;成为开发者关注的核心问题。尤其是在中文使用场景中&#xff0c;简体与繁体并存于不同地区——中国大陆广…

作者头像 李华
网站建设 2026/3/26 23:46:40

Langchain-Chatchat打造虚拟偶像互动系统

Langchain-Chatchat 打造虚拟偶像互动系统 在数字人、元宇宙和 AIGC 技术交织演进的今天&#xff0c;虚拟偶像早已不再是简单的动画形象或预录语音。她们需要“有记忆”、“懂情绪”&#xff0c;能与粉丝进行自然对话&#xff0c;甚至记住某位忠实支持者的名字和喜好——这种拟…

作者头像 李华
网站建设 2026/3/30 16:38:39

【课程设计/毕业设计】基于springboot+vue的自行车仓库管理系统设计与实现【附源码、数据库、万字文档】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/3/27 8:55:38

【毕业设计】基于springboot的足球训练营系统的设计与实现设计与实现(源码+文档+远程调试,全bao定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/4/1 0:05:32

利用DeepSeek计算abcde五人排成一队,要使c在ab 之间,有几种排法

我们来逐步推理&#xff1a;1. 理解条件 “c在ab之间”意思是 a、b、c三人的相对顺序必须是 a-c-b 或者 b-c-a&#xff08;即c在a和b正中间&#xff09;&#xff0c;并且它们三个人之间不一定相邻&#xff0c;但整体相对顺序要满足中间的是c。 条件&#xff1a;c的位置在a与b的…

作者头像 李华
网站建设 2026/3/31 22:34:33

786786

786786

作者头像 李华