理解Spring事务
事情源于一个Required New事务的需求,可能写代码时都遇到过这样的场景:一个比较重要的操作,需要在数据库保存日志或者流水之类的信息。写之前老大特地叮嘱,Required New方法和业务方法不要放在同一个类中,不然Required New事务会不生效,我不信邪,就试了下,emmmmmm,脸疼。网上搜了下,发现类似的问题很多,如一个类的两个方法,如果直接用没有事务控制的方法调用有@Transactional声明事务的方法,会不生效。然后查了些资料,发现了原因,记录分享下。
References:
- https://spring.io/blog/2012/05/23/transactions-caching-and-aop-understanding-proxy-usage-in-spring 强烈推荐看一下,本文主要翻译自这篇文章
- https://github.com/spring-projects/spring-petclinic/issues/195 另一种优雅的解决方案
- https://stackoverflow.com/questions/3423972/spring-transaction-method-call-by-the-method-within-the-same-class-does-not-wo
- https://docs.spring.io/spring/docs/5.1.5.RELEASE/spring-framework-reference/data-access.html#transaction-declarative-annotations 官方doc
测试和介绍的部分省略,直接说原理:
在开启了注解的声明式事务后,就意味着所有标记有@Transactional的方法应该在启动时被扫描并且变为有事务的方法。那么事务行为发生在什么地方呢?
举个栗子:
@Service
public class AccountServiceImpl implements AccountService {
@PersistenceContext
private EntityManager entityManager;
@Transactional
public void create(Account account) {
entityManager.persist(account);
}
}
在启动时,创建了一个新的代理类。代理类负责添加事务控制,如下所示:
生成的代理类是最重要的AccountService实现类,它添加了事务。
那么如何确信使用的是生成的代理而不是我们自己的实现类?有兴趣可以自己跑一下代码试试,这里简单的给出一个运行示例:
AccountService accountService = (AccountService) applicationContext.getBean(AccountService.class);
String accountServiceClassName = accountService.getClass().getName();
logger.info(accountServiceClassName);
输出如下:
INFO : transaction.TransactionProxyTest - $Proxy13
这个类是一个动态代理的类,是Spring使用JDK的反射API生成的(可以了解下java.lang.reflect.Proxy)。
应用关闭后,代理类就会被销毁并且你的磁盘上依然只有AccountService和AccountServiceImpl。
那么Spring如何做到装配代理而不是我们的实现类呢?
先看下这么一段代码:
@Controller
public class AccountController {
private AccountService accountService;
private void setAccountService(AccountService accountService) {
this.accountService=accountService;
}
//…
}
accountService属性是AccountService(接口)类型。变量依赖的是接口类型的AccountService,而不是具体的实现类。这样可以减少类之间的依赖。(最佳实践)
如上所示,AccountServiceImpl和生成的代理类都实现了AccountService接口。
- 如果有代理,Spring会注入代理
- 如果没有,Spring注入AccountServiceImpl的实例
上面是bean实现了接口的情况,Spring会和bean实现相同的接口,那如果bean没有实现任何接口呢?
Spring也能代理没有接口的bean,因为在许多情况下,实现接口并不是好的实践。
默认情况下,如果bean没有实现接口,Spring会使用技术继承:在启动时,创建一个新类,它继承自你的bean并添加相应的行为。
为了生成这种代理类,Spring使用了一个叫做cglib的第三方库。(自行了解)
看下使用Java注解的方式:
@Configuration
public class JavaConfig {
@Bean
public AccountService accountService() {
return new AccountServiceImpl((accountRepository());
}
@Bean
public AccountRepository accountRepository() {
// …
}
}
每当需要注入“accountService”的bean实例时,Spring就会调用accountService()方法,然后方法会返回一个“new”出来的AccountService。如果有10个地方有这个依赖,这个方法就会被调用10次。
但是,不管Spring的配置是不是用注解的方式,每个bean都应该默认是单例。这是如何实现的呢?
下面这张图解释内部如何工作的:
图中可以看到,
- 如果scope是“singleton”,就检查是否有实例已经被创建了。
- 如果有,返回已经有的实例
- 如果没有,调用super.accountService()
所以是代理类处理了这些逻辑,将你的POJO转成了单例模式。
原理就是以上,所以现在看看问题就很清楚了,那么如何解决呢?
这里总结了三个方案:
- 放到一个新的类里(简单粗暴)
自己注入自己,比如:
public class UserServiceImpl { @Autowired private UserService self; }
上面的代码在4.3版本后可以使用,如果之前的版本需要注入ApplicationContext手动调用getBean()。但是这个方法有个缺陷,如果bean不是单例的可能会有意想不到的惊喜(bug>_<)。
- 代理方式换CGLIB(不知道就自行了解)
路漫漫其修远兮~~!