深入探索Springboot2.x依赖注入Bean的CGLIB代理类

news2024/9/27 23:30:40

问题起源

笔者最近在做一个功能,使用了工厂模式/策略模式设计的,定义了一个接口,下面有多种实现并通过@Component注解定义为Bean,在运行时根据不同的业务调用不同实现的Bean,所以需要在运行时动态获取Bean。因此,笔者尝试了好几种方法动态获取Bean,最后经过实践,采用以下方式动态获取Bean:

@Autowired
private ApplicationContext applicationContext;

applicationContext.getBeansOfType(<接口>.class)

但在开发的过程中通过IDEA的debug功能,发现从applicationContext中获取的Bean中的成员变量都是null,而且莫名其妙多了很多"CGLIBS$CALLBACK_"+数字的成员变量。如图:
Bean的成员变量
一开始我还以为是我拿错了,没拿到正确的Bean或者Bean未正确注册或初始化,但是我通过IDEA的断点步进(step into)发现是能正常进入方法里面的,而且进入方法后观察成员变量都是有值的,所以功能算是实现了,代码能正常跑,但是也引起了笔者的疑问和好奇心,引申出本文的核心问题:

为什么从applicationContext中获取的Bean中的成员变量都是null,而且莫名其妙多了很多"CGLIBS$CALLBACK_"+数字的成员变量?

排查

疑问 1:是Bean的注册方式不对,导致拿到的Bean的成员变量都是null?

答:不是的,经过笔者实验,Bean都是通过@Component注册的,经过笔者通过IDEA debug其他通过@Autowired注入的Bean的成员变量都是null,但也能正常调用的。

疑问2:是通过ApplicationContext获取Bean和普通的@Autowired的Bean不一样,导致拿到的Bean的成员变量都是null?

答:不是的,经过笔者实验结合网上的技术文章,@Autowired本质上也是通过ApplicationContext获取Bean的,所以通过applicationContext.getBeansOfType或applicationContext.getBean来获取到的Bean是和@Autowired注入的Bean一样的。

继续排查

既然我Bean的注册和Bean的获取都是正确的,那为什么Bean中的成员变量都是null,而且莫名其妙多了很多"CGLIBS$CALLBACK_"+数字的成员变量?
为回答这个问题,我尝试了在搜索引擎查找,发现原来Spring中依赖注入的Bean都是通过代理的方式实现的,在依赖注入时注入的并不是对象实例而是代理类。而Java中实现代理有两种方式:JDK动态代理及CGLIB代理,这两种代理方式的区别、原理、应用场景在各大搜索引擎都有很多文章可参考,这里就不详细赘述了,感兴趣的同学可以自己到搜索引擎找来看。这里特别提一嘴:两种代理方式很重要的区别就是JDK动态代理所代理的类必须实现了某个接口,而CGLIB代理则没有这个限制(注意这里是个伏笔,后面会讲到)。Spring会自动选择使用哪种代理方式。
举个栗子:

// 定义接口
public interface IUserService {
	void addUser()
}

// 定义接口实现
@Service
public class UserServiceImpl implements IUserService{
    public void addUser() {
        System.out.println("添加用户");
    }
}

// 依赖注入方式一:
// 这种方式无论JDK动态代理的Bean还是CGLIB代理的Bean都不会出现问题
@Autowired
private IUserService userService;

// 依赖注入方式二:
// 是的,这也是依赖注入的一种方式
// 只是我们大多数情况下是通过接口(即上面的方式一)注入的
// 这种方式因为不是基于接口的,所以如果是JDK动态代理的Bean就会出现问题。
// 而CGLIB代理的Bean则不会出现问题
@Autowired
private UserServiceImpl userService;
 

这里又又又引申出一个疑问:我的Bean都是有实现某个接口的,为什么Spring并没有使用JDK动态代理,而是使用了CGLIB代理呢?
带着这个疑问我又在搜索引擎咔咔一顿查,最后发现原来:

  1. Spring 5.x 中 AOP 默认依旧使用 JDK 动态代理。
  2. SpringBoot 2.x 开始,为了解决使用 JDK动态代理可能导致的类型转化异常而默认使用 CGLIB。
  3. 在 SpringBoot 2.x 中,如果需要默认使用 JDK动态代理可以通过配置项spring.aop.proxy-target-class=false来进行修改,proxyTargetClass配置已无效。

关于以上结论可参考以下文章:
https://blog.csdn.net/qq_45607784/article/details/134781410
https://www.163.com/dy/article/J8871AJ5055616YO.html
https://blog.csdn.net/HD243608836/article/details/122246618

笔者刚好是用 SpringBoot 2.x 的,所以这也解释了我的Bean都是有实现某个接口但依然使用了CGLIB代理的原因。

探索

既然知道了:

  1. Springboot2.x中依赖注入的Bean都是CGLIB代理
  2. 为什么是CGLIB代理而不是JDK动态代理的原因

那么通过搜索引擎就能找到很多关于Bean以及CGLIB代理的原理。基于Spring中CGLIB代理类生成的原理及过程就可以回答最初的疑问了:

  1. Spring通过reflectionFactory.newConstructorForSerialization生成被代理类的子类时是不会对成员变量初始化的,所以生成的代理类中的被代理类的成员属性都是null
    在这里插入图片描述

  2. 真正的Bean的对象实例其实是包裹在Bean代理类的成员变量callback当中(CGLIB$CALLBACK_0.advised.targetSource.target就是真正的对象实例,通过IDEA观察可以看到里面的成员变量都是经过初始化,是有赋值的),当调用代理类方法时会进入到CGLIB代理的拦截器(即CglibAopProxy.DynamicAdvisedInterceptor#intercept)中再查找真正实例中的方法进行调用。
    在这里插入图片描述

结论

综上所述:无论通过@Autowired依赖注入还是applicationContext获得的Bean其实都是经过CGLIB代理生成的包装着对象实例的代理类,代理类中最外层的原对象的成员变量和一堆CALLBACK都是由反射生成的,其中最外层的原对象的成员变量未经过初始化所以是没用的,真正有用到的其实是那一堆CALLBACK,因为调用Bean方法时都是通过这些CALLBACK调用包装在代理类里面的真正的对象实例里面的方法(具体调用过程详见org.springframework.aop.framework.CglibAopProxy.DynamicAdvisedInterceptor#intercept这里不详细赘述了)。

以上都是笔者针对观察到的现象去尝试探索本质,虽然上面的探索已解答了笔者心中疑惑,但依然还不算非常底层的探索,更谈不上底层源码解析。因为本文的内容涉及到的最底层是关于Java反射原理及Spring依赖注入原理,而Java及Spring的设计也十分精妙,所以想再深入了解的同学或感兴趣的同学可以自行到各大搜索引擎查找相关文章学习。

参考文章:
https://www.jianshu.com/p/68946d8db139
https://blog.csdn.net/qq_37294047/article/details/136056613

ps:关于IDEA中智能步进(smart step into)的坑

我在上述探索过程中都是使用Jetbrain IDEA进行debug调试来观察变量情况的,但是在这个过程中出现部分Bean并未正常步进到代理类的Callback中(即CglibAopProxy.DynamicAdvisedInterceptor#intercept),部分Bean在步进时却能进到代理类的Callback中(即CglibAopProxy.DynamicAdvisedInterceptor#intercept)的现象,一开始我还怀疑Bean的代理方式是不是不一样或者和类有没有实现接口有关。但最后发现其实Bean都是CGLIB代理而且和被代理类是否有实现接口也无关,出现这种现象并不是代码问题,而是IDEA默认开启smart step into的,导致有时会步进到依赖jar包的classes代码中,有时却不会,直接帮你跳到下一行自己项目代码中。

百思不得其解的笔者最后发现在“运行”(新版IDEA好像叫“服务”)的Threads & Variables中的Frames面板中其实是有显示整个完整的步进过程的。

Threads & Variables中的Frames面板示例:
Threads & Variables中的Frames面板示例
通过Threads & Variables中的Frames面板发现其实都是有进入依赖jar包的classes代码的过程,意味着调用Bean的时候其实都是有步进到代理类的Callback中(即CglibAopProxy.DynamicAdvisedInterceptor#intercept),只是如果你开了智能步进(smart step into),部分情况下会帮你跳过步进到依赖jar包的classes代码的过程,直接帮你跳到下一行自己项目代码中。笔者不了解IDEA智能步进(smart step into)的逻辑也不好评价这个smart step into是否智能,毕竟这个也涉及开发者个人习惯。但IDEA是可以关闭这个智能步进(smart step into)的:
关闭智能步进(smart step into)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2104328.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Keil5如何生成静态库.lib文件

一、这是一个Keil5工程 二、我想把这个文件输出成一个.lib文件&#xff0c;里面有一个加法函数&#xff0c;其它工程想实现加法只需调用这个函数即可 三、因为我只需要输出这一个文件成.lib&#xff0c;所以我需要屏蔽其它文件 1、右键其它文件&#xff0c;点击这个 2、把这个…

欧元承压续跌,黄金市场波动待指引

欧元兑美元延续跌势&#xff0c;季节性因素加剧市场担忧 &#xff0c;欧元/美元汇率继续徘徊在两周低点附近&#xff0c;美元的强势表现以及即将公布的关键经济数据共同施压欧元。市场分析指出&#xff0c;欧元在9月的历史表现普遍不佳&#xff0c;尤其是过去五年连续下跌的记录…

深入理解神经网络:从基础到深度学习

深入理解神经网络&#xff1a;从基础到深度学习 前言1. 神经元模型的构建代码示例&#xff1a;M-P神经元模型 2. 感知机与多层网络的实现代码示例&#xff1a;简单感知机模型 3. 误差逆传播算法&#xff08;BP&#xff09;的实践代码示例&#xff1a;BP算法的简化实现 4. 探索全…

Altium Designer爬虫工具/网页信息获取工具

使用Altium Designer脚本系统的爬虫功能&#xff0c;可以有效的提高数据处理能力&#xff0c;作为新质生产力工具&#xff0c;可以在很多方面使用到爬虫功能或者网页信息读取功能&#xff0c;如一份原理图文档有很多的物料&#xff0c;需要整理这些物料的价格、是否有现货和数据…

HiGPT:异构图语言模型的突破

人工智能咨询培训老师叶梓 转载标明出处 人工智能领域的一大挑战是如何从海量复杂的数据中提取有价值的信息&#xff0c;特别是在处理异构图数据时。异构图由多种类型的节点和边组成&#xff0c;它们之间的相互关系丰富而复杂。传统的图神经网络&#xff08;GNNs&#xff09;在…

2024最新PyCharm下载安装激活汉化教程!(附激活码)

激活码&#xff08;文末附带精品籽料&#xff09;&#xff1a; K384HW36OB-eyJsaWNlbnNlSWQiOiJLMzg0SFczNk9CIiwibGljZW5zZWVOYW1lIjoibWFvIHplZG9uZyIsImxpY2Vuc2VlVHlwZSI6IlBFUlNPTkFMIiwiYXNzaWduZWVOYW1lIjoiIiwiYXNzaWduZWVFbWFpbCI6IiIsImxpY2Vuc2VSZXN0cmljdGlvbiI6I…

【NAT】NAT:实现网络地址转换

目录 什么是NAT&#xff1f;NAT的功能NAT的弊端NAT的工作原理NAT分类SNAT&#xff08;源NAT&#xff09;DNAT&#xff08;目的NAT&#xff09;双向NAT 什么是NAT&#xff1f; NAT是一种在IP数据包传输过程中&#xff0c;将源IP地址或目标IP地址在私有网络和公有网络之间进行转…

芯片设计五部曲之四 | 电磁玄学宗师——射频芯片

去年我们发布的《芯片设计五部曲》&#xff0c;还挺受欢迎的&#xff1a; 芯片设计五部曲之一 | 声光魔法师——模拟IC 芯片设计五部曲之二 |‍ 图灵艺术家——数字IC 芯片设计五部曲之三 | 战略规划家——算法仿真 不少人辗转问过我们下一集什么时候出。 放心&#xff0c;…

U盘不小心格式化了怎么恢复?别慌!教你快速恢复

在日常工作和生活中&#xff0c;U盘已成为我们存储和传输数据的重要工具。然而&#xff0c;有时由于误操作或其他原因&#xff0c;我们可能会不小心格式化U盘&#xff0c;导致重要数据的丢失。这时&#xff0c;如何恢复这些数据就显得尤为重要。下面&#xff0c;我们将介绍几种…

JS笔记

9.3 1.数据类型 1.1.Object 对象 对象&#xff1a;一切皆对象 面向过程&#xff1a;按照时间的发生顺序&#xff0c;从上往下依次执行 对象对象&#xff1a;指挥对象做某件事 1.2.数据类型的检测 1.3.数据类型转换 1.自动转换&#xff1a;js会通过关系运算符&#xff0c…

基于asp.net的简单的留言板管理系统附源码

今天给大家分享一个基于asp.net的留言板管理系统源码&#xff0c;SQLserver数据库&#xff0c;VS开发&#xff0c;具体的模块如下&#xff1a; 需要的盆友可以自己下载哈&#xff0c;链接放在下面了 链接&#xff1a;https://pan.quark.cn/s/12021e583fd7

使用豆包 MarsCode 编程助手,兑换 4090 显卡!

关于豆包 MarsCode 编程助手 豆包 MarsCode 编程助手是一个智能化的编程工具&#xff0c;通过与 AI 的对话&#xff0c;用户可以轻松获取编程建议、解决问题并提高编程效率。无论您是编程新手还是经验丰富的开发者&#xff0c;MarsCode 都能为您提供极具价值的帮助。 点击直达…

三非渣本前端成功转行AI绘画:我的学习历程与心得分享

大家好&#xff0c;我是一名普通的前端程序员&#xff0c;毕业于一所名不见经传的“三非”院校。在编程这条路上&#xff0c;我一直觉得自己平平无奇&#xff0c;直到有一天&#xff0c;我接触到了AI绘画&#xff0c;从此开启了一段全新的转行之旅。以下是我的学习经历、学习路…

vs2022 的wpf应用,需要生成的WpfApp1\bin\Debug\WpfApp1.exe添加图

在Visual Studio 2022中为WPF应用程序的生成的WpfApp1.exe添加图标&#xff0c;您可以按照以下步骤进行&#xff1a; 1. 准备图标文件 图标格式: 需要准备一个.ico格式的图标文件。确保图标文件符合标准的尺寸&#xff08;通常是32x32像素或更大&#xff09;。路径: 将图标文…

mybatis 出错:java.lang.NumberFormatException: For input string: “A“

GetMapping("/generateOldCarModelRanking") ApiOperation("老旧车车型排名") public Result generateOldCarModelRanking(RequestParam(value "year")String year,RequestParam(value "isCommercial")boolean isCommercial ,Reques…

FineBI与FineReport的区别

在企业信息化的浪潮中&#xff0c;数据分析和报表工具的需求日益增加。FineBI与FineReport是帆软公司旗下两款常见的数据分析和报表工具&#xff0c;它们各自有着不同的特点和应用场景。本文将从功能、适用场景和用户体验等方面分析FineBI与FineReport的区别。 一、功能对比…

【漏洞复现】蜂信物联 FastBee 开源物联网平台 download 任意文件读取漏洞

【漏洞复现】蜂信物联 FastBee 开源物联网平台 download 任意文件读取漏洞、 01 漏洞描述 蜂信物联 FastBee 开源物联网平台 download 接口存在任意文件读取漏洞&#xff0c;未经身份验证攻击者可通过该漏洞读取系统重要文件&#xff08;如数据库配置文件、系统配置文件&…

python中.之后的圈c、圈v分别代表什么意思?

python中.之后的圈c、圈v分别代表什么意思&#xff1f; Python中&#xff0c;.之后的圈c表示类的实例方法&#xff0c;而圈v表示类的成员变量。 在面向对象编程中&#xff0c;类是一种抽象的数据类型&#xff0c;实例方法是定义在类中的函数&#xff0c;用于操作类的实例变量…

C++11新增特性:列表初始化(std::initializer_list) decltype、auto、nullptr、范围for

C11新增特性&#xff1a;列表初始化&#xff08;std::initializer_list&#xff09;& decltype、auto、nullptr、范围for 一、C11新增统一初始化方式1.1 新增方式1.2 初始化容器底层原理&#xff08;std::initializer_list&#xff09; 二、新增声明2.1 decltype2.3 auto &…

零基础 Tomcat 环境搭建(图解)

目录 介绍 下载 安装 环境配置 启动服务 无法正常启动的可以去更改端口号 显示乱码的可以去更改为BGK 正常启动 ​编辑 浏览器URL测试 关闭服务 静态项目测试 创建部署 创建任意文件夹 编写测试内容 浏览器URL测试 介绍 Tomcat是Apache 软件基金会&#xff08…