文章目录
一.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(无修饰符)方法,如果代理类与目标类在同一包下是能够增强的。
- 目标类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方法可以增强。
综上测试场景总结几点:
- 因为Spring的CglibAopProxy或JdkDynamicAopProxy代理类是在aop的源码包下,与我们的目标对象(容器里面的Bean对象)不可能在同一个包下,所以Spring事物针对目标对象的任何private、protected、deafult(没访问修饰符修饰)的方法,因为访问权限问题都是无法进行增强的,访问不到就做不了增强
- 针对目标类的static方法也无法增强,static方法是属于类所有,不属于任何对象实例,因此无法通过代理类来重写或增强。
- 针对目标对象的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