
簡稱: test=a,test2=b
此時,調用a方法,b里的事務將不生效
這個問題,表面上是事務聲明失效的問題,實質上很可能是Spring的AOP機制實現角度的問題。我想到很久以前研究Spring的AOP實現時發現的一個現象:對於以Cglib方式增強的AOP目標類,會創建兩個對象,一個事Bean實例本身,一個是Cglib增強代理對象,而不僅僅是只有后者。我曾經疑惑過這一點,但當時沒有再仔細探究下去。
我們知道,Spring的AOP實現方式有兩種:1、Java代理方式;2、Cglib動態增強方式,這兩種方式在Spring中是可以無縫自由切換的。Java代理方式的優點是不依賴第三方jar包,缺點是不能代理類,只能代理接口。
Spring通過AopProxy接口,抽象了這兩種實現,實現了一致的AOP方式:
因此,從上面的分析可以看出,methodB沒有被AopProxy通知到,導致最終結果是:被Spring的AOP增強的類,在同一個類的內部方法調用時,其被調用方法上的增強通知將不起作用。
而這種結果,會造成什么影響呢:
1:內部調用時,被調用方法的事務聲明將不起作用
2:換句話說,你在某個方法上聲明它需要事務的時候,如果這個類還有其他開發者,你將不能保證這個方法真的會在事務環境中
3:再換句話說,Spring的事務傳播策略在內部方法調用時將不起作用。不管你希望某個方法需要單獨事務,是RequiresNew,還是要嵌套事務,要Nested,等等,統統不起作用。
4:不僅僅是事務通知,所有你自己利用Spring實現的AOP通知,都會受到同樣限制。。。。
解決辦法:
spring同類調用事務不生效-原因及三種解決方式
spring提供的聲明式事務注解@Transactional,極大的方便了開發者管理事務,無需手動編寫開啟、提交、回滾事務的代碼。
但是也帶來了一些隱患,如果注解使用不當,可能導致事務不生效,最終導致臟數據也入庫。
如果在同一個類直接調用事務方法,就會導致事務不生效,示例如下
public class StudentServiceImpl implements StudentService { @Autowired private StudentMapper studentMapper; @Override public void insertStudent(){ insert(); } @Transactional(rollbackFor = Exception.class) public void insert() { StudentDO studentDO = new StudentDO(); studentDO.setName("小民"); studentDO.setAge(22); studentMapper.insert(studentDO); if (studentDO.getAge() > 18) { throw new RuntimeException("年齡不能大於18歲"); } } }
事務不生效的原因在於,spring基於AOP機制實現事務的管理,@Authwired StudentService studentService這樣的方式,調用StudentService的方法時,實際上是通過StudentService的代理類調用StudentService的方法,代理類在執行目標方法前后,加上了事務管理的代碼。
因此,只有通過注入的StudentService調用事務方法,才會走代理類,才會執行事務管理;如果在同類直接調用,沒走代理類,事務就無效。
注意:除了@Transactional,@Async同樣需要代理類調用,異步才會生效
但是在實際的業務場景中,同類調用事務方法難以避免,怎么讓同類調用時事務依然生效呢?有以下三個方法
方法一
自己@Autowired自己,示例如下
@Service public class StudentServiceImpl implements StudentService { @Autowired private StudentMapper studentMapper; @Autowired private StudentService studentService; @Override public void insertStudent(){ studentService.insert(); } @Override @Transactional(rollbackFor = Exception.class) public void insert() { StudentDO studentDO = new StudentDO(); studentDO.setName("小民"); studentDO.setAge(22); studentMapper.insert(studentDO); if (studentDO.getAge() > 18) { throw new RuntimeException("年齡不能大於18歲"); } } }
可能有人會擔心這樣會有循環依賴的問題,事實上,spring通過三級緩存解決了循環依賴的問題,所以上面的寫法不會有循環依賴問題。
但是!!!,這不代表spring永遠沒有循環依賴的問題(@Async導致循環依賴了解下)
方法二
使用AopContext獲取到當前代理類,需要在啟動類加上@EnableAspectJAutoProxy(exposeProxy = true),
示例如下
@Service public class StudentServiceImpl implements StudentService { @Autowired private StudentMapper studentMapper; @Override public void insertStudent(){ getService().insert(); } @Override @Transactional(rollbackFor = Exception.class) public void insert() { StudentDO studentDO = new StudentDO(); studentDO.setName("小民"); studentDO.setAge(22); studentMapper.insert(studentDO); if (studentDO.getAge() > 18) { throw new RuntimeException("年齡不能大於18歲"); } } /** * 通過AopContext獲取代理類 * @return StudentService代理類 */ private StudentService getService(){ return Objects.nonNull(AopContext.currentProxy()) ? (StudentService)AopContext.currentProxy() : this; } }
exposeProxy = true用於控制AOP框架公開代理,公開后才可以通過AopContext獲取到當前代理類。(默認情況下不會公開代理,因為會降低性能)
注意:不能保證這種方式一定有效,使用@Async時,本方式可能失效。
(從@Async案例找到Spring框架的bug:exposeProxy=true不生效原因大剖析)
方式三(推薦)
通過spring上下文獲取到當前代理類,示例如下
@Component public class SpringBeanUtil implements ApplicationContextAware { private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } /** * 通過class獲取Bean * @param clazz class * @param <T> 泛型 * @return bean */ public static <T> T getBean(Class<T> clazz) { return applicationContext.getBean(clazz); } }
@Service public class StudentServiceImpl implements StudentService { @Autowired private StudentMapper studentMapper; @Override public void insertStudent(){ StudentService bean = SpringBeanUtil.getBean(StudentService.class); if (null != bean) { bean.insert(); } } @Override @Transactional(rollbackFor = Exception.class) public void insert() { StudentDO studentDO = new StudentDO(); studentDO.setName("小民"); studentDO.setAge(22); studentMapper.insert(studentDO); if (studentDO.getAge() > 18) { throw new RuntimeException("年齡不能大於18歲"); } } }
當一個類實現ApplicationContextAware接口后,就可以方便的獲得ApplicationContext中的所有bean。
原文出處:https://www.jianshu.com/p/083605986c8f
來源:簡書
