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

虚幻大学 xuhss 169℃ 0评论

? 优质资源分享 ?

学习路线指引(点击解锁) 知识定位 人群定位
? Python实战微信订餐小程序 ? 进阶级 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
?Python量化交易实战? 入门级 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统

问题起因

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

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

  • 类目录结构
    f665b62d1ea6d7fd190eab897949ccb8 - Aop踩坑!记一次模板类调用注入属性为空的问题
  • AbstractTestAop:顶层抽象类,定义骨架和执行顺序,内部通过Autowired注入了TopClassBean的实例对象。
    880aaa4dfab87aa4f91743321f697d05 - Aop踩坑!记一次模板类调用注入属性为空的问题

  • AbstractTestCglibAop:二级抽象类,继承自AbstractTestAop,空类无实现。
    15b8b436b31c3da653de852efb26a3a0 - Aop踩坑!记一次模板类调用注入属性为空的问题

  • TestCglibAopExample:具体子类,类上添加了@Component注解,空类无实现。
    359f4a5c1353d24514df0dd8a68a881f - Aop踩坑!记一次模板类调用注入属性为空的问题

  • TestAopRemoteEntrance:调用入口,它是一个Bean。
    f7cc36d056eadf413e904324c4c28d1b - Aop踩坑!记一次模板类调用注入属性为空的问题

  • TopClassBean:实例对象,内部提供一个方法用来表示被调用。
    19fd31b5f3e3feec435698507a593950 - Aop踩坑!记一次模板类调用注入属性为空的问题

  • AsyncExportLogAspect:方法切面(路径可以自己配置,此处对切面路径做了处理所以飘红)
    95752585eca1f87ff45eac292ff12d13 - Aop踩坑!记一次模板类调用注入属性为空的问题

单元测试

489d3ed4cde46c8a4cc4a100204b017f - Aop踩坑!记一次模板类调用注入属性为空的问题
单测结果:
9b0566f7b015203e101ebfce762a0377 - Aop踩坑!记一次模板类调用注入属性为空的问题

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

排查

方法debug

  1. 获取bean
    f0b510eb6b4a1e844f8c7d4d21289037 - Aop踩坑!记一次模板类调用注入属性为空的问题

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

  1. before()
    3f46d6bb6671a563cf6c4d8c45ad3ff4 - Aop踩坑!记一次模板类调用注入属性为空的问题

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

  1. myDo()
    03ee247a67f42a1e765700034108cc0d - Aop踩坑!记一次模板类调用注入属性为空的问题

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

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

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

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

源码debug

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

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

  1. 实例化完成情况
    cb358052910ddaba42e9dc4eacf225cc - Aop踩坑!记一次模板类调用注入属性为空的问题

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

  1. 后置处理器
    03f9d2a3ec3826756ab3f147a65bf288 - Aop踩坑!记一次模板类调用注入属性为空的问题

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

  1. 跟随断点进入AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsAfterInitialization方法查看
    f7f8111d23786647cc27f7ee5428374e - Aop踩坑!记一次模板类调用注入属性为空的问题

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

  1. 在方法中AbstractAutoProxyCreator#wrapIfNecessary判断了是否存在代理,此处生成了代理对象
    267fcfa75d1bd86b245fdf32d3a53243 - Aop踩坑!记一次模板类调用注入属性为空的问题

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

  1. 代理生成
    12b27ee4276c40a1cfb1cd8bd138ea93 - Aop踩坑!记一次模板类调用注入属性为空的问题

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

  1. 代理实现
    15be63916bc7014b4e7616a265662ac8 - Aop踩坑!记一次模板类调用注入属性为空的问题

这里我们可以很清楚的看到是使用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方法的时候仍然是目标类。
有兴趣的同学可以去翻一下源码,一起交流

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

附:代理类

ba368d8a0297986bf9baa4c499fa18cd - Aop踩坑!记一次模板类调用注入属性为空的问题
从代理类上面我们可以看出:

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

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


关注我的公众号一起交流吧!
c5b1443438c25570e39c747145c418f5 - Aop踩坑!记一次模板类调用注入属性为空的问题

转载请注明:xuhss » Aop踩坑!记一次模板类调用注入属性为空的问题

喜欢 (0)

您必须 登录 才能发表评论!