【深入学习Spring声明式事务,测试失效场景及原因分析】

一.Spring声明式事务是什么?

Spring声明式事务是一种通过配置的方式管理事务的方法,它通过注解或XML配置来声明哪些方法需要事务管理,从而将事务管理逻辑与业务逻辑分离,简化了代码的复杂度,提高了代码的可读性和可维护性。‌

声明式事务的核心在于通过AOP(面向切面编程)技术实现事务管理的自动化。开发者只需在需要事务管理的方法上添加@Transactional注解,Spring框架会自动在方法执行前后进行事务的开启和关闭,以及在出现异常时进行事务的回滚。这种方式使得开发者可以专注于业务逻辑的实现,而不需要编写繁琐的事务管理代码‌。

二.Spring事务失效场景

在这里插入图片描述

1.访问权限问题

我们知道 @Transactional 注解依赖于Spring AOP来增强事务行为,AOP 是通过动态代理来实现的,而 private 方法、procted修饰的方法由于访问权限的限制是不能被代理的,所以使用@Transactional也就不会生效,只有public方法才能被代理对象拦截,事务才会生效。

FAQ:这就跟咱学习java基础的时候访问修饰符有关了,我们仔细想想,Spring是通过CglibAopProxy或JdkDynamicAopProxy创建代理对象来增强目标方法的(代理类与我们的目标类不在同一类、同一个包),那代理对象来访问目标对象的私有方法、protected、default(无修饰符)方法,这样可以吗?
答:都是不可以的哈。本质是由访问修饰符来决定能不能操作目标对象的方法的,具体原因需要了解下我们的访问修饰符作用范围。

CGLIB动态代理的工作原理是通过继承目标类并重写其方法来实现增强的,创建被代理类的子类,重写所有非final的方法,并在方法调用时通过MethodInterceptor拦截器进行增强逻辑的处理。由于static方法不属于任何对象实例,它们是在类级别上定义的,因此无法通过代理类来重写或增强。此外,CGLIB动态代理底层使用ASM框架生成目标类的子类,并在运行时动态插入额外的逻辑,但由于static方法的特性,这些逻辑无法应用到static方法上‌。

这里我自行使用自己的CglibPrpxy动态代理(非Spring的CglibAopProxy)来做的测试(与目标类Tatget在同一包下,得出如下结论:

  • 代理目标对象的私有方法,由于访问权限的问题,编译期都会报错哈,怎么都增强不了。
  • 代理目标对象的protected方法,如果我们的代理类与目标类在同一个包下可以增强
  • 代理目标对象的default(无修饰符)方法,如果代理类与目标类在同一包下是能够增强的
  1. 目标类Target与代理类CglibProxy 在同一个包下测试:
package com.jinbiao.javaStudy.modifier;
public class Target {
   
   

   private void test1(){
   
   
        System.out.println("目标对象的私有方法执行了..");
    }
    
    protected void test2(){
   
   
        System.out.println("目标对象的保护方法执行了..");
    }

    void test3(){
   
   
        System.out.println("目标对象的默认方法执行了..");
    }

    public final void test4(){
   
   
        System.out.println("目标对象的final方法执行了..");
    }

    public static void test5(){
   
   
        System.out.println("目标类的static方法执行了..");
    }
    public void test6(){
   
   
        System.out.println("目标对象的public方法执行了..");
    }
}

package com.jinbiao.javaStudy.modifier;
public class CglibProxy implements MethodInterceptor{
   
   

    //需要代理的目标对象
    private Object target;

    //重写拦截方法--
    @Override
    public Object intercept(Object obj, Method method, Object[] arr, MethodProxy proxy) throws Throwable {
   
   
        System.out.println("Cglib动态代理,监听开始!");
        Object invoke = method.invoke(target, arr);
        System.out.println("Cglib动态代理,监听结束!");
        return invoke;
    }

    //定义获取代理对象方法
    public Object getCglibProxy(Object objectTarget){
   
   
         //为目标对象target赋值
        this.target=objectTarget;
        Enhancer enhancer = new Enhancer();
        //设置父类,因为Cglib是针对指定的类生成一个子类,所以需要指定父类
        enhancer.setSuperclass(objectTarget.getClass());
        // 设置回调
        enhancer.setCallback(this);
        //创建并返回代理对象
        return enhancer.create();
    }
    public static void main(String[] args) {
   
   
      	//实例化CglibProxy对象
        CglibProxy cglib=new CglibProxy();
        Target targetProxy = (Target) cglib.getCglibProxy(new Target());

        // 代理目标对象的private方法,程序报错,提示:java: test5() 在 com.jinbiao.javaStudy.modifier.Target 中是 private 访问控制
        // targetProxy.test1();

        // 代理目标对象的protected方法
        targetProxy.test2();

        // 代理目标对象的default方法(无访问修饰符修饰)
        targetProxy.test3();

        // 代理目标对象的final方法
        targetProxy.test4();

        // 代理目标对象的static方法
        targetProxy.test5();

        // 代理目标对象的public方法
        targetProxy.test6();
    }
}

测试结果:
代理类与目标类在同一包下,使用cglib动态代理对目标对象的protected方法、default方法还是能够增强的,public方法是100%可以增强。对private方法、final方法、static方法是完全无法增强的。
在这里插入图片描述

2.目标类Target与代理类CglibProxy2在不同包下测试:目标类Target在modifier下,把代理类CglibProxy2挪动到modifier2下。

 public static void main(String[] args) {
   
   
        //实例化CglibProxy对象
        CglibProxy2 cglib=new CglibProxy2();
        /** 在同一包:父类的代理对象可以访问到父类的protected方法的,把CglibProxy类换到其他包目录就访问不了了*/
        Target targetProxy = (Target) cglib.getCglibProxy(new Target());

        // 代理目标对象的private方法,程序报错,提示:java: test5() 在 com.jinbiao.javaStudy.modifier.Target 中是 private 访问控制
        // targetProxy.test1();

        log.info("目标对象的代理对象targetProxy 是否是 目标对象Target的子类?{}",targetProxy instanceof Target);

        // 代理目标对象的protected方法,程序报错,提示:java: test2() 在 com.jinbiao.javaStudy.modifier.Target 中是 protected 访问控制
        // targetProxy.test2();

        // 代理目标对象的default方法(无访问修饰符修饰),程序报错,提示:java: test3()在com.jinbiao.javaStudy.modifier.Target中不是公共的; 无法从外部程序包中对其进行访问
        // targetProxy.test3();

        // 代理目标对象的final方法
        targetProxy.test4();
        // 代理目标对象的static方法
        targetProxy.test5();
        // 代理目标对象的public方法
        targetProxy.test6();
    }

在这里插入图片描述
测试结果:在不同包下,targetProxy代理对象因为访问权限问题,无法访问目标对象的private、protected、default方法,对目标对象的static、final方法能访问无法增强。仅仅只有public方法可以增强。

综上测试场景总结几点:

  1. 因为Spring的CglibAopProxy或JdkDynamicAopProxy代理类是在aop的源码包下,与我们的目标对象(容器里面的Bean对象)不可能在同一个包下,所以Spring事物针对目标对象的任何private、protected、deafult(没访问修饰符修饰)的方法,因为访问权限问题都是无法进行增强的,访问不到就做不了增强
  2. 针对目标类的static方法也无法增强,static方法是属于类所有,不属于任何对象实例,因此无法通过代理类来重写或增强。
  3. 针对目标对象的final方法也无法增强,原因:CGLIB动态代理的工作原理是通过继承目标类并重写其方法来实现增强的,创建被代理类的子类,重写所有非final的方法,并在方法调用时通过MethodInterceptor拦截器进行增强逻辑的处理,子类重写不了父类的final方法,所以就重写不了,所以增强不了。

然后看下四种修饰符的作用域范围吧:
在这里插入图片描述

关于jdk动态代理和cglib动态代理:
代理能干嘛?代理可以帮我们增强对象的行为!使用动态代理实质上就是调用时拦截对象方法,对方法进行改造、增强!Spring AOP的底层原理就是动态代理!
1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP
3、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换

2.方法使用static/final修饰

static 静态方法/静态属性属于类所有,Spring是管理bean对象的map容器,它进行依赖注入的的时候是不支持静态属性的。
源码在:AutowiredAnnotationBeanPostProcessor => buildAutowiringMetadata
在这里插入图片描述

接下来开始测试下事物方法用static修饰的场景吧~
我们在前面文章中了解过Spring编程式事务,看过Spring声明式事务源码的小伙伴们应该也清楚Spring声明式事务底层是借助于AOP+编程式事务去做的。我们知道Mybatis/JdbcTempalte是基于数据源的方式,其编程式事务依赖于PlatformTransactionManager接口的实现DataSourceTransactionManager事务管理器进行事务管理(开启、提交、回滚)的。
所以我们测试Spring声明式事务前提条件:需要往Spring容器中注入事务管理器所需的相关bean信息。

//在Spring开启声明式事务支持时,启动类需要加@EnableTransactionManagement注解。‌ 
//这个注解告诉Spring容器要启用基于注解的事务管理功能,否则事务不生效
@EnableTransactionManagement
@ComponentScan({
   
   "com.jinbiao.spring_study.transactionTest"})
@Configuration
public class JDBCConfig {
   
   
    @Bean
    public DataSource dataSource(){
   
   
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
        druidDataSource.setUrl("jdbc:mysql://localhost:3306/study_test");
        druidDataSource.setUsername("root");
        druidDataSource.setPassword("123456");
        return druidDataSource;
    }

    /**
     * 如果使用到mybatis需要给sqlSessionFactoryBean注入数据源信息
     * @param dataSource
     * @return
     */
    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){
   
   
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值