likes
comments
collection
share

Spring好坑!为什么代理对象的属性没有值?先看代码: 关键点: 加了@Transactional,所以ZhouyuS

作者站长头像
站长
· 阅读数 33

先看代码:

@Service
@Transactional
public class ZhouyuService {

    private String name = "zhouyu";

    public final void test() {
        System.out.println(name);
    }
}

关键点:

  1. 加了@Transactional,所以ZhouyuService会生成代理对象作为Bean对象
  2. name属性有默认值“zhouyu”
  3. 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底层原理的同学应该会想到,代理对象执行方法时,逻辑是这样的:

  1. 代理对象先执行自己的test()方法,从而执行切面逻辑
  2. 然后执行被代理对象的test()方法,从而执行原本逻辑

代理对象对应的是代理类,是ZhouyuService$EnhancerBySpringCGLIB$f4bc73d9类 被代理对象对应的是被代理类,就是ZhouyuService类

一般情况下:

  1. 代理类的父类是被代理类
  2. 代理类会重写父类里面被代理的方法,比如test()方法
  3. 代理类会在自己的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来解析一下这个技术: Spring好坑!为什么代理对象的属性没有值?先看代码: 关键点: 加了@Transactional,所以ZhouyuS

假如,我们用Objenesis来创建一个对象,并打印name属性:

Objenesis objenesis = new ObjenesisStd();
ZhouyuService zhouyuService = objenesis.newInstance(ZhouyuService.class);
System.out.println(zhouyuService.name);

结果为null,因为使用Objenesis创建对象根本就没有走属性初始化这一步。

而Spring AOP里默认就会用这个技术,对应的类为ObjenesisCglibAopProxy,关键代码为: Spring好坑!为什么代理对象的属性没有值?先看代码: 关键点: 加了@Transactional,所以ZhouyuS

通过上面的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()方法中了。

分析了这么多,是不是有点晕了,最后,我再来给大家梳理一下:

  1. Spring会用cglib来创建代理类,会用Objenesis来创建代理对象,因此不会初始化代理对象中的属性,这是可以理解的,因为代理对象的作用是去代理方法,而不是代理属性,所以代理对象不关心属性,使用Objenesis可以更快的创建代理对象,但是会导致代理对象中的属性为null
  2. 如果方法加了final,那么就不能被代理到,导致打印的是代理对象的name属性,如果不是final,就被代理到了,导致打印的是被代理对象的name属性
  3. final的属性很有可能会被编译内联到方法中

以上,正式结束,非常感谢我的一位学员,是他发现并一起解决了这个问题。

关注我,我是大都督周瑜,我的公众号:IT周瑜。

转载自:https://juejin.cn/post/7403187632836526117
评论
请登录