Spring事务和动态代理

2022年 3月 31日 65点热度 0人点赞 0条评论

Spring AOP 动态代理机制在不同版本中的差异

这里仅作为备忘点介绍一下, 大多数情况下在使用的时候并不会关注到.

  • Spring 5.x 版本开始, AOP 默认依旧使用 JDK 动态代理, 并非网上说的默认改成了 CGLIB.
  • SpringBoot 2.x 开始, 由于使用 JDK 动态代理可能导致的类型转化异常问题, 默认使用 CGLIB.
  • SpringBoot 2.x 中, 如果需要默认使用 JDK 动态代理可以通过配置项 spring.aop.proxy-target-class=false 来进行修改,proxyTargetClass 配置已无效.

作者: 磊叔的技术博客
链接:https://juejin.cn/post/6986112993259945992

JDK 动态代理可能导致的类型转化异常问题

看到上文的作者提到了这个问题, 我有点好奇, 这个类型转化的问题会在什么场景下触发.

Spring AOP 代理机制

Spring 代理机制造成的.

简单的说, 就是通过 Spring 容器获取的类对象, 很多情况下并不是原类, 而是被 Spring 修饰过了的代理类.

例如你执行 A 类对象的方法 A.invoke(), 而 spring 对 A 类做了修饰, 实际你运行的是 spring 修饰过的代理类 proxyAbean.invoke() 方法.

这样就会造成一个问题, 如果你在 invoke() 中调用 A 类的其余方法 invoke2(), 此时 invoke2() 是直接调用的原类的 A.invoke2(), 而不是代理类 proxyAbean.invoke2(),Spring 对方法做的修饰增强 (@Async,@Transational,AOP) 全部不会实现.

有意思的问题

问题 1: 调用 saveInit() 方法, 下面的代码插入了几条数据?

/**
 * @author wangzhi
 */
@Service
public class TransactionService {

    @Autowired
    private CourseMapper courseMapper;

    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    public void save() {
        CourseEntity course = new CourseEntity();
        course.setCourseName("语文");
        course.setPrice(new BigDecimal(100));
        courseMapper.insert(course);
        //故意制造异常
        System.out.println(1/0);
    }

    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    public void saveInit() {
        CourseEntity course = new CourseEntity();
        course.setCourseName("数学");
        course.setPrice(new BigDecimal(100));
        courseMapper.insert(course);
        //调用 save 方法
        try{
            save();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

问题 2: save() 的事务传播改成 Request_NEW, 那么会插入几条数据呢?

/**
 * @author wangzhi
 */
@Service
public class TransactionService {

    @Autowired
    private CourseMapper courseMapper;

    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
    public void save() {
        CourseEntity course = new CourseEntity();
        course.setCourseName("语文");
        course.setPrice(new BigDecimal(100));
        courseMapper.insert(course);
        //故意制造异常
        System.out.println(1/0);
    }

    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    public void saveInit() {
        CourseEntity course = new CourseEntity();
        course.setCourseName("数学");
        course.setPrice(new BigDecimal(100));
        courseMapper.insert(course);
        //调用 save 方法
        try {
            save();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

以上来源: https://blog.csdn.net/qq_42290561/article/details/105913690

答案都是插入了两条数据, 想一想 Spring 动态代理在这个地方的坑吧, 抓住要点, 谁是切面以及切面有没有切进去.

注解@EnableAspectJAutoProxy(exposeProxy = true)

@EnableAspectJAutoProxy(exposeProxy = true)
TransactionService proxy = (TransactionService) AopContext.currentProxy();
    /**
     * ThreadLocal holder for AOP proxy associated with this thread.
     * Will contain {@code null} unless the "exposeProxy" property on
     * the controlling proxy configuration has been set to "true".
     * @see ProxyConfig#setExposeProxy
     */
    private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal<Object>("Current AOP proxy");

这个地方就是存放 AOP 代理对象的, 不是真实的对象.

    /**
     * Try to return the current AOP proxy. This method is usable only if the
     * calling method has been invoked via AOP, and the AOP framework has been set
     * to expose proxies. Otherwise, this method will throw an IllegalStateException.
     * @return Object the current AOP proxy (never returns {@code null})
     * @throws IllegalStateException if the proxy cannot be found, because the
     * method was invoked outside an AOP invocation context, or because the
     * AOP framework has not been configured to expose the proxy
     */
    public static Object currentProxy() throws IllegalStateException {
        Object proxy = currentProxy.get();
        if (proxy == null) {
            throw new IllegalStateException(
                    "Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.");
        }
        return proxy;
    }

currentProxy() 试图返回当前的 AOP 代理对象. 这个方法只能被 AOP 环绕的中的方法调用, 并且 AOP 框架还要设置为可以暴露代理才行. 否则, 这个方法将会抛出 IllegalStateException 异常.

我在 IDEA 中点了一下方法 StaticUnadvisedExposedInterceptor#intercept, 发现了这个有意思的代码, 证实了我的猜想: 即每次切进去 AOP 的时候都会把上一个的 AOP 对象给弹出来, 并且将新的 AOP 对象设置进去, 然后等新的 AOP 执行完了, 再把旧的 AOP 对象设置进去.

@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) {
    Object oldProxy = null;
    try {
        oldProxy = AopContext.setCurrentProxy(proxy);
        Object retVal = methodProxy.invoke(this.target, args);
        return processReturnType(proxy, this.target, method, retVal);
    }
    finally {
        AopContext.setCurrentProxy(oldProxy);
    }
}

这个是否暴露 AOP 对象的开关位置在:

public class ProxyConfig implements Serializable {

    /** use serialVersionUID from Spring 1.2 for interoperability */
    private static final long serialVersionUID = -8409359707199703185L;

    private boolean proxyTargetClass = false;

    private boolean optimize = false;

    boolean opaque = false;
    // here!!!
    boolean exposeProxy = false;

    private boolean frozen = false;
    }

然后在这里进行判断的:

@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
    Object oldProxy = null;
    boolean setProxyContext = false;
    Class<?> targetClass = null;
    Object target = null;
    try {
        if (this.advised.exposeProxy) {
            // Make invocation available if necessary.
            oldProxy = AopContext.setCurrentProxy(proxy);
            setProxyContext = true;
        }

AopContext 还有没有其他用途?

使用这个工具类的时候, 还需要关注另一个问题, 就是当在跨线程, 或者使用线程池的情况下, 需要手动将 proxy 透传到新的线程中, 否则, 即使开始 exposeProxy = true, 同样也会出现 Cannot find current proxy: Set 'exposeProxy' 报错.

链接:https://juejin.cn/post/6986112993259945992

本文来自:https://blog.duhbb.com

本文链接地址:Spring事务和动态代理,英雄不问来路,转载请注明出处,谢谢。

有话想说:那就赶紧去给我留言吧。

rainbow

这个人很懒,什么都没留下

文章评论