转载

神奇的空指针异常,动态代理惹的祸?

笔者在重构定时任务项目时,限定了一个类只能写一个 Job ,类似于写脚本,一个 Job 一个脚本。对于简单的任务我们并不约定一定要有 Service 层。

Job 中可能需要将某些数据库操作放到事务中执行,为了让注解事务生效,我们不能直接使用 this 调用事务方法。有两种方式可以让事务生效,一是通过在类中注入自己,也就是循环依赖注入,二是在需要时再从 bean 工厂中获取 bean

场景描述

假设现有类 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 方法: proxyObjFieldNpe.methodA();

结果输出的是: "methodB NPE..."

为什么 methodA 方法获取到 fieldValue 字段的值不为空,而 methodB 方法获取到的 fieldValue 却为空呢?这就是笔者遇到的问题。细心的朋友,你有没有看出原因呢?

实际项目中调试的结果截图如下:

神奇的空指针异常,动态代理惹的祸?

图中 AutoCloseTimeoutOrderJob 实例的字段都为空,这些字段都是自动注入的 MapperService ,不可能为空。但从调试结果我们可以看出,从 bean 工厂获取到的是 AutoCloseTimeoutOrderJob 的代理对象,并非 AutoCloseTimeoutOrderJob

ProxyObjFieldNpe 的例子中,我们从 bean 工厂获取到的也是 ProxyObjFieldNpe 的代理对象,该代理对象继承 ProxyObjFieldNpe ,因此 thisRef.methodB(); 实际调用的是代理类父类的 methodB 方法。与上面截图一样,代理对象的字段都是 NULL ,这就是 methodA 方法获取到 fieldValue 字段的值不为 NULL ,而 methodB 方法获取到 fieldValue 字段的值为 NULL 的原因。

外部调用 ProxyObjFieldNpemethodA 方法也是调用其代理类的 methodA 方法,为什么 methodA 方法没有问题但调用 methodB 方法有问题?因为 methodB 方法被声明为 private 了。

那么问题来了:

  • 为什么 methidB 方法的访问标志是 private ,代理对象是 ProxyObjFieldNpe 的子类,却能调用其父类的 methidB 方法?
  • 为什么代理对象的字段为 NULL ?

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

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

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

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
复制代码

偏移量为 151720 三条指令是:从 bean 工厂获取代理 bean ,并使用 checkcast 指令将代理对象类型强制转为父类类型。偏移量为 2425 两条字节码实现调用 methodB 方法,非静态方法的第一个隐式参数为 this 引用,此处传的是代理类对象的引用,因此在 methodB 方法中,使用 this (代理对象的引用)获取到的字段都是空的。

问题二:为什么代理对象的字段为NULL?

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

bean 的实例化过程如下:

  • 1、反射创建 bean ;
  • 2、为 bean 注入属性;
  • 3、调用 *Aware 接口的方法;
  • 4、调用 BeanPostProcessorpostProcessBeforeInitialization 方法;
  • 5、调用初始化方法, afterPropertiesSet 或自定义的初始化方法;
  • 6、调用 BeanPostProcessorpostProcessAfterInitialization 方法;

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

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

神奇的空指针异常,动态代理惹的祸?

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

神奇的空指针异常,动态代理惹的祸?

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

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

END

熟悉 Spring 源码的好处:没有解决不了的问题!没错,笔者又骗大家去看框架源码了。

我的学习方法:不要求自己懂得多,但一定要懂得深!像 Spring Cloud 这些,我也就看看书,没有去深入学习过,甚至现在都不会用,因为工作中没用到,只是先了解一下。而像 Dubbo ,以前项目中用到,为了解决一些问题,啃完源码。但是光有深度没有广度也不行,道阻且长...

原文  https://juejin.im/post/5ebb9c48e51d454dd5062f3c
正文到此结束
Loading...