问题起因

在做一个需求的时候,发现原来的代码逻辑都是基于模板+泛型的设计模式,模板用于规整逻辑处理流程,泛型用来转换参数和选取实现类。听上去是不是很nice!
  • 类目录结构

  • 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切面存在,所以导致启用了代理问题一解决

  1. 代理生成

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

  1. 代理实现

这里我们可以很清楚的看到是使用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


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

Aop踩坑!记一次模板类调用注入属性为空的问题的更多相关文章

  1. Springcloud踩坑记---使用feignclient远程调用服务404

    公司项目进行微服务改造,由之前的dubbo改用SpringCloud,微服务之间通过FeignClient进行调用,今天在测试的时候,eureka注册中心有相应的服务,但feignclient就是无法 ...

  2. 【踩坑记】从HybridApp到ReactNative

    前言 随着移动互联网的兴起,Webapp开始大行其道.大概在15年下半年的时候我接触到了HybridApp.因为当时还没毕业嘛,所以并不清楚自己未来的方向,所以就投入了HybridApp的怀抱. Hy ...

  3. Spark踩坑记——共享变量

    [TOC] 前言 Spark踩坑记--初试 Spark踩坑记--数据库(Hbase+Mysql) Spark踩坑记--Spark Streaming+kafka应用及调优 在前面总结的几篇spark踩 ...

  4. Spark踩坑记——从RDD看集群调度

    [TOC] 前言 在Spark的使用中,性能的调优配置过程中,查阅了很多资料,之前自己总结过两篇小博文Spark踩坑记--初试和Spark踩坑记--数据库(Hbase+Mysql),第一篇概况的归纳了 ...

  5. EOS踩坑记

    [EOS踩坑记] 1.每个account只能更新自己的contract,即使两个account的秘钥相同,也不允许. 如下,使用alice的权限来更新james的contract.会返回 Missin ...

  6. Spring @Transactional踩坑记

    @Transactional踩坑记 总述 ​ Spring在1.2引入@Transactional注解, 该注解的引入使得我们可以简单地通过在方法或者类上添加@Transactional注解,实现事务 ...

  7. Vue + TypeScript + Element 搭建简洁时尚的博客网站及踩坑记

    前言 本文讲解如何在 Vue 项目中使用 TypeScript 来搭建并开发项目,并在此过程中踩过的坑 . TypeScript 具有类型系统,且是 JavaScript 的超集,TypeScript ...

  8. WinUI 3 踩坑记:第一个窗口

    本文是 WinUI 3 踩坑记 的一部分,该系列发布于 GitHub@Scighost/WinUI3Keng,文中的代码也在此仓库中,若内容出现冲突以 GitHub 上的为准. WinUI 3 应用的 ...

  9. Spark踩坑记——Spark Streaming+Kafka

    [TOC] 前言 在WeTest舆情项目中,需要对每天千万级的游戏评论信息进行词频统计,在生产者一端,我们将数据按照每天的拉取时间存入了Kafka当中,而在消费者一端,我们利用了spark strea ...

随机推荐

  1. Android 12(S) 图形显示系统 - 简单聊聊 SurfaceView 与 BufferQueue的关联(十三)

    必读: Android 12(S) 图形显示系统 - 开篇 一.前言 前面的文章中,讲解的内容基本都是从我们提供的一个 native demo Android 12(S) 图形显示系统 - 示例应用( ...

  2. 论文翻译:2021_Joint Online Multichannel Acoustic Echo Cancellation, Speech Dereverberation and Source Separation

    论文地址:https://arxiv.53yu.com/abs/2104.04325 联合在线多通道声学回声消除.语音去混响和声源分离 摘要: 本文提出了一种联合声源分离算法,可同时减少声学回声.混响 ...

  3. 对象头源码讲解,原来,指向objectMonitor的指针在这里

    markword 注释 该文件目录在: \openjdk-jdk8u\hotspot\src\share\vm\oops\markOop.hpp #ifndef SHARE_VM_OOPS_MARKO ...

  4. 使用 Spring Cloud 有什么优势?

    使用 Spring Boot 开发分布式微服务时,我们面临以下问题 与分布式系统相关的复杂性-这种开销包括网络问题,延迟开销,带宽问题,安全问题. 服务发现-服务发现工具管理群集中的流程和服务如何查找 ...

  5. Redis 的数据类型?

    Redis 支持五种数据类型:string(字符串),hash(哈希),list(列表), set(集合)及 zsetsorted set:有序集合). 我们实际项目中比较常用的是 string,ha ...

  6. Java 中的同步集合与并发集合有什么区别?

    同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发 集合的可扩展性更高.在 Java1.5 之前程序员们只有同步集合来用且在多线程并发 的时候会导致争用,阻碍了系统的扩展性.Jav ...

  7. 一张自增表里面总共有 7 条数据,删除了最后 2 条数据,重启 MySQL 数据库,又插入了一条数据,此时 id 是几?如何获取当前数据库版本?

    一张自增表里面总共有 7 条数据,删除了最后 2 条数据,重启 MySQL 数据库,又插入了一条数据,此时 id 是几? 一般情况下,我们创建的表的类型是InnoDB,如果新增一条记录(不重启mysq ...

  8. 面试问题之C++语言:说一下static关键字的作用

    1.全局静态变量 在全局变量加上关键字static,全局变量就定义成一个全局静态变量,存放于静态存储区,在整个程序运行期间一直存在:未经初始化的全局静态变量会被自动初始化为0:全局静态变量在声明他的文 ...

  9. 面试问题之计算机网络:简述TCP和UDP的区别以及优缺点

    转载于:https://blog.csdn.net/liulin1207/article/details/80960964 一.TCP: TCP是面向连接的通讯协议,通过三次握手建立连接,通讯完成时四 ...

  10. 学习zabbix(四)

    一. 监控知识 1.硬件监控: 机房巡检,远程控制卡,IPMI 2.系统监控: cpu负载查看(指标):uptime(低于3),top(30%-70%), vmstat, mpstat 内存监控: f ...