背景
遇到问题:在进行Spring单元测试编写时,发现被测方法是一个私有方法,无法直接通过注入对象调用
解决思路:首先想到通过反射获取该私有方法的访问权限,并传入注入对象,最终调用对象的私有方法。
出现的异常
运行时抛出空指针异常
定位问题
- 点击异常代码行打上断点,debug调试
- 通过查看变量值发现roleMapper为空,从而导致空指针
- 而roleMapper是传入this对象的属性,因此,问题来自传入的对象
分析问题
- 通过分析this对象,可以发现它是一个被Cglib代理后的实例,由此可知,该类方法上必定有@Transactional事务注解或AOP注解修饰,从而被SpringCglib代理
- 查看cglib原理:
动态生成一个要代理类的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用java反射的JDK动态代理要快。
其中重要的一点,代理类是被代理类的子类,回想关于Java中的继承,有一条很重要的特性就是:
- 子类拥有父类非 private 的属性、方法。
- 此时,尝试修改私有方法变成public,发现this对象恢复正常,由此锁定代理类和私有方法出现问题
- 通过搜索cglib代理类私有方法发现原因:
- 由此可知,此处注入的cglib代理对象中不包含private方法!
- 那为啥同样传入的代理对象,调用public方法就成功,而调用private方法就失败呢?
- 如果是私有方法,那么在代理类中,不会包含这个方法。此时通过Method.invoke()来调用目标方法,传入的实例对象是userController的代理类,而这个代理类中的userService为NULL,所以,执行的时候,才会看到userService没有注入,导致空指针异常。
- 如果是公共方法,在代理类中,就有它的子类实现,则会先调用到代理类的拦截器MethodInterceptor。拦截器负责链式调用AOP方法和目标方法。在拦截器执行过程中,又调用了方法。但不同的是,此时传入的实例对象并不是代理类,而是代理类的目标对象。
结论:可以发现代理类正常情况下,执行到原方法时是通过代理的目标对象(即原始对象)来执行,而当代理类发现没有代理对应的private方法时,则直接通过代理对象(即上文的this)执行目标方法。
解决方法
既然我们需要的是只原始对象执行私有方法,只要通过代理类获取原始的目标对象即可。
// 由于cglib类是通过继承代理,无法代理私有方法,因此无法通过原始对象执行方法
if (AopUtils.isCglibProxy(menuService)) {
// 如果是cglib代理对象,则转为原始对象
menuService = (MenuServiceImpl)AopProxyUtils.getSingletonTarget(menuService);
}
此时得到的对象即为原始对象,bug成功消灭!
参考文章: