JDK 动态代理动态织入事务详解
一、目标:手动实现事务代理
Spring 的@Transactional底层就是通过动态代理织入事务的。我们手动实现一个简化版,彻底理解这个过程。
需求:Service 方法执行前开启事务,执行成功提交事务,抛出异常回滚事务。
调用 Service 方法 → 代理拦截 → 开启事务 → 执行目标方法 → 成功 → 提交 → 异常 → 回滚 → 抛出异常二、完整代码实现
1. 事务管理器
模拟一个事务管理器,负责开启、提交、回滚事务。
publicclassTransactionManager{// 模拟 ThreadLocal 保存当前线程的事务状态privatestaticfinalThreadLocal<Boolean>transactionStatus=newThreadLocal<>();publicvoidbegin(){System.out.println("【事务】开启事务");transactionStatus.set(true);}publicvoidcommit(){System.out.println("【事务】提交事务");transactionStatus.remove();}publicvoidrollback(){System.out.println("【事务】回滚事务");transactionStatus.remove();}publicbooleanisActive(){returnBoolean.TRUE.equals(transactionStatus.get());}}2. 抽象主题
publicinterfaceUserService{voidsave(Useruser);UserfindById(Longid);voidbatchSave(List<User>users);}3. 真实主题
publicclassUserServiceImplimplementsUserService{@Overridepublicvoidsave(Useruser){System.out.println("保存用户:"+user.getName());// 模拟数据库操作}@OverridepublicUserfindById(Longid){System.out.println("查询用户:"+id);returnnewUser(id,"张三");}@OverridepublicvoidbatchSave(List<User>users){for(Useruser:users){System.out.println("批量保存:"+user.getName());}// 模拟异常if(users.size()>5){thrownewRuntimeException("批量保存失败");}}}4. 事务 InvocationHandler
核心逻辑在invoke()方法中:方法执行前开启事务,成功提交,异常回滚。
publicclassTransactionInvocationHandlerimplementsInvocationHandler{// 目标对象privatefinalObjecttarget;// 事务管理器privatefinalTransactionManagertransactionManager;publicTransactionInvocationHandler(Objecttarget,TransactionManagertransactionManager){this.target=target;this.transactionManager=transactionManager;}@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{// 1. 判断方法是否需要事务if(!needTransaction(method)){returnmethod.invoke(target,args);}// 2. 开启事务transactionManager.begin();Objectresult=null;try{// 3. 执行目标方法result=method.invoke(target,args);// 4. 方法正常返回,提交事务transactionManager.commit();returnresult;}catch(Exceptione){// 5. 方法抛出异常,回滚事务transactionManager.rollback();// 6. 把原始异常抛出去throwe.getCause()!=null?e.getCause():e;}}/** * 判断方法是否需要事务 * 查询方法不需要事务,增删改方法需要 */privatebooleanneedTransaction(Methodmethod){StringmethodName=method.getName();returnmethodName.startsWith("save")||methodName.startsWith("update")||methodName.startsWith("delete")||methodName.startsWith("batch");}}5. 客户端调用
publicclassClient{publicstaticvoidmain(String[]args){// 1. 创建真实对象UserServicetarget=newUserServiceImpl();// 2. 创建事务管理器TransactionManagertxManager=newTransactionManager();// 3. 创建代理对象UserServiceproxy=(UserService)Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),newTransactionInvocationHandler(target,txManager));// 4. 通过代理调用System.out.println("===== 正常保存 =====");proxy.save(newUser(1L,"张三"));System.out.println("\n===== 正常查询(无事务)=====");proxy.findById(1L);System.out.println("\n===== 批量保存异常 =====");try{proxy.batchSave(Arrays.asList(newUser(1L,"A"),newUser(2L,"B"),newUser(3L,"C"),newUser(4L,"D"),newUser(5L,"E"),newUser(6L,"F")));}catch(Exceptione){System.out.println("捕获异常:"+e.getMessage());}}}6. 输出结果
===== 正常保存 ===== 【事务】开启事务 保存用户:张三 【事务】提交事务 ===== 正常查询(无事务)===== 查询用户:1 ===== 批量保存异常 ===== 【事务】开启事务 批量保存:A 批量保存:B 批量保存:C 批量保存:D 批量保存:E 批量保存:F 【事务】回滚事务 捕获异常:批量保存失败三、代理类的结构
Proxy.newProxyInstance()生成的代理类大致如下:
publicfinalclass$Proxy0extendsProxyimplementsUserService{privatestaticMethodm0;// saveprivatestaticMethodm1;// findByIdprivatestaticMethodm2;// batchSavepublic$Proxy0(InvocationHandlerh){super(h);}@Overridepublicfinalvoidsave(Useruser){try{super.h.invoke(this,m0,newObject[]{user});}catch(Throwablee){thrownewUndeclaredThrowableException(e);}}@OverridepublicfinalUserfindById(Longid){try{return(User)super.h.invoke(this,m1,newObject[]{id});}catch(Throwablee){thrownewUndeclaredThrowableException(e);}}@OverridepublicfinalvoidbatchSave(List<User>users){try{super.h.invoke(this,m2,newObject[]{users});}catch(Throwablee){thrownewUndeclaredThrowableException(e);}}}每个方法都把调用转发到InvocationHandler.invoke(),事务逻辑就在这里织入。
四、关键细节
1. 异常处理
method.invoke()抛出的异常会被包装成InvocationTargetException,需要取出原始异常:
catch(Exceptione){transactionManager.rollback();// e 是 InvocationTargetException,需要 getCause()throwe.getCause()!=null?e.getCause():e;}2. 事务传播
上面的实现是简化版,每次调用都开启新事务。真实场景中需要处理事务传播:
- 当前没有事务时,开启新事务。
- 当前已有事务时,加入当前事务(
REQUIRED)。 - 当前已有事务时,挂起当前事务,开启新事务(
REQUIRES_NEW)。
简化版传播处理:
@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{if(!needTransaction(method)){returnmethod.invoke(target,args);}// 如果当前已有事务,直接加入if(transactionManager.isActive()){returnmethod.invoke(target,args);}// 没有事务,开启新事务transactionManager.begin();try{Objectresult=method.invoke(target,args);transactionManager.commit();returnresult;}catch(Exceptione){transactionManager.rollback();throwe.getCause()!=null?e.getCause():e;}}3. 只读事务
查询方法可以标记为只读,优化性能:
privatebooleanisReadOnly(Methodmethod){Stringname=method.getName();returnname.startsWith("find")||name.startsWith("get")||name.startsWith("query")||name.startsWith("select");}4. 事务超时
记录事务开始时间,超时后自动回滚:
longstart=System.currentTimeMillis();try{Objectresult=method.invoke(target,args);longcost=System.currentTimeMillis()-start;if(cost>timeout){transactionManager.rollback();thrownewTransactionTimeoutException("事务超时");}transactionManager.commit();returnresult;}catch(Exceptione){transactionManager.rollback();throwe.getCause()!=null?e.getCause():e;}五、内部调用失效问题
这是动态代理最经典的坑。同一个类中方法 A 调用方法 B,B 的事务不会生效。
publicclassUserServiceImplimplementsUserService{@Overridepublicvoidsave(Useruser){// 直接调用 this.batchSave(),不经过代理batchSave(Arrays.asList(user,user));}@OverridepublicvoidbatchSave(List<User>users){// 事务不生效}}原因:save()内部调用batchSave()时,this是原始对象,不是代理对象。只有外部通过代理调用才会触发InvocationHandler.invoke()。
解决方案一:注入自身代理
@ServicepublicclassUserServiceImplimplementsUserService{@AutowiredprivateUserServiceself;// 注入代理对象@Overridepublicvoidsave(Useruser){self.batchSave(Arrays.asList(user,user));// 通过代理调用}@OverridepublicvoidbatchSave(List<User>users){// 事务生效}}解决方案二:使用 AopContext
@ServicepublicclassUserServiceImplimplementsUserService{@Overridepublicvoidsave(Useruser){((UserService)AopContext.currentProxy()).batchSave(Arrays.asList(user,user));}}需要在配置类上添加@EnableAspectJAutoProxy(exposeProxy = true)。
解决方案三:拆分类
把batchSave移到另一个 Bean 中,通过注入调用。
六、JDK 动态代理的局限
| 局限 | 说明 | 解决方案 |
|---|---|---|
| 必须实现接口 | 目标类没有接口时无法使用 | 用 CGLIB |
| 只能代理接口方法 | 实现类中额外的方法无法代理 | 用 CGLIB |
| 性能开销 | 反射调用比直接调用慢 | 用 CGLIB 或缓存 Method |
| 内部调用失效 | this 调用不经过代理 | 注入自身代理或 AopContext |
| 构造方法不执行 | 代理类不调用目标类构造方法 | 无影响,目标对象已创建 |
七、Spring 中事务代理的完整链路
Spring 的@Transactional底层由TransactionInterceptor实现,它本质上就是一个MethodInterceptor(CGLIB)或配合InvocationHandler(JDK)。
调用 @Transactional 方法 → 代理对象拦截 → TransactionInterceptor.invoke() → 获取 @Transactional 属性(传播行为、隔离级别、超时、只读) → 创建/加入事务 → 执行目标方法 → 成功 → commit → 异常 → rollback(根据 rollbackFor 判断)核心类:
| 类 | 作用 |
|---|---|
TransactionInterceptor | 事务拦截器,织入事务逻辑 |
PlatformTransactionManager | 事务管理器,开启/提交/回滚 |
TransactionAttributeSource | 解析@Transactional注解属性 |
TransactionInfo | 保存当前事务状态 |
八、总结
| 维度 | 核心要点 |
|---|---|
| 核心机制 | Proxy.newProxyInstance()+InvocationHandler |
| 织入逻辑 | 在invoke()中前后插入事务开启、提交、回滚 |
| 事务管理器 | 负责 begin、commit、rollback |
| 异常处理 | 回滚后抛出原始异常 |
| 内部调用 | 不经过代理,事务失效 |
| Spring 对应 | TransactionInterceptor+PlatformTransactionManager |
| 代理选择 | 有接口用 JDK,无接口用 CGLIB |
JDK 动态代理织入事务的本质是:在InvocationHandler.invoke()中,围绕method.invoke(target, args)包裹事务的开启、提交和回滚逻辑。Spring 的@Transactional就是把这段逻辑封装在了TransactionInterceptor中,通过动态代理自动应用到所有需要事务的方法上。理解这个手动实现,就能理解 Spring 事务代理的底层原理,也能解释为什么内部调用会导致事务失效。