Spring中的@Transactional相信大家都已经用过不少,不知道大家有没有遇到过明明检查了三遍事务配置都是正确,但是它就是不生效,嘿,你说气不气。

翻车现场

  1. 我们模拟一下用户注册成功后调用第三方网关发送短信提醒的这么一个过程,主要代码只有两个方法,一是用户新增,还有一个是短信发送。都在其方法上方加上@Transactional事务注解,接着为了防止由于第三方短信网关的不稳定从而导致影响到用户注册的主要流程,我们在调用短信的地方捕获下异常:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* 用户服务
* 转载请注明出处,更多技术文章欢迎大家访问我的个人博客站点:https://www.doufuplus.com
*
* @author 丶doufu
* @date 2019/08/14
*/
@Service
public class UserServiceImpl implements UserService {

private static final Log logger = LogFactory.getLog(UserServiceImpl.class);

@Autowired
private UserMapper userMapper;

@Autowired
private MsgMapper msgMapper;

/**
* 用户注册
* 转载请注明出处,更多技术文章欢迎大家访问我的个人博客站点:https://www.doufuplus.com
*
* @author 丶doufu
* @date 2019/08/14
*/
@Override
@Transactional
public int register(User user) {
// ...
userMapper.insert(user);
// sendMsg(user);
try {
sendMsg(user);
} catch (Exception e) {
logger.info(e.getMessage());
}
// ...
return 0;
}

/**
* 模拟调用短信网关发送短信
* 转载请注明出处,更多技术文章欢迎大家访问我的个人博客站点:https://www.doufuplus.com
*
* @author 丶doufu
* @date 2019/08/14
*/
@Override
@Transactional
public void sendMsg(User user) {
Msg msg = new Msg(user.getId(), "欢迎来到豆腐别馆", new Date());
msgMapper.insert(msg);

// 调用短信网关
// ...
throw new RuntimeException("调用短信网关失败!");
}
}
  1. 看一下测试用例:
1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringRunner.class)
@SpringBootTest(classes = WebApplication.class)
public class TransactionDisableTest {

@Autowired
private UserService userService;

@Test
public void testTransactional() {
User user = new User("doufuplus", "123456", new Date());
userService.register(user);
}
}
  1. 我们可以猜测下代码的最终运行结果,sendMsg()方法由于加了@Transactional事务注解,且在方法内部已经抛出了RuntimeException,那么运行结果应该是:用户表成功插入数据,短信表由于发生异常产生回滚所以没有数据。只是事实真的是这样的吗?来看一看数据库:
    运行结果
    嗯哼?说好的要做彼此的天使,你却不管不顾插入了数据库~

事故原因

我们知道@Transactional是Spring基于aop机制实现的一个事务注解,而aop的本质实际上就是动态代理。在应用系统调用声明@Transactional 的目标方法时, aop动态代理会在代码运行时会生成一个代理对象(我们有时候在debug时看到的类似{$Proxy0}),再由这个代理对象来进行统一调用及增强。
而当我们在Service直接调用内部方法时,其本质是通过this对象来调用的方法,即上文中的sendMsg(user);事实上等价于this.sendMsg(user);,这时候的调用,它已经绕开了sendMsg()的代理对象,因此也就做不了代理对象的相关增强,事务也就自然而然地失效了。

解决办法

  1. (方法一)既然刚才的代码是通过this对象调用导致的失效,那么我们是不是也可以强制让它用回代理对象呢?当然是可以的,我们可以使用AopContext.currentProxy();获取当前类对象的动态代理进行显式调用:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /**
    * 用户注册
    * 转载请注明出处,更多技术文章欢迎大家访问我的个人博客站点:https://www.doufuplus.com
    *
    * @author 丶doufu
    * @date 2019/08/14
    */
    @Override
    @Transactional
    public int register(User user) {
    // ...
    userMapper.insert(user);
    // sendMsg(user);
    try {
    UserService currentProxy = (UserService) AopContext.currentProxy();
    currentProxy.sendMsg(user);
    } catch (Exception e) {
    logger.info(e.getMessage());
    }
    // ...
    return 0;
    }

如使用Spring框架,需在xml配置文件新增如下语句开启cglib代理,开启exposeProxy=true暴露代理对象:

<aop: aspectj-autoproxy expose-proxy="true"/>

  1. (方法二)同样的,当然也可以将sendMsg()方法放置于不同的类来避免this调用问题。

  2. 看看运行结果,可以看到已经达到了我们的预期效果:
    解决结果

后记

事实上,此种情况不单单只是在使用@Transactional事务注解的情况下会失效,所有以aop实现的注解都是有可能失效的。比如@Async异步注解,比如一些我们自定义的aop注解等等。
项目源码:GitHub (注意选择分支:transactional)