问题起源
笔者最近在做一个功能,使用了工厂模式/策略模式设计的,定义了一个接口,下面有多种实现并通过@Component注解定义为Bean,在运行时根据不同的业务调用不同实现的Bean,所以需要在运行时动态获取Bean。因此,笔者尝试了好几种方法动态获取Bean,最后经过实践,采用以下方式动态获取Bean:
@Autowired
private ApplicationContext applicationContext;
applicationContext.getBeansOfType(<接口>.class)
但在开发的过程中通过IDEA的debug功能,发现从applicationContext中获取的Bean中的成员变量都是null,而且莫名其妙多了很多"CGLIBS$CALLBACK_"+数字的成员变量。如图:
一开始我还以为是我拿错了,没拿到正确的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代理呢?
带着这个疑问我又在搜索引擎咔咔一顿查,最后发现原来:
- Spring 5.x 中 AOP 默认依旧使用 JDK 动态代理。
- SpringBoot 2.x 开始,为了解决使用 JDK动态代理可能导致的类型转化异常而默认使用 CGLIB。
- 在 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代理的原因。
探索
既然知道了:
- Springboot2.x中依赖注入的Bean都是CGLIB代理
- 为什么是CGLIB代理而不是JDK动态代理的原因
那么通过搜索引擎就能找到很多关于Bean以及CGLIB代理的原理。基于Spring中CGLIB代理类生成的原理及过程就可以回答最初的疑问了:
-
Spring通过reflectionFactory.newConstructorForSerialization生成被代理类的子类时是不会对成员变量初始化的,所以生成的代理类中的被代理类的成员属性都是null。
-
而真正的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面板发现其实都是有进入依赖jar包的classes代码的过程,意味着调用Bean的时候其实都是有步进到代理类的Callback中(即CglibAopProxy.DynamicAdvisedInterceptor#intercept),只是如果你开了智能步进(smart step into),部分情况下会帮你跳过步进到依赖jar包的classes代码的过程,直接帮你跳到下一行自己项目代码中。笔者不了解IDEA智能步进(smart step into)的逻辑也不好评价这个smart step into是否智能,毕竟这个也涉及开发者个人习惯。但IDEA是可以关闭这个智能步进(smart step into)的: