原创作品,可以转载,但是请标注出处地址: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单例实例来调用的,这也侧面证明我们提供的方法时有效的。

  还有几种解决方案:

    一种就是Spring基础系列-AOP源码分析中的源码6里面所说的通过暴露AOP代理的方式实现。

    一种是将事务注解添加到类上。

    再一种就是就是将被调用的事务方法,放到另一个类中再进行调用。

    这里再添加一种方法:使当前类实现BeanFactoryAware接口,并实现setBeanFactory方法,添加BeanFactory字段,然后通过beanFactory的getBean方法获取当前类的Bean实例来调用目标事务方法,即可实现嵌套之类的事务调用。

四、问题引申

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动态代理依靠字节码技术实现。

Spring基础系列-Spring事务不生效的问题与循环依赖问题的更多相关文章

  1. Spring基础系列-AOP源码分析

    原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9560803.html 一.概述 Spring的两大特性:IOC和AOP. AOP是面向切 ...

  2. spring中注解式事务不生效的问题

    常用的解决方法可以百度,我针对我的问题描述一下 Mysql中InnoDB引擎才支持事务, MyISAM不支持事务. 当你尝试了各种方法解决spring中注解式事务不生效时, 一定要查看一下数据库中表的 ...

  3. Spring基础系列--AOP织入逻辑跟踪

    原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9619910.html 其实在之前的源码解读里面,关于织入的部分并没有说清楚,那些前置.后 ...

  4. Spring基础系列-Web开发

    原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9996902.html SpringBoot基础系列-web开发 概述 web开发就是集成 ...

  5. Spring框架系列(六)--事务Transaction

    本文绝大部分内容为转载,原文地址:https://blog.csdn.net/trigl/article/details/50968079 除此之外,后面还有延伸内容 事务在企业日常开发中几乎是一定会 ...

  6. 【spring基础】spring声明式事务详解

    一.spring声明式事务 1.1 spring的事务管理器 spring没有直接管理事务,而是将管理事务的责任委托给JTA或相应的持久性机制所提供的某个特定平台的事务实现.spring容器负责事物的 ...

  7. Spring Boot系列——Spring Boot如何启动

    Spring Boot启动过程 ​上篇<Spring Boot系列--5分钟构建一个应用>介绍了如何快速创建一个Spring Boot项目并运行.虽然步骤少流程简单,为开发者省去了很多重复 ...

  8. Spring基础系列-参数校验

    原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9953744.html Spring中使用参数校验 概述 ​ JSR 303中提出了Bea ...

  9. Spring基础系列-容器启动流程(1)

    原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9870339.html 概述 ​ 我说的容器启动流程涉及两种情况,SSM开发模式和Spri ...

随机推荐

  1. 阿里云 ECS centos java timer进程异常/混乱......的解决方法

    之前就知道timer进程长久运行容易出问题,所以一直对timer进行了很长一段时间的日志监控和数据库记录,大概观察了几个月,没发现过问题....然后就没管理了,数据库记录也没做了,昨天这问题就来了,t ...

  2. mycat 主从切换分析过程

    67 68互为主从 66为67从 区分双主写的数据,设置不同的自增id 67: SET @@auto_increment_offset=2;SET @@auto_increment_increment ...

  3. [转]找到MySQL发生swap的原因

    背景: 最近遇到了一个郁闷的问题:明明OS还有大量的空闲内存,可是却发生了SWAP,百思不得其解.先看下SWAP是干嘛的,了解下它的背景知识.在Linux下,SWAP的作用类似Windows系统下的“ ...

  4. OC协议、代理的简单使用

    在不同类之间传递数据,我所学到的有三种,1.代理,2.block,3.通知.在这里,我们先来讲一下代理的使用,后面我会继续讲到block和通知.代理通常和协议是一起使用的,协议通常写在代理类里面,被代 ...

  5. ICO图标下载地址

    http://findicons.com/ http://www.iconfont.cn/

  6. Linux下Memcache服务器端的安装

    最近在研究怎么让Discuz!去应用Memcache去做一些事情,记录下Memcache安装的过程. Linux下Memcache服务器端的安装服务器端主要是安装memcache服务器端,目前的最新版 ...

  7. Android-Could not download kotlin-reflect.jar

    在AndroidStudio中报以下错误: 错误详情: Could not download kotlin-reflect.jar (org.jetbrains.kotlin:kotlin-refle ...

  8. c++ 实现hashmap

    由于hashmap不是c++ stl中标准实现,这样在跨平台使用时就可能会出现问题,于是想到自己实现一个hashmap hash算法使用开链法解决hash冲突,主要实现了添加,删除,查找几个方法 头文 ...

  9. 背水一战 Windows 10 (119) - 后台任务: 后台下载任务(任务分组,组完成后触发后台任务)

    [源码下载] 背水一战 Windows 10 (119) - 后台任务: 后台下载任务(任务分组,组完成后触发后台任务) 作者:webabcd 介绍背水一战 Windows 10 之 后台任务 后台下 ...

  10. Docker应用:Kubernetes(容器集群)

    阅读目录: Docker应用:Hello World Docker应用:Docker-compose(容器编排) Docker应用:Kubernetes(容器集群) 前言: 终于出第三篇了,上个月就已 ...