先看代码:
@Service
@Transactional
public class ZhouyuService {
private String name = "zhouyu";
public final void test() {
System.out.println(name);
}
}
关键点:
- 加了@Transactional,所以ZhouyuService会生成代理对象作为Bean对象
- name属性有默认值“zhouyu”
- test()方法为final
现在,通过Spring容器获取ZhouyuService的Bean对象,并执行test方法打印name属性:
ConfigurableApplicationContext applicationContext = SpringApplication.run(Main.class, args);
ZhouyuService zhouyuService = applicationContext.getBean(ZhouyuService.class);
zhouyuService.test();
问题来了,test()方法打印出来的name属性值为:null !
是不是不敢相信,不信你可以自己在电脑上试试,一开始我也不信,name属性有默认值啊,怎么会为null呢?
熟悉AOP底层原理的同学应该会想到,代理对象执行方法时,逻辑是这样的:
- 代理对象先执行自己的test()方法,从而执行切面逻辑
- 然后执行被代理对象的test()方法,从而执行原本逻辑
代理对象对应的是代理类,是ZhouyuService$EnhancerBySpringCGLIB$$f4bc73d9类
被代理对象对应的是被代理类,就是ZhouyuService类
一般情况下:
- 代理类的父类是被代理类
- 代理类会重写父类里面被代理的方法,比如test()方法
- 代理类会在自己的test()方法中,执行切面逻辑,并执行被代理对象的test()方法,被代理对象就是一个ZhouyuService对象
因此,当代理对象执行test()方法时,最终仍然会执行被代理对象的test()方法,从而打印被代理对象的name属性。
如果是以上流程,那么打印出来的name应该是有值的。
但是,上面的代码中,test()方法前面加了final,表示不能被子类重写,因此代理类中是没有test()方法的,代理对象执行的test()方法,并不是自己的test()方法,也就是不会执行切面逻辑,也就是事务会失效。
但是,自己没有test()方法,父类有啊,所以,代理对象实际上执行的是ZhouyuService类里的test()方法,从而打印name属性,但是打印的是代理对象的name属性,再由于ZhouyuService中的name属性为private,因此代理类中也没有继承该属性,因此代理对象中name属性为null,这是正常的。
以上的分析没有问题,可是,如果我把name属性改成public呢?那代理类就可以继承name属性了吧,那应该就能打印出来值了吧?
震惊的地方就在这里,打印出来的仍然是:null !
不理解了吧,子类继承父类里面的public属性,这不是天经地义的吗?
这里面的魔鬼在于Objenesis,第一次听说这个技术?让GPT来解析一下这个技术:
假如,我们用Objenesis来创建一个对象,并打印name属性:
Objenesis objenesis = new ObjenesisStd();
ZhouyuService zhouyuService = objenesis.newInstance(ZhouyuService.class);
System.out.println(zhouyuService.name);
结果为null,因为使用Objenesis创建对象根本就没有走属性初始化这一步。
而Spring AOP里默认就会用这个技术,对应的类为ObjenesisCglibAopProxy,关键代码为:
通过上面的Spring AOP源码,发现其实可以通过开关来关闭使用Objenesis,这个开关是-Dspring.objenesis.ignore=true
,设置为true,Spring AOP就不会使用Objenesis来创建代理对象了。
因此,我们把这个开关加上,重新回到上面让我们震惊的场景中进行测试,就能发现name属性有值了。
因此,我们上面分析的代理对象执行方法的流程并没有问题,代理类肯定会继承父类的name属性,只是代理对象在创建时默认使用的是Objenesis,创建出来的对象根本就没有对属性做初始化,所以最终name属性为null,不使用Objenesis就正常了。
好了,分析到这里文章其实可以结束了,但是,再给大家一个彩蛋。
我们把刚刚的Objenesis开关再去掉,也就是还是让Spring使用Objenesis,只不过,我们把name属性改为final。
你会发现,最终打印出来的name属性还是有值的,并不是null,这又是为啥?不是说用Objenesis创建的对象不会初始化属性吗?难道会初始化final的属性?
没有这种说法,没有说只初始化final的属性,而不初始化非final的属性,我们不妨看看现在的ZhouyuService:
@Service
@Transactional
public class ZhouyuService {
public final String name = "zhouyu";
public final void test() {
System.out.println(name);
}
}
仔细看看,不知道大家能不能分析出原因?如果分析出来了,记得给文章点个赞之后,就可以离开了。
如果没分析出来,那就看看编译后的ZhouyuService:
@Service
@Transactional
public class ZhouyuService {
public final String name = "zhouyu";
public ZhouyuService() {
}
public final void test() {
System.out.println("zhouyu");
}
}
明白了吗?点赞了吗?
甚至,你现在debug去看ZhouyuService代理对象,会发现debug会显示name属性为null,但是最终test()方法却能打印出来“zhouyu”。
因为,ZhouyuService代理对象的name属性确实没有值,没有值的原因就是Objenesis,test()方法之所以能打印出来值,是因为编译优化,直接将name属性的值内联到test()方法中了。
分析了这么多,是不是有点晕了,最后,我再来给大家梳理一下:
- Spring会用cglib来创建代理类,会用Objenesis来创建代理对象,因此不会初始化代理对象中的属性,这是可以理解的,因为代理对象的作用是去代理方法,而不是代理属性,所以代理对象不关心属性,使用Objenesis可以更快的创建代理对象,但是会导致代理对象中的属性为null
- 如果方法加了final,那么就不能被代理到,导致打印的是代理对象的name属性,如果不是final,就被代理到了,导致打印的是被代理对象的name属性
- final的属性很有可能会被编译内联到方法中
以上,正式结束,非常感谢我的一位学员,是他发现并一起解决了这个问题。
关注我,我是大都督周瑜,我的公众号:IT周瑜。