一次SPRING声明式事务失效的探讨

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/u013467442/article/details/98865455

楔子

现在在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直接调用内部方法
展开阅读全文

关于Spring声明式事务的隔离级别。

07-27

最近在研究Spring事务的隔离级别Serializable,可是在一些实验中遇到了一些问题,实验过程如下:rn[color=#FF0000]有表 T ,它只有二个字段:id,name,这个表只一条数据,id=1;name='hello'[/color]rn1、先在Oracle数据库验证Serializable这个隔离级别的情况。rn 事务A有如下过程:开启事务,设置事务级别为Serializable,睡眠30秒,从表T查询id=1的name值并打印到DBMS,提交事务。rn 事务B有如下过程:开启事务,设置事务级别为Serializable,更新id=1的name值为[color=#FF0000]'world'[/color],提交事务。rn rn 现在先执行事务A,并且10秒内执行事务B,plsql上的确显示事务B是比事务A先结束的,此时DBMS的output上显示name值为[color=#FF0000]hello[/color],这个结果符合了事务在Serializable隔离级别时,事务是串先执行的。rnrn2、在Spring中验证事务隔离级别为Serializable的情况,仍然用初始化的数据表Trn 有两个java方法:getName()、updateName(),rn getName()方法有如下过程:睡眠30秒,从表T查询id=1的name值并打印到控制台。rn updateName()方法有如下过程:更新id=1的name值为[color=#FF0000]'world'[/color]。rn Spring为这两个方法都配置了事务,传播性都为REQUIRED,隔离级别都为SERIALIZABLE。rn 现在main方法中通过多线程调用getName()和updateName()这两个方法,保证这样一个执行顺序:rn [color=#FF0000]getName()先执行,然后在10~20秒内执行updateName()方法,updateName()方法比getName()方法要先执行完。[/color]rnrn 这个执行后,通过在控制台的Spring Debug信息中可以看到执行过程如下:rn a、为getName方法开启一个事务A,更改其隔离级别为SERIALIZABLE,程序睡眠30秒。rn b、为updateName方法开启一个事务B,更改其隔离级别为SERIALIZABLE,更新name值为'world',提交事务Brn c、getName程序睡眠结束,事务A继续执行,查找name字段的值,[color=#FF0000]这时候居然输出为'world'[/color],提交事务A。rnrn[color=#800000]现在的问题是[/color]:Spring配置两个事务隔离级别都为SERALIZABLE的时候,居然会出现c这种情况。rn 明明是事务A先开启,那么按照串行事务的说法,不是说哪个事务先开启,就先执行完那个事务再执行后面的事务吗?rn[color=#FF0000]那么为什么c步打印到的name值不是hello值而是world值呢?[/color]rnrnrn文字表达了这么多,也不知道大神们有没有耐心看下去了。。。rnrn 论坛

没有更多推荐了,返回首页