笔者在新的定时任务项目中,限定一个类只能写一个Job
,类似于写脚本,一个Job
一个脚本。对于简单的任务我们并不约定一定要有Service
层,但在Job
中我们可能需要将某些数据库操作放到事务中执行,为让注解事务生效,我们不能直接使用this
调用事务方法。
调用本类事务方法有两种方式可以让注解事务生效:
- 一是通过在类中注入自己,也就是循环依赖注入;
- 二是在需要时再从
bean
工厂中获取bean
;
场景描述
假设现有类A
,在类A
的methodA
方法中,先从Spring
的bean
工厂获取到类A
的实例,再调用类A
的methodB
方法,这样做的目的是使事务生效。
代码如下:
@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
:
图中AutoCloseTimeoutOrderJob
实例的字段都为空,这些字段都是声明自动注入的Mapper
与Service
,不可能为空。但从调试结果我们可以看出,从bean
工厂获取到的是AutoCloseTimeoutOrderJob
的代理类的实例,而不是AutoCloseTimeoutOrderJob
实例。因为代理类实例是不会为字段赋值的,只会代理父类的方法,所以这些字段就是NULL
了。
回到ProxyObjFieldNpe
的例子中,我们从bean
工厂获取到的是ProxyObjFieldNpe
的代理对象,该代理对象继承ProxyObjFieldNpe
。因此,与上面截图一样,代理对象的字段都会是NULL
。当外部调用ProxyObjFieldNpe
的methodA
方法,实际调用的是代理类的methodA
方法,而代理类的methodA
方法会调用父类的methodA
方法,所以methodA
方法拿到字段的值非空。
但是methodA中
中调用methodB
不是this.
调用,而是再次从bean
工厂获取ProxyObjFieldNpe
的代理对象调用,而methodB
方法是private
方法,代理类没办法重写该方法,而是直接抄了该方法,代理对象的methodA
中调用methodB
法,调用的是代理对象的methodB
方法,并不会调用父类的methodB
方法,所以methodB
中拿任何字段的值都会是空的。
为什么代理对象能调用父类的private方法?
因为调用访问标志为private
的methodB
方法是在ProxyObjFieldNpe
类的methodA
方法中调用的,而不是在代理类的methodA
方法中调用的,内部调用所以有访问权限。
代理类继承ProxyObjFieldNpe
,外部调用代理类的methodA
方法时,最终经过方法拦截器调用代理类父类的methodA
方法,因此methodA
方法中调用methodB
方法实际上是在父类(ProxyObjFieldNpe
)中调用的。
ProxyObjFieldNpe
的methodA
方法编译后生成的字节码如下(部分):
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
- 偏移量为
15
、17
、20
三条指令是:从bean
工厂获取代理bean
,并使用checkcast
指令将代理对象类型强制转为父类类型。 - 偏移量为
24
、25
两条字节码实现调用methodB
方法,非静态方法的第一个隐式参数为this
引用,此处传的是代理类对象的引用,因此在methodB
方法中,使用this
(代理对象的引用)获取到的字段都是空的。
为什么代理对象的字段为NULL?
如果熟悉Spring Bean
生命周期,那么就不难理解。
bean
的创建过程如下:
- 1、反射创建
bean
; - 2、为
bean
注入属性; - 3、调用
*Aware
接口的方法; - 4、调用
BeanPostProcessor
的postProcessBeforeInitialization
方法; - 5、调用初始化方法,
afterPropertiesSet
或自定义的初始化方法; - 6、调用
BeanPostProcessor
的postProcessAfterInitialization
方法;
代理对象是在上述步骤的第六步创建的,即调用某个BeanPostProcessor
的postProcessAfterInitialization
方法之后,返回代理对象,如果是单例对象,则会将该对象保存到bean
工厂(容器)中。也就是说,bean
工厂中存储的是代理对象。
下面两张图是我在项目中调试Spring
代码的截图。(图中的小红点下方有个问号,这是条件断点,只有满足条件时才会停在断点处。条件的设置可右击小红点,在弹出框中输出条件,条件的编写与在代码中添加一个if
语句是一样的。)
在调用BeanPostProcessor
的postProcessAfterInitialization
方法之前, bean
还是原生的bean
。
在调用BeanPostProcessor
的postProcessAfterInitialization
方法之后,bean
已经变成代理对象了。
因此,使用cglib
生成的代理对象 (继承方式),在父类中,通过代理对象调用父类私有方法不会报错,但字段都是空的。