让Spring事务支持同一个类的内部调用

November 9, 2020

问题:当同一个类中的方法A调用方法B时,即使两个方法都打上了@Transactional注解,方法B的事务也不会生效。

原因:默认情况下Spring事务是基于代理的,也就意味着获取到的service对象是代理后的对象(class com.sun.proxy.$Proxy,基于接口的情况)。当外部调用该对象上的方法时,经过aop加上的事务逻辑后,最终会进入到目标对象(即原始的service对象)的方法逻辑,此时在方法内部再调用自己的另一个方法B,本质上就是在原始对象上进行调用,此时自然而然不会牵扯到任何aop的事务逻辑。

解决办法:由上而知,采用代理的方式肯定行不通,故而必须采用字节码提升的方式,直接修改类的字节码,为每个打上@Transactional注解的方法都加上事务逻辑。具体分两步走:

  1. 设置 @EnableTransactionManagement(mode = AdviceMode.ASPECTJ) ,表示采用Aspectj 来对需要事务管理的方法进行字节码提升;

  2. 首先引入

    1
    2
    3
    4
    5
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>5.3.0</version>
    </dependency>
    

​ 加入spring对Aspectj的支持。

​ 而Aspectj weaving 的方式有两种:编译时和运行时。

​ 编译时即在编译期间就完成了对类的字节码的修改。可以通过加入一个maven插件实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  <build>
      <plugins>
          <plugin>
              <groupId>org.codehaus.mojo</groupId>
              <artifactId>aspectj-maven-plugin</artifactId>
              <version>1.6</version>
              <executions>
                  <execution>
                      <goals>
                          <goal>compile</goal>
                          <!-- <goal>test-compile</goal> -->
                      </goals>
                  </execution>
              </executions>
              <configuration>
                  <complianceLevel>1.8</complianceLevel>
                  <forceAjcCompile>true</forceAjcCompile>
                  <weaveDirectories>
                      <weaveDirectory>${project.build.outputDirectory}</weaveDirectory>
                  </weaveDirectories>
                  <aspectLibraries>
                      <aspectLibrary>
                          <groupId>org.springframework</groupId>
                          <artifactId>spring-aspects</artifactId>
                      </aspectLibrary>
                  </aspectLibraries>
              </configuration>
          </plugin>
      </plugin>
  </build>

运行时即在class被加入到ClassLoader之前动态的修改类的字节码。

具体用到了LoadTimeWeaving,详情参考

Foo类中方法A调用Bar类中的方法B是ok的,因为是在代理后的Bar的对象上进行操作,事务逻辑是存在的。但是要注意方法B的事务的传播级别,默认为REQUIRED,即如果存在了一个事务则直接使用,不存在才会创建一个新的事务。所以加入方法A的事务是READ-WRITE,方法B的事务是READ-ONLY,这时方法B会直接使用方法A的事务,会导致READ-ONLY失效。这时需要将方法B的传播级别改为REQUIRES_NEW。


建议: 强烈建议同一个类内部的事务方法之间不要进行相互调用,尽可能的重构此类代码,最好合并到一个事务方法中。这也是Spring官方推荐的。