Spring动态代理奇怪的空指针异常,字段明明不为空,但方法中获取字段的值确是空的

原创 吴就业 95 0 2020-05-28

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/caa1fc0d2cc14e59bd82cf4246c47214

作者:吴就业
链接:https://wujiuye.com/article/caa1fc0d2cc14e59bd82cf4246c47214
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

笔者在新的定时任务项目中,限定一个类只能写一个Job,类似于写脚本,一个Job一个脚本。对于简单的任务我们并不约定一定要有Service层,但在Job中我们可能需要将某些数据库操作放到事务中执行,为让注解事务生效,我们不能直接使用this调用事务方法。

调用本类事务方法有两种方式可以让注解事务生效:

场景描述

假设现有类A,在类AmethodA方法中,先从Springbean工厂获取到类A的实例,再调用类AmethodB方法,这样做的目的是使事务生效。

代码如下:

@Component
public class ProxyObjFieldNpe {

    @Value("${field_value}")
    private String fieldValue;

    public void methodA() {
        if (fieldValue == null) {
            System.out.println("methodA NPE...");
        }
        // 从bean工厂取,使AOP生效
        ProxyObjFieldNpe thisRef = OnionXxlJobApplicationContent.getBean(ProxyObjFieldNpe.class);
        // ......调用某些事务方法
        thisRef.methodB();
    }

    private void methodB() {
        if (fieldValue == null) {
            System.out.println("methodB NPE...");
        }
    }

}

外部调用methodA方法,结果输出的是:"methodB NPE...",显然,这是methodA调用了methodB,然后是methodB输出的。

为什么methodA方法获取到fieldValue字段的值不为空,而methodB方法获取到的fieldValue却为空呢?这就是笔者遇到的问题。

笔者在实际项目中调试的结果截图如下,实际项目中Job的类名为AutoCloseTimeoutOrderJob

img

图中AutoCloseTimeoutOrderJob实例的字段都为空,这些字段都是声明自动注入的MapperService,不可能为空。但从调试结果我们可以看出,从bean工厂获取到的是AutoCloseTimeoutOrderJob的代理类的实例,而不是AutoCloseTimeoutOrderJob实例。因为代理类实例是不会为字段赋值的,只会代理父类的方法,所以这些字段就是NULL了。

回到ProxyObjFieldNpe的例子中,我们从bean工厂获取到的是ProxyObjFieldNpe的代理对象,该代理对象继承ProxyObjFieldNpe。因此,与上面截图一样,代理对象的字段都会是NULL。当外部调用ProxyObjFieldNpemethodA方法,实际调用的是代理类的methodA方法,而代理类的methodA方法会调用父类的methodA方法,所以methodA方法拿到字段的值非空。

但是methodA中中调用methodB不是this.调用,而是再次从bean工厂获取ProxyObjFieldNpe的代理对象调用,而methodB方法是private方法,代理类没办法重写该方法,而是直接抄了该方法,代理对象的methodA中调用methodB法,调用的是代理对象的methodB方法,并不会调用父类的methodB方法,所以methodB中拿任何字段的值都会是空的。

为什么代理对象能调用父类的private方法?

因为调用访问标志为privatemethodB方法是在ProxyObjFieldNpe类的methodA方法中调用的,而不是在代理类的methodA方法中调用的,内部调用所以有访问权限。

代理类继承ProxyObjFieldNpe,外部调用代理类的methodA方法时,最终经过方法拦截器调用代理类父类的methodA方法,因此methodA方法中调用methodB方法实际上是在父类(ProxyObjFieldNpe)中调用的。

ProxyObjFieldNpemethodA方法编译后生成的字节码如下(部分):

   15: ldc           #6    // class com/wujiuye/test/ProxyObjFieldNpe
   17: invokestatic  #7    // Method com/wujiuye/test/OnionXxlJobApplicationContent.getBean:(Ljava/lang/Class;)Ljava/lang/Object;
   20: checkcast     #6    // class com/wujiuye/test/ProxyObjFieldNpe
   23: astore_1
   24: aload_1
   25: invokespecial #8    // Method com/wujiuye/test/ProxyObjFieldNpe.methodB:()V

为什么代理对象的字段为NULL?

如果熟悉Spring Bean生命周期,那么就不难理解。

bean的创建过程如下:

代理对象是在上述步骤的第六步创建的,即调用某个BeanPostProcessorpostProcessAfterInitialization方法之后,返回代理对象,如果是单例对象,则会将该对象保存到bean工厂(容器)中。也就是说,bean工厂中存储的是代理对象。

下面两张图是我在项目中调试Spring代码的截图。(图中的小红点下方有个问号,这是条件断点,只有满足条件时才会停在断点处。条件的设置可右击小红点,在弹出框中输出条件,条件的编写与在代码中添加一个if语句是一样的。)

img

在调用BeanPostProcessorpostProcessAfterInitialization方法之前, bean还是原生的bean

img

在调用BeanPostProcessorpostProcessAfterInitialization方法之后,bean已经变成代理对象了。

因此,使用cglib生成的代理对象 (继承方式),在父类中,通过代理对象调用父类私有方法不会报错,但字段都是空的。

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

Spring Cloud kubernetes入门项目sck-demo

本篇我们将从一个简单的demo上手Spring Cloud kubernetes,当然,我们只用到Spring Cloud kubernetes的服务注册与发现、配置中心模块。

为什么要选择Spring Cloud Kubernetes?

选择Spring Cloud Kubernetes意味着我们想要将服务部署到Kubernetes集群,Spring Cloud Kubernetes为我们实现了Spring Cloud的一些接口,让我们可以快速搭建Spring Cloud微服务项目框架,并能使用Kubernetes云原生服务。

Spring Cloud Kubernetes入门必知运维知识之Kubernetes

作为开发者,只有足够了解容器技术,才能做好技术选型,以及开发部署在Kubernetes容器服务之上的应用应该要注意哪些问题。如果运维不了解代码,开发也不了解Kubernetes,谁能解决将服务迁移到Kubernetes上遇到的各种问题呢?

Java反序列化JSON,要避免泛型的类型擦除问题

如图,反序列化JSON数组正常,却在获取数组元素时抛出了类型转换异常。

基准测试框架JMH快速上手

基准测试Benchmark是测量、评估软件性能指标的一种测试,对某个特定目标场景的某项性能指标进行定量的和可对比的测试。

如何获取泛型类的参数化类型?解密Java泛型

框架怎么知道这个`T`到底是什么类型呢?