前天遇到一个很奇怪的问题,在Service的PostConstruct里修改了公有成员变量的值,但获取的时候还是初始值。
服务类定义大概是这样的:
@Service
@Slf4j
public class PaymentService{
public int payDurationSec;
@PostConstruct
public void init(){
log.info("初始化前值:{}", payDurationSec);
payDurationSec = 900;
log.info("初始化后值:{}", payDurationSec);
}
@Transactional(rollbackFor = Exception.class)
public void business(){
//业务代码
}
}
调用方代码:
@Service
@Slf4j
public class MyCaller{
@Autowired
private MyService myService;
public boolean pay(int currentTime, int createTime){
log.info("调用方取值:{}", myService.payDurationSec)
boolean timeout = (currentTime - createTime) > myService.payDurationSec;
if(timeout){
return false;
}else{
//业务代码
}
}
}
项目启动后,手动调用接口触发pay方法,查看info日志。
日志输出:
初始化前值:0
初始化后值:900
调用方取值:0
我们知道Spring默认是单例模式的,这个问题看着非常奇怪,就像是变量被人偷偷改了一样。因为全文搜索已经没有地方引用这个变量了,还怀疑过是有人通过反射修改了这个值。
尝试解决
起初,看到这篇文章,以为这个问题和多线程有关:
https://blog.csdn.net/zhanyu1/article/details/82634376
由于是手动调用的,不存在两个线程并发冲突的问题。
推测这个问题是由于两个线程各自保留了自己的数据副本。
于是在payDurationSec
上添加volatile
关键字:
public volatile int payDurationSec;
结果照旧,排除这个可能。
尝试给变量添加Get方法:
@Getter
public int payDurationSec;
并且调用方使用get方法获取值:
log.info("调用方取值:{}", myService.getPayDurationSec())
boolean timeout = (currentTime - createTime) > myService.getPayDurationSec();
info日志:
初始化前值:0
初始化后值:900
调用方取值:900
问题消失。因为比较晚了,后来也不了了之了。
同事再战怪Bug
今天同事在自己搭建的环境尝试重现这个问题。
同事首先是在出问题的代码添加一个方法:
@Service
@Slf4j
public class PaymentService{
public int payDurationSec;
//添加了这个方法
public void testCheck(PaymentService me){
if(this != me){
log.error("我不是我");
}
}
@PostConstruct
public void init(){
log.info("初始化前值:{}", payDurationSec);
payDurationSec = 900;
log.info("初始化后值:{}", payDurationSec);
}
@Transactional(rollbackFor = Exception.class)
public void business(){
//业务代码
}
}
然后调用
@Service
public class Test{
@Autowired
private PaymentService paymentService;
public void test(){
paymentService.testCheck(paymentService);
}
}
error日志竟然输出了
我不是我
深入讨论之后,想到可能是Spring的代理机制在作怪。输出paymenService的类型,发现调用方注入的确实是一个Spring生成的代理类。
解释
猜想是这样的,Spring在使用了@Transactional
注解的类上,会创建一个代理类,可能是通过继承原有类来实现的。类似这样:
public class PaymentServiceProxy extends PaymentService{
private PaymentService originObj;
//还有一个继承而来的成员:
//public int payDurationSec;
@Override
public int getPayDurationSec(){
return originObj.getPayDurationSec();
}
@Override
public void init(){
originObj.init();
}
@Override
public void business(){
try{
newTransaction();
originObj.business();
commit();
}catch(Excetion e){
rollback();
throw e;
}
}
}
Spring分别初始化了PaymentService
和PaymentServiceProxy
,而且在调用方注入的时候注入了PaymentServiceProxy
。
这样,如果调用get方法,代理会将方法调用转发给被代理对象,结果上就像是直接访问被代理对象一样。被代理对象内部的修改也能体现到代理对象的调用结果上。
但是,当调用方直接访问公有变量时,代理无法监测用户的调用行为,也无法将请求转发给被代理对象,如果Spring的Proxy实现是向我这样用继承实现的话,获取到的公有变量就是代理对象自己的那一份,被代理对象就算自己修改了自己的公有变量,也无法反映到外部。
由于对Spring代理机制不熟悉,这个解释还只算是一个理论。要找时间深入学习一下Spring代理机制才行。
如果这个说法成立的话,那就可以提出两点行动建议了:
- 不要在Spring中使用公有变量(除非是静态的),而是提供getter,setter方法(大家总算知道getter、setter到底有什么用了吧)。
- 如果你头铁,硬要用公有变量,确保你没有诱发Spring创建代理的用法(比如@Transactional)
体会
- 要解决问题,还是要深入了解框架和工具的原理,不能只想着会怎么用就行了,像这个问题,百度都没有找到答案
- JavaBean的Getter,Setter算是一种规范了,这些标准用法还是要遵守,不能想当然地就用一些很浪的方式实现
- 不要回避问题不了了之,要尝试重现bug,如果不是同事的钻研精神,我也不会对这个问题有什么深入的思考,就算对Spring原理有深入了解,没有与实际经验结合,哪里有成长?