为什么事务注解会失效?一文搞懂 AOP 代理原理与解决方案

一、问题的重现:事务失效的诡异现象

在前段时间的项目学习中,我遇见了一个问题,就是当我在一个方法中调用一个有事务注解的方法时,idea提示事务会失效。如下图,我对一个单个创建的方法加了回滚的事务,而后续批量插入调用了这个方法,此时出现了这个提示。

@Transactional 自调用(实际上是目标对象内)的方法调用目标对象的另一个方法/在运行时不会导致实际的事务

起初我并没有深究,直到近期的面试中,面试官问了我一个问题,当两个serviceA和serviceB方法位于同一个Service类中,serviceB定义了切面,然后serviceA调用serviceB,切面会生效吗?我当时回答不会,因为无法创建代理对象。

是的,@Transaction 注解就是通过AOP实现的,事后我恍然大悟,原来是AOP搞的鬼!

二、AOP:Spring框架的隐形功臣

那什么是AOP呢?AOP作为Spring的核心思想,与IoC和DI共同实现了Spring的高内聚、低耦合、轻量的特性,是Spring整个生态不可或缺的基石。AOP(Aspect-Oriented Programming)是一种编程范式,旨在解决横切关注点(cross-cutting concerns)的模块化问题。它允许开发者将影响多个类的公共行为(如日志、事务、安全等)从业务逻辑中分离出来。具有以下的核心思想:

  • 关注点分离:将系统分为核心关注点(业务逻辑)和横切关注点(通用功能)
  • 增强而非修改:通过动态织入方式为现有代码添加功能,而不是直接修改源代码

对于一个切面,我们会通过@Before、@After、@Around等注解定义切面织入的位置。

三、深入理解:AOP的实现原理------代理模式

要真正理解为什么事务注解会失效,我们需要深入AOP的实现机制。Spring AOP的核心就是代理模式。Spring不会直接调用我们的业务对象,而是创建一个代理对象,通过这个代理对象来间接调用。

代理对象的工作机制

typescript 复制代码
// 原始业务对象
public class UserServiceImpl implements UserService {
    
    @Transactional
    public void createUser() {
        // 业务逻辑
    }
    
    public void batchCreateUsers() {
        // 这里直接调用createUser() - 问题所在!
        for (int i = 0; i < 10; i++) {
            createUser(); // ❌ 事务不会生效,因为不是代理对象调用,没有实现切面的织入!
        }
    }
}

Spring AOP的两种动态代理方式

Spring AOP主要使用两种动态代理技术:JDK动态代理和CGLIB动态代理。

1. JDK动态代理

原理:基于接口实现,使用Java反射机制

typescript 复制代码
// 示例:JDK动态代理的实现原理
public class JdkDynamicProxyDemo {
    
    public static void main(String[] args) {
        // 目标对象
        UserService target = new UserServiceImpl();
        
        // 创建代理对象
        UserService proxy = (UserService) Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) 
                    throws Throwable {
                    
                    // 前置增强:开启事务
                    System.out.println("开启事务...");
                    
                    // 调用目标方法
                    Object result = method.invoke(target, args);
                    
                    // 后置增强:提交事务
                    System.out.println("提交事务...");
                    
                    return result;
                }
            }
        );
        
        // 通过代理对象调用方法
        proxy.createUser(); // ✅ 事务生效,代理对象织入了切面的逻辑
    }
}

JDK动态代理的特点:

  • 只能为接口创建代理
  • 要求目标类至少实现一个接口
  • 使用java.lang.reflect.Proxy
  • 性能相对较好(Java 8+优化后)

2. CGLIB动态代理

原理:基于类继承,使用字节码生成技术

typescript 复制代码
// 示例:CGLIB动态代理的实现原理
public class CglibProxyDemo {
    
    public static void main(String[] args) {
        // 创建Enhancer对象
        Enhancer enhancer = new Enhancer();
        
        // 设置父类(目标类)
        enhancer.setSuperclass(UserServiceImpl.class);
        
        // 设置回调
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object obj, Method method, 
                                    Object[] args, MethodProxy proxy) 
                throws Throwable {
                
                // 前置增强:开启事务
                System.out.println("CGLIB代理:开启事务...");
                
                // 调用父类方法
                Object result = proxy.invokeSuper(obj, args);
                
                // 后置增强:提交事务
                System.out.println("CGLIB代理:提交事务...");
                
                return result;
            }
        });
        
        // 创建代理对象
        UserService proxy = (UserService) enhancer.create();
        
        // 通过代理对象调用方法
        proxy.createUser(); // ✅ 事务生效,代理对象织入了切面的逻辑
    }
}

CGLIB动态代理的特点:

  • 可以为类创建代理(不需要接口)
  • 通过继承目标类实现代理
  • 使用ASM字节码操作框架
  • 不能代理final类和方法(无法继承或重写)
  • 性能略低于JDK动态代理(但差距不大)

四、揭开谜底:为什么事务注解会失效?

现在我们可以回答开头的问题了。事务失效的根本原因在于代理对象的调用机制

问题重现分析

typescript 复制代码
@Service
public class UserService {
    
    @Transactional
    public void createUser(User user) {
        // 数据库插入操作
        userDao.insert(user);
    }
    
    public void batchCreateUsers(List<User> users) {
        for (User user : users) {
            // 问题:这里调用的是this.createUser(),而不是代理对象的createUser()
            this.createUser(user);
            // 正确做法:应该调用代理对象的createUser()方法
        }
    }
}

图解调用流程

scss 复制代码
错误调用流程:
batchCreateUsers() → this.createUser() → 原始对象的createUser()
                            ↓
                     ❌ 切面没有织入,没有事务控制!

正确调用流程:
batchCreateUsers() → 代理对象.createUser() → 原始对象的createUser()
                            ↓
                    ✅ 切面织入,有事务控制(前置:开启事务,后置:提交事务)

Spring的选择:默认使用哪种代理?

Spring AOP根据以下规则选择代理方式:

  1. 如果目标对象实现了接口 → 默认使用JDK动态代理
  2. 如果目标对象没有实现接口 → 使用CGLIB动态代理
  3. 可以通过配置强制使用CGLIB:spring.aop.proxy-target-class=true

五、解决方案:如何避免事务失效

方案1:自我注入(推荐)

typescript 复制代码
@Service
public class UserService {

    //SpringBoot 2.x + 默认开启循环依赖支持,自我注入不会有问题;
    //若关闭循环依赖(spring.main.allow-circular-references=false),则此方案失效
    @Autowired
    private UserService self; // 注入自己
    
    @Transactional
    public void createUser(User user) {
        userDao.insert(user);
    }
    
    public void batchCreateUsers(List<User> users) {
        for (User user : users) {
            self.createUser(user); // 通过代理对象调用
        }
    }
}

方案2:从ApplicationContext获取代理对象

typescript 复制代码
@Service
public class UserService implements ApplicationContextAware {
    
    private ApplicationContext applicationContext;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }
    
    @Transactional
    public void createUser(User user) {
        userDao.insert(user);
    }
    
    public void batchCreateUsers(List<User> users) {
        UserService proxy = applicationContext.getBean(UserService.class);
        for (User user : users) {
            proxy.createUser(user);
        }
    }
}

方案3:使用AopContext获取当前代理对象

typescript 复制代码
@Service
public class UserService {
    
    @Transactional
    public void createUser(User user) {
        userDao.insert(user);
    }
    
    public void batchCreateUsers(List<User> users) {
        // 启用暴露代理(以下三种方法)
        // 在配置中设置:expose-proxy=true
        // 注解方式:@EnableAspectJAutoProxy(exposeProxy = true)
        // 配置文件方式(SpringBoot):spring.aop.expose-proxy=true
        UserService proxy = (UserService) AopContext.currentProxy();
        for (User user : users) {
            proxy.createUser(user);
        }
    }
}

方案4:重构代码结构

typescript 复制代码
// 将事务方法拆分到独立的Service中
@Service
public class UserTransactionService {
    
    @Transactional
    public void createUser(User user) {
        userDao.insert(user);
    }
}

@Service  
public class UserService {
    
    @Autowired
    private UserTransactionService userTransactionService;
    
    public void batchCreateUsers(List<User> users) {
        for (User user : users) {
            userTransactionService.createUser(user);
        }
    }
}

六、面试回答指南

针对面试官的问题:"当两个serviceA和serviceB方法位于同一个Service类中,serviceB定义了切面,然后serviceA调用serviceB,切面会生效吗?"

标准回答:

"不会生效。这是因为Spring AOP是基于代理实现的。当serviceA调用serviceB时,实际上是通过this.serviceB()这种方式直接调用,绕过了代理对象。代理对象只拦截外部调用,对于同一个类内部的相互调用,Spring无法通过代理进行增强。要解决这个问题,可以通过自我注入、从ApplicationContext获取代理对象、或者重构代码结构等方式来确保通过代理对象进行调用。"

七、总结

通过本文的分析,我们可以得出以下结论:

  1. 事务失效的本质:AOP代理机制导致的内部调用绕过代理对象
  2. Spring AOP的实现:基于JDK动态代理(接口)和CGLIB动态代理(类继承)
  3. 代理对象的作用:在目标方法执行前后插入增强逻辑(如事务控制)
  4. 解决方案:确保通过代理对象调用有切面的方法

理解AOP的代理机制不仅有助于解决事务失效的问题,还能帮助我们更好地设计系统架构,合理使用AOP的各种特性。记住:在Spring的世界里,有些时候"绕过自己"(通过代理调用)才能更好地完成工作!

相关推荐
q***73551 小时前
springboot与springcloud对应版本
java·spring boot·spring cloud
e***98571 小时前
Springboot的jak安装与配置教程
java·spring boot·后端
qq_2704900961 小时前
基于SSM的智能校内点餐系统设计与实现
java·eclipse·tomcat·mybatis
大道戏1 小时前
互联网程序设计第12 讲 RMI 程序设计
java·开发语言·计算机网络
路边草随风1 小时前
flink实现写orc对数据进行分目录(分区表)写入
java·大数据·flink
geekmice1 小时前
通过账户信息操作加深对DTO,VO,BO理解
java
r***01381 小时前
Java进阶,时间与日期,包装类,正则表达式
java·mysql·正则表达式
APIshop1 小时前
Java爬虫第三方平台获取1688关键词搜索接口实战教程
java·开发语言·爬虫
k***12171 小时前
SpringCloud实战【九】 SpringCloud服务间调用
java·spring boot·spring cloud