楔子
现在在Spring开发过程中使用声明式事务的次数要远远大于编程式事务,这一切都要归功于声明式事务让我们从复杂的事务处理中解脱出来。它会自动帮我们进行获取连接,关闭连接、事务提交、回滚、异常处理等操作。正因为这一切都是Spring自动帮我们完成的,所以我们也更容易掉入一些非常低级的陷阱中。
本文我们通过一个实际的例子来看一些声明式事务中的陷阱。
正文
首先让我们先来看一下以下代码都犯了哪些错误
@Service
public class DelayTradeService {
...
/**
* 将延时交易信息放入延时队列中
*/
public void putTradeInfoToQueue() {
// 从数据库中取出消息数据
this.takeOutTradeList(int size);
//TODO 将消息数据放到本地内存队列中
}
/**
* 采用阻塞的方式从数据库中读取消息数据
* @param size
* @return
*/
@Transactional(isolation = Isolation.READ_COMMITTED)
private List<DelayTradeInfo> takeOutTradeList(int size) {
//TODO 在数据库中采用for update的方式获取前size 数量的记录
}
}
大家可能首先看到的问题是事务方案的访问类型private
private List<DelayTradeInfo> takeOutTradeList(int size)
这里便是事务使用中常见的第一个问题:
1. Spring 要求使用@Transactional 注解的方法必须是public类型
这里的原因后面再说。在这段代码中其实还藏着另外一个坑,如果没有看出哪里存在问题,大家可以好好想想为什么Spring 要求声明事务的方法必须是public类型的。
这里略过5分钟…
这段代码中的第二个问题便是这里
this.takeOutTradeList(int size);
这里的写法是 putTradeInfoToQueue 方法通过内部调用的方式来调用 takeOutTradeList 方法。
大家都知道@Transactional的实现机制是通过Spring AOP来实现的,那么这第二个问题其实可以抽象为
2.Spring AOP 是不会拦截对象内部方法间的调用
为什么会这样呢?这就不得不得不重提AOP的实现逻辑。AOP本质上就是一种动态代理模式,简单来说就是通过InvocationHandler将待调用的目标对象注入到一个新的代理对象中(通过Proxy.newProxyInstance来实例化一个代理对象),然后调用代理对象中的方法(通过反射再来调用目标对象中的方法)来实现切面功能。所以AOP是否生效的关键在于是否可以将请求转到代理类的方法中。
那这里又来了两个问题“一个类的代理类是什么时候生成的?“、”又是谁将调用目标类中的方法转向了它代理类中的方法?“。
首先、Spring在使用ApplicationContext相关实现类加载bean的时候,会针对所有单例且非懒加载的bean,在构造ApplicationContext的时候就会创建好这些bean,而不会等到使用的时候才去创建。这也就是单例bean默认非懒加载的应用
Spring 在实例化bean之后会调用实现了BeanPostProcessor
接口中的postProcessAfterInitialization
方法来执行一些bean初始化之后的一些操作
那我们来看一些AOP包中的AnnotationAwareAspectJAutoProxyCreator类(该类实现了BeanPostProcessor)接口。
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (!this.earlyProxyReferences.contains(cacheKey)) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
// wrapIfNecessary
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (beanName != null && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
// 如果该类有advice则创建proxy,
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 1.通过方法名也能简单猜测到,这个方法就是把bean包装为proxy的主要方法,
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
// 2.返回该proxy代替原来的bean
return proxy;
}
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
至此就可以回答上面的两个问题
一个类的代理类是由Spring 在bean实例化之后生成的,并且将代理类的bean将原始类的bean替换掉。所以我们在使用springBeanName.method 调用目标类的方法时已经被Spring偷梁换柱 使用 proxyBeanName.method。
现在对于为什么通过this调用函数内部方法的形式无法触发AOP的拦截已经是显而易见的了。this调用内部方法是直接使用的是原始对象来调用,已经绕开了Spring的管理所以肯定不会触发AOP。
然后对于为什么Spring 要求使用@Transactional 注解的方法必须是public类型,其实这里也可以抽象为所有需要被AOP拦截的方法都必须被定义为public。因为Spring是不会管理到这些private方法的。
总结
总后来总结一下,如果想要使用@Transactional 或者说要使用AOP拦截方法必须遵循一下规则
- 目标函数必须为public类型
- 调用目标函数的方法必须通过springBeanName.method 的形式来调用,不能使用this直接调用内部方法