Spring事务与JDK/CGLIB动态代理

近日面试问及AOP和动态代理,再之后的事务方面有所生疏。
面试面了四个多小时,虽然凉了,但感谢面试官对我的错误给予了讲解,nice。(竟然没凉。。。)

  • 首先Spring事务在开发过程中是通过@Transactional注解来控制。

1. Transactional注解的原理

  • 对于使用了Transactional注解的方法的类,Spring AOP代理会在运行时生成这个类的代理对象。
  • 当这个对象运行这个注解方法时,会读取@Transactional注解里面配置的信息,决定该方法是否要使用TransactionInterceptor拦截器来拦截。
  • 当拦截器拦截该方法时,会在调用该方法之前,创建事务,并在这个事务中执行该方法,最后根据执行是否有异常使用抽象事务管理器(AbstractPlatformTransactionManager)操作数据源提交或者回滚这个事务。
  • 它本质使用一个事务拦截器,在方法调用的前后/周围进行事务性增强(advice),来驱动事务完成。也就是在业务方法外边通过Spring AOP包上一层事务管理器的代码(即插入切面),这是Java设计模式中常见的通过代理增强被代理类的做法。

2. JDK 和 CGLib动态代理区别

Spring AOP的底层有2种实现:JDK动态代理、CGLIB。

  • 前者的原理是JDK反射,并且只支持Java接口的代理;
  • 后者的原理是继承(extend)与覆写(override),因此能支持普通的Java类的代理。两种方式都是动态代理,即运行时实时生成代理。

由于JVM的限制,CGLIB无法替换被代理类已经被载入的字节码,只能生成并载入一个新的子类作为代理类,被代理类的字节码依然存在于JVM中。

  • 区别于前两者,AspectJ是一种静态代理的实现,即在编译时或者载入类时直接修改被代理类文件的字节码,而非运行时实时生成代理。因此这种方式需要额外的编译器或者JVM Agent支持,通过一些配置Spring和AspectJ也可以配合使用。

@Aspect一开始是AspectJ推出的Java注解形式,后来Spring AOP也支持使用这种形式表示切面,但实际上底层实现和AspectJ毫无关系,毕竟Spring AOP是动态代理,和静态代理是不兼容的。

2.1 JDK动态代理具体实现原理:
  • 通过实现InvocationHandlet接口创建自己的调用处理器;
  • 通过为Proxy类指定ClassLoader对象和一组interface来创建动态代理;
  • 通过反射机制获取动态代理类的构造函数,其唯一参数类型就是调用处理器接口类型;
  • 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数参入;
  • JDK动态代理是面向接口的代理模式,如果被代理目标没有接口那么Spring也无能为力,Spring通过Java的反射机制生产被代理接口的新的匿名实现类,重写了其中AOP的增强方法。
2.2 CGLib动态代理:

CGLib是一个强大、高性能的Code生产类库,可以实现运行期动态扩展java类,Spring在运行期间通过 CGlib继承要被动态代理的类,重写父类的方法,实现AOP面向切面编程呢。

2.3 两者对比:

JDK动态代理是面向接口的。
CGLib动态代理是通过字节码底层继承要代理类来实现(如果被代理类被final关键字所修饰,会失败)。

  • 如果要被代理的对象是个实现类,那么Spring会使用JDK动态代理来完成操作(Spirng默认采用JDK动态代理实现机制);
  • 如果要被代理的对象不是个实现类那么,Spring会强制使用CGLib来实现动态代理。

3. 两个问题

针对面试官的提问和回来后自己的测试和了解,有两个问题:

  1. 在一个实例方法中调用被@Transactional注解标记的另一个方法,且两个方法都属于同一个类时,事务不会生效。
  2. 调用被@Transactional注解标记的非public方法,事务不会生效。

4. 关于问题

4.1 自调用失效

一个例子,有如下调用逻辑:

controller部分代码

@RequestMapping("/transaction")
public Response test() {
    testService.create();
    return new Response();
}

TestService部分代码

@Override
public void create() {
    ....
    insert();
    ....
}

@Transactional(rollbackFor = Exception.class)
public void insert() {
    insertBid();
}

private User insertTest() {
    User user = new User();
    user.setName("xx");
    user.setId(100);
    userMapper.insert(user);
    return user;
}
  • TestService的对外接口是create(),但不想整个方法做事务,只对于insert()里面的操作做事务。
    但这种写法是无效的。虽然执行时候不会有任何报错异常,但一旦insert()方法执行过程中抛出异常,事务是不会生效的,即使方法是public也没用(Transaction注解要求作用于public的方法上)
@Transactional 的AOP切点
  • 上述问题涉及到@Transactional 的AOP切点。
  • Spring AOP是与IOC配合使用的,而Spring AOP是用动态代理的技术。 也就是说一个类被IOC所注入生成的对象被Spring动态代理成一个新的代理对象。
  • 无效的原因其实就容易理解了,在外部调用这个动态代理对象,会在代理的时候增强对象,但在对象的内部调用的时候,调用的还是原来的对象的方法,该方法明细不会被AOP增强。
  • 如上面的例子,在Spring里,TestService注入到controller中,生成一个代理的testService对象,在controller调用testService的方法时,被代理的方法拦截。该方法中会找出这个AOP连接点的Advice,然后切入执行(也就是执行@Transactional)。而若是调用对象内容方法的时候,就不会被代理发放拦截的了。
  • 下图即为拦截切入点,框内为切入后执行该切点的Advice,事务就在其中执行
    Spring事务与JDK/CGLIB动态代理_第1张图片

4.2 不支持非public

第二个问题则是Spring AOP不支持非public方法增强,与自调用类似,也是动态代理无法解决的盲区。

  • 从代码分析原因是TransactionInterceptor拦截器会间接调用到一个AbstractFallbackTransactionAttributeSource里面的computeTransactionAttribute方法,通过这个方法来获取注解上的各个属性,同时判断该方法是否为public,若不是public则不会读取注解属性,返回null。

Spring事务与JDK/CGLIB动态代理_第2张图片

虽然CGLIB通过继承的方式是可以支持public、protected、package级别的方法增强的,但是由于JDK动态代理必须通过Java接口,只能支持public级别的方法,因此Spring AOP不得不取消非public方法的支持。

5.总结

面试造航母就完事了。

你可能感兴趣的:(Spring)