原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9476550.html
一、提出问题
不知道你是否遇到过这样的情况,在ssm框架中开发web引用,或者使用springboot开发应用,当我们调用一个带有@Transactional注解的方法执行某项事务操作的时候,有时候会发现事务是不生效的。
你是否考虑过这是为什么,又该如何来修复事务呢?
二、分析问题
要想弄明白事务不生效的原因,我们首先要弄明白Spring中事务的实现原理,而Spring中的声明式事务是使用AOP来实现的。
Spring中AOP又是依靠什么实现的呢?动态代理,在Spring中使用的两种动态代理,一种是java原生提供的JDK动态代理,另一种是第三方提供的CGLIB动态代理,前者基于接口实现,后者基于类实现,明显后者的适用范围更加广泛,但是原生的JDK动态代理却是速度要快很多,两者各有特色。
动态代理的目的就是在应用运行时实时生成代理类,这样我们就能在已有实现的基础上对其进行增强,这其实也就是AOP的目的所在,增强类的功能。
动态代理生成的代理类拥有原生类的所有公有方法,针对指定方法的调用会转移到代理类的同名方法之上,而在这个方法之内会在调用原生类的同名方法之外进行一些其他的操作,比如日志记录,比如安全检查,比如事务操作等。
当我们在Controller层直接调用service层的一个带有事务注解的方法时,就会执行以上步骤:生成代理类,调用代理类的同名方法,由代理类实现事务功能,再调用原生类的方法进行逻辑执行。
上面这种情况是没有问题的,有问题的是我们在service层内部的方法调用本类中的带有事务注解的方法时,该事务注解将失效,我们的调用方式无非就是直接调用或者用this调用,这两种情况效果其实是一样的,都是用当前实例调用。
结合之前的AOP和动态代理的介绍,我们很容易就能理解这里事务失效的原因:那就是我们调用目标事务方法的时候直接调用的原生的方法,而没有调用代理类中的代理方法,也就是说,我们没有调用进行了事务增强的方法,如此一来事务当然会失效了。
这么来说,我们需要调用代理类中增强之后的代理方法,才能使事务生效。
三、解决问题
那么我们要如何来修复呢?其实很简单,只要我们不使用this调用即可。this代表的是当前实例,在spring中一般就是单例实例,自己调用自己的方法,事务注解等于摆设。如果我们更改调用方式,在当前类中注入自身单例实例,使用注入的实例来调用该方法,即可使事务生效。
为什么呢?一般我们的SSM架构中的Service层都是有接口和实现类的,既然存在接口,那么这里使用的必然是JDK动态代理来生成代理类。当我们将当前类的单例实例注入到自身之后,使用这个注入的实例来调用接口中的方法时,如果存在@Transactional之类的AOP增强注解存在,那么就是生成代理类来实现功能增强。(在Springboot中开发的时候我们习惯去掉接口开发,那么代理类就是使用CGLIB动态代理生成的)。
这样也就要求我们的事务方法需要先在接口中声明,然后在实现类中实现逻辑,并添加事务注解。
这种方式适用于解决在Service中调用Service中的事务方法时事务失效的问题。这么想想之前从Controller调用Service的时候也是通过注入的Service单例实例来调用的,这也侧面证明我们提供的方法时有效的。
四、问题引申
4.1 引申问题:循环依赖
至于由此引发的另一个问题:当我们在当前类中注入当前类的实例后,在创建这个类的实例的时候是需要注入这个类的实例的,但是这时候这个类有没有创建完成,这该怎么办呢???
这就是Spring中著名的循环依赖问题。
更明显的样例是在A中依赖B,B中又依赖A的情况,依赖相互彼此,那么会不会导致两个实例都创建失败呢?
4.2 循环依赖的解决方案
有必要简单说下Spring中针对这个问题的解决方案。为什么是简单介绍呢,因为我也只是简单理解,但是这种简单理解更加适用于不明白的朋友,不至于一来就懵逼。
我们都知道在Spring中Bean有多种生命周期范围,主要就是单例和原型(当然还有request、Session等范围),单例表示在整个应用上下文中只会存在一个Bean实例,而原型正好相反,可以存在多个Bean实例,每次调用getBean的时候都会新建一个新的bean实例。
我们要强调,在Spring中原型范围的Bean实例如果发生循环依赖,只有一种下场:抛异常。
而针对单例bean,Spring内部提供了一种有效的提前暴露的机制解决了循环依赖的问题。当然这里仅仅解决的是使用setter方式实现依赖注入的情况,如果是使用构造器依赖注入的情况还是那种下场:抛异常。
抛异常代表,Spring无能力解决此问题,程序出错。
为什么呢?难道Spring不想解决吗?肯定不是,而是无能为力罢了。
我们先简单了解下setter方式实现依赖注入的单例Bean的循环依赖的解决方法:
先介绍下Spring中的那几个缓存池:
singletonObjects:单例缓存池,用于保存创建完成的单例Bean,是Map,凡是创建完毕的Bean实例全部保存在该缓存池中,不存在循环依赖的Bean会直接在创建完之后保存到该缓存中,而存在循环依赖的bean则会在其创建完成后由earlySingletonObjects转移到此缓存中。
singletonFactories:单例工厂缓存池,用于保存提前暴露的ObjectFactory,是Map
earlySingletonObjects:早期单例缓存池,用于保存尚未创建完成的用于早期暴露的单例Bean,是Map,它与singletonObjects是互斥的,就是不可能同时保存于两者之中,只能择一而存,保存在该缓存池中的是尚未完成创建,而被注入到其他Bean中的Bean实例,可以说该缓存就是一个中间缓存(或者叫过程缓存),只在当将该BeanName对应的原生Bean(处于创建中池)注入到另一个bean实例中后,将其添加到该缓存中,这个缓存中保存的永远是半成品的bean实例,当Bean实例最终完成创建后会从此缓存中移除,转移到singletonObjects缓存中保存。
registeredSingletons:已注册的单例缓存池,用于保存已完成创建的Bean实例的beanName,是Set(此缓存未涉及)
singletonsCurrentlyInCreation:创建中池,保存处于创建中的单例bean的BeanName,是Set,在这个bean实例开始创建时添加到池中,而来Bean实例创建完成之后从池中移除。
当存在循环依赖的情况时,比如之前的情况:A依赖B,B又依赖A的情况,这种情况下,首先要创建A实例,将其beanName添加到singletonsCurrentlyInCreation池,然后调用A的构造器创建A的原生实例,并将其ObjectFactory添加到singletonFactories缓存中,然后处理依赖注入(B实例),发现B实例不存在且也不在singletonsCurrentlyInCreation池中,表示Bean实例尚未进行创建,那么下一步开始创建B实例,将其beanName添加到singletonsCurrentlyInCreation池,然后调用B的构造器创建A的原生实例,并将其ObjectFactory添加到singletonFactories缓存中,再然后处理依赖注入(A实例),发现A实例尚未创建完成,但在singletonsCurrentlyInCreation池中发现了A实例的beanName,说明A实例正处于创建中,这时表示出现循环依赖,Spring会将singletonFactories缓存中获取对应A的beanName的ObjectFactory中getObject方法返回的Bean实例注入到B中,来完成B实例的创建步骤,同时也会将A的Bean实例添加到earlySingletonObjects缓存中,表示A实例是一个提前暴露的Bean实例,B实例创建完毕之后需要将B的原生实例从singletonFactories缓存中移除,并将完整实例添加到SingletonObjects缓存中(当然earlySingletonObjects中也不能存在),并且将其beanName从singletonsCurrentlyInCreation池中移除(表示B实例完全创建完毕)。然后将B实例注入到A实例中来完成A实例的创建,最后同样将A的原生实例从earlySingletonObjects中移除,完整实例添加到SingletonObjects中,并将A的beanName从创建中池中移除。到此完成A和B两个单例实例的创建。
了解了上面所述的解决方案之后,我们可以明白针对构造器实现依赖注入的Bean发生循环依赖的情况下为什么无法解决。那就是因为,之前提前暴露的前提是创建好原生的Bean实例,原声的Bean实例就是依靠构造器创建的,如果在构造器创建Bean的时候就需要注入依赖,而依赖又正处于创建中的话,由于无法暴露ObjectFactory,而无法解决循环依赖问题。
另外原型bean的情况,Spring根本就不会对原型的Bean添加缓存,因为添加缓存的目的是为了保证单例Bean的唯一性,但是对于原型,就不能缓存了,如果从缓存获取的Bean实例,那还是原型模式吗?不存在缓存当然也就无法实现上面描述的那一系列操作,也就无法解决循环依赖的问题了。
五、总结
Spring中的事务问题归结为注入问题,循环依赖问题也是注入问题,有关注入的问题以后再讨论。
Spring之中所有的增强都是依靠AOP实现的,而AOP又是依靠动态代理实现的,JDK的动态代理依靠反射技术实现,而CGLIB动态代理依靠字节码技术实现。