3

Aop踩坑!记一次模板类调用注入属性为空的问题 - 邱志强

 2 years ago
source link: https://www.cnblogs.com/qiuzhiqiang/p/16164861.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Aop踩坑!记一次模板类调用注入属性为空的问题 - 邱志强 - 博客园

在做一个需求的时候,发现原来的代码逻辑都是基于模板+泛型的设计模式,模板用于规整逻辑处理流程,泛型用来转换参数和选取实现类。听上去是不是很nice!
但是在方法调用的时候却突然爆出一个NPE,直接给人整蒙了!不过懵归懵,该排查的还是需要排查的,下面我使用一个例子来模拟分析我这次的排查的过程。

tips:因为例子我直接就定义在公司的项目当中,所以很多路径打上了马赛克,请勿介意噢!毕竟我们主要还是学习避坑的。ღ( ´・ᴗ・` )比心

  • 类目录结构
  • AbstractTestAop:顶层抽象类,定义骨架和执行顺序,内部通过Autowired注入了TopClassBean的实例对象。
  • AbstractTestCglibAop:二级抽象类,继承自AbstractTestAop,空类无实现。
  • TestCglibAopExample:具体子类,类上添加了@Component注解,空类无实现。
  • TestAopRemoteEntrance:调用入口,它是一个Bean。
  • TopClassBean:实例对象,内部提供一个方法用来表示被调用。
  • AsyncExportLogAspect:方法切面(路径可以自己配置,此处对切面路径做了处理所以飘红)

单测结果:

很明显:顶层接口内部实例引用的TopClassBean对象未注入,属性为空,导致空指针!

方法debug

  1. 获取bean

可以看到此时获取到的Bean类型为一个代理类,继续往下,进入到invoke方法
2. before()

可以发现进入到protected修饰的Before方法的时候由代理转变为实际的类方法调用了

  1. myDo()

进入到final修饰的Mydo方法的时候又由实际类切换到代理类调用了,这时候内部引用topClassBean为空,最后NPE

总结:
由上可知,cglib动态代理可以代理目标类非final和private方法,当调用final或者private方法时,由于目标类中不存在此方法,所以还是使用代理类进行调用。

下面我们可以进行源码debug,主要解决两个问题:

  1. 为什么会发生代理
  2. 代理类为啥属性为空

源码debug

通常代理都是发生在Bean实例化完成之后,对成品的Bean进行代理,多发生在BeanProcess后置处理中

按照这个思路咱们开始走断点debug:

  1. 实例化完成情况

我们发现实例化完成内部属性是有引用值的,不等于null,所以问题不在这,往下看
2. 后置处理器

重点:从这里我们发现Bean变成了代理对象,并且内部引用变成了null,证实了我们的猜想,由此可断定问题出现在BeanProcess的后置处理中

  1. 跟随断点进入AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsAfterInitialization方法查看

发现经历了AbstractAutoProxyCreator#postProcessAfterInitialization方法后就发生了代理改变,我们继续往下

  1. 在方法中AbstractAutoProxyCreator#wrapIfNecessary判断了是否存在代理,此处生成了代理对象

在此处我们发现了因为aop切面存在,所以导致启用了代理问题一解决

因为没有接口,所以使用cglib代理

这里我们可以很清楚的看到是使用new构造生成出来的代理类,所以实例属性值为空就解释的通了,问题二解决

总结:
由于AOP切面存在,导致目标类发生代理,生成了目标子类的代理Bean,代理类是通过 objenesis.newInstance(proxyClass, enhancer.getUseCache())构造出来的,所以不存在相关属性,联系到cglib代理原理---通过ASM字节码框架在运行期写入字节码跳过了编译期,可以佐证咱们的定论。
针对上面两个问题结论如下:

  1. 由于方法切面导致目标类发生代理
  2. 代理类是在运行期通过构造new出来的,属性值为空,所以代理类进行实例调用,会报NPE

我们对整个问题进行一个完整性总结:
由于AOP切面代理的原因,导致内部final方法调用走的代理类调用,代理类实例属性为空,导致NPE。
模板顶层为抽象类,未实现接口,导致选择cglib代理,cglib通过构造new实现代理类,内部属性均为空,由于通过继承实现,final和private方法无法被代理,所以当不可继承方法被调用时,当前对象为代理类,否则为目标类。

  1. 顶层实现接口,避免cglib代理
  2. 方法访问修饰变更,可被继承代理
  3. 手动getBean,指定目标类对象调用
在调试的过程还发现一个有意思的现象:
整个引用调用链的方法栈上只要有一个方法被代理,调用链后端的所有方法都将使用目标类调用,不会导致NPE。
举个例如下:invoke(final) -> myDo1(非final) -> myDo(final),此时不会产生NPE,因为这个时候执行Mydo方法的时候仍然是目标类。
有兴趣的同学可以去翻一下源码,一起交流

附:代理类

从代理类上面我们可以看出:

  • 代理类继承具体子类TestCglibAopExample,所以final或者private相关方法,即Mydo()和invoke()方法代理类未提供实现,无法被代理。

获取代理类class文件命令,在idea启动参数中添加
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
-Dcglib.debugLocation=/Users/xxx


关注我的公众号一起交流吧!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK