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

楔子

现在在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直接调用内部方法
发布了418 篇原创文章 · 获赞 745 · 访问量 126万+

猜你喜欢

转载自blog.csdn.net/u013467442/article/details/98865455
今日推荐