空指针异常经典例子(空指针异常是什么意思)
导语:线上问题之:诡异的空指针异常
没写过空指针的开发,不是个好厨子
一、问题背景
某个周1晚上,刚吃完晚饭到工位坐下,准备刷刷微博早早撤了,这时徒弟小王急冲冲找过来,说:“师傅,我这边有个很诡异的空指针异常,调了一个下午还没解决,需求今天要提交测试,能帮帮我不?”。都要提交测试了,那肯定得帮徒弟看看问题了,毕竟徒弟也花了那么久去解决。(本人比较反感那些有疑问但是自己都不好好研究就一味问别人的同事,不是不愿意帮,而是一些低级问题,真的很浪费时间,毕竟线程上下文切换也很耗时对不hhh)
来到徒弟工位,了解下需求点,大概是这样的:
(1)别的组的同事A提供了一个基础类,给徒弟调用。其实就是获取一些秘钥。(如图1)
(2)同事A提供的这个基础类的方法调用需要做记录、校验权限,加了个切面,对方法调用进行鉴权,如图2(看到这里是不是有些读者同学已经发现异常的端倪了)
图1,同事A提供的基础类(简化代码)
图2,同事A提供的基础类的切面(简化代码)
乍一看,感觉怪怪的,但是也说的过去(这里作者严重不建议把明文秘钥写在类里面,事后也跟同事A说了让他改掉)。然后接着看徒弟的调用代码,如下:
图3,徒弟调用同事A提供的BaseUtil的代码(简化代码)
二、排查过程
徒弟说,空指针就出现在40行(如图3)他拿到secret后做业务逻辑的过程中,也就是说拿到的这个secret是个null!但是如图1,同事A提供的secret是有值的。问题出在哪里?聪明的同学其实已经猜到了,这个切面加上同事A提供的BaseUtil,很是有问题!!
三、解开谜团
先说结论:由于final方法无法被代理,所以实际调用的是代理类(proxy)自身的getSecret方法,又由于代理类的成员变量不会被自动初始化,所以secret为null,即造成了代理类的getSecret方法返回的是null!
有了结论,有些同学可能还有点懵,听我细细道来:
(1)代理(proxy)的生成其实是spring使用cglib静态代理以原类(target)为父类生成了一个子类,然后proxy的成员变量包含了一个原类(target)的实例(这里也就是BaseUtil的实例),proxy会重写(override)所有的方法,然后在调用时都会路由到原类(target)的原方法上执行。示例如下图:
图3,proxy代理类示例
(2)既然代理(proxy)是原类的一个子类,那么大家应该都知道,子类是无法重写原类的final方法的,所以,这里调用的baseUtil.getSecret其实是cglib为代理(proxy)生成的一个同名方法(并没有路由到原类-target的getSecret方法!!!),这个方法逻辑和原类-target保持一致,即:return secret,返回的是代理(proxy)从父类继承过来的secret变量,有同学肯定会问,调用的是proxy自己的getSecret我认了,但是父类继承过来的secret明明是123456,那也应该同样返回这个呀,为什么是null??继续往下看解答你的疑问
(3)大家都知道,成员变量的初始化是在构造方法中执行的,常规的我们编写的java类,编译器默认会在构造方法第一行加上super(),而代理类是cglib生成的,并没有这个隐含设置。罪魁祸首就在这里,代理(proxy)的构造方法,并不会调用super(),而secret的初始化,是在父类(Baseutil)的构造方法中执行的(这里虽然直接复赋值,但是其实底层是把赋值动作放在了构造方法中,如图4),不调用super(),就无法完成secret的初始化(悲伤,cglib你好坏),所以导致,代理(proxy)的secret字段是null(如图5)。
图4,经过编译后,secret其实是放在构造方法中赋值的(这里只是伪代码示例)
图5,代理类的构造方法,不会带上super() (这里只是伪代码示例)
破案。
修复措施,想必大家已经想到了,徒弟气冲冲的找到同事A,让他去掉了getSecret的final修饰符,程序正常。此时调用就正常路由到了原类-target上)。各位读者朋友在日常开发中,也要特别注意使用aop带来的坑。也可以在评论区分享,您遇到的aop的坑:)
(PS:现在有好多开发同学,开发时间紧的话,都不自测了嘛?)
各位朋友路过点个关注呗,每天分享编程干货,来自互联网公司的真实新鲜的线上问题解析,帮您少踩点坑~
本文内容由小蔼整理编辑!