最近再使用Spring的AOP的时候碰到了两个问题。
- Spring的事务的自调用失效
- Spring的bean中的public字段, 直接修改(不是通过set方法)不生效的问题
这个促使我需要弄清楚Spring的AOP的原理, 因此才有这篇博客。
SpringAOP的实现方式
Spring的实现方式有两种, 通过jdk的动态代理来实现的和通过Cglib框架的修改字节码的方式实现。
- jdk的动态代理
动态代理有其局限性。 就是被代理对象必须是某个接口的实现类。 同时生成的代理类也只能对接口中的已有的方法代理, 并且是在运行过程中动态生成代理类的。
- 有局限性, 只能通过接口来实现代理的,
- 并且是在运行中生成的代理类, 反射调用的代理方法, 在运行过程中有一定的性能问题
- Cglib字节码代理
这个是通过字节码生成工具, 生成被代理类的子类, 子类持有父类对象的引用, 然后通过静态代理的方式来实现的。
静态代理的实现, 不用反射调用,性能好。
通过字节码来生成的子类, 可以在项目启动过程中就加载完成类, 不用运行中加载,性能好
但是需要依赖第三方的字节码包来生成子类, 并且需要多生成加载一个我们没有定义的子类
下面的内容都是讲的Cglib的方式来阐述的, 只要理解了Cglib后, JDK的动态代理的方式会很简单的。
事务自调用失效问题
我们看下Controller中注入的代理类的内部情况
可以看到代理类的内部持有很多的GGLIB$CALLBACK_2
类似的内部成员。 其中GGLIB$CALLBACK_1
成员才是真正的被代理类的对象,
Cglib生成的代理类的代码逻辑如下:
代理类实际上是被代理类的子类, 并且在这个子类中还持有父类对象的引用。从图中可以看到实际上真正的业务逻辑都是交给父类对象的方法的。 并且在 doService2()
中插入了事务的逻辑。
实际上就是静态代理, 它是Cglib的方式生成的静态代理类的。 明白静态代理和动态代理的区别就容器理解。
下面截图中可以看到被代理类的逻辑
被代理类逻辑很简单, 在doService方法的内部调用了自己的doService2方法, 并且标注了doService2方法是开启事务的。
他们的UML关系图如下
如果我们通过代理类调用doService方法, 实际上的调用链路是:
代理类ServiceImpl#doService
-------------------> 被代理类ServiceImpl#doService
----------------------->被代理类ServiceImpl#doService2()
整个链路中根本就没有调用到 代理类SerivceImpl#doService2()方法。 而我们的的事务代码逻辑是在这个代理类SerivceImpl#doService2()方法中的, 也就是事务根本不会触发。 事务自然也就失效了。
代理类是什么时候注入到容器中的
如果我们开启了事务,会注册一个BeanPostProcesser
的实现类到容器中, 这个BeanPostProcesser会在postProcessAfterInitialization方法中使用代理类来偷梁换柱真正的bean。
postProcessAfterInitialization 是在bean初始化完成后会调用的方法,也就是说被偷梁换柱的真正bean已经完成了初始化, 该有的依赖已经注入进去了。
代理类中的public/protected字段无效的问题
我们明明在customerRepository字段上注解了Autowire, 但是在Controller中注入的代理类中的customerRepository
字段却是空的。
doService内部使用了customerRepository来访问数据, 我们调用doService方法, 又是可以成功的。 这就出现了一个很奇怪的现象。
如果我们开启了事务,会注册一个
BeanPostProcesser
的实现类到容器中, 这个BeanPostProcesser会在postProcessAfterInitialization方法中使用代理类来偷梁换柱真正的bean。
postProcessAfterInitialization 是在bean初始化完成后会调用的方法,也就是说被偷梁换柱的真正bean已经完成了初始化, 该有的依赖已经注入进去了。
有上面疑问的原因还是弄混了, 我们往往以为注入的类就是我们代码写的类, 实际上不是的, 它注入的实际是Cglib自动生成的类, Cglib自动生成的类中的customerRepository字段是继承自父类的, 它不会进行依赖注入的。 所以customerRepository字段是空的。
doService能够使用customerRepository字段 是因为这个方法会内部实际上是调用父类对象的同名doService方法的, 父类对象实际上已经完成了依赖的注入了。 所以父类对象中直接使用它自己的customerRepository字段没有问题。
JDK的动态代理的问题
自调用失效
JDK的动态代理实际上也是生成的一个子类, 但是这个子类不是被代理类的子类,而是接口的实现子类。 所以自调用失效还是因为根本就没有调用子类中的包含事务逻辑的方法。
public 字段无效的问题
JDK的动态代理不会有这个问题的, 代理类是接口的实现子类, 它根本就不会继承被代理类的的字段, 都没有这个字段哪里来的失效问题。
看过的很多的说法,Spring会默认使用JDK的代理。
我在写代码发现, 无论代理类有没有接口, 都是使用的Cglib的方式来代理类的,于是我查了下文档和博客。
https://www.cnblogs.com/coderxiaohei/p/11758239.html 在这篇博客中说的很清楚。
我们常用的SpringBoot2.x中 会自动配置成Cglib的代理方式,所以在SpringBoot2.x中无论我们怎么设置JDK代理的配置,都不会生效, 它会坚定的使用Cglib。
但是如果我们使用Spring5.0, 它的内部还是会默认使用JDK的代理的, 并且可以根据我们的配置来切换Cglib还是jdk。