这个 bug 让我更加理解 Spring 单例了

谁还没在 Spring 里栽过跟头呢,从哪儿跌倒,就从哪儿睡一会儿,然后再爬起来。

讲点儿武德

这是由一个真实的 bug 引起的,bug 产生的原因就是忽略了 Spring  Bean 的单例模式。来,先看一段简单的代码。

public class TestService {private String callback = "https://ip.com/token={token}";public String getCallback() {Random random = new Random();int number = random.nextInt(100);System.out.println("本次随机数为:" + number);callback = callback.replace("{token}", String.valueOf(number));return callback;}public static void main(String[] args) {TestService testService = new TestService();while (true) {Scanner reader = new Scanner(System.in);int number = reader.nextInt();if (number > 0) {String url = testService.getCallback();System.out.println(url);}}}
}

callback是一个带有一个回调地址,参数 token是不确定的。

getCallback方法每次调用,会随机生成一个100以内的数字,然后将 callback中的{token}替换为这个随机数字,最后的格式就像这样的:

https://ip.com/token=88

然后在 main方法中接收控制台输入,每次输入的数字大于0,调用 getCallback方法,然后输出 url。

相信各位都能轻易的看出这段程序的输出。

执行程序之后,不管你输入多少次数字,最后输出的 callback都是第一次的那个。

虽然每次生成的随机数都变了,但是 callback没变。

其实就是单例

有同学说,你过分了啊,这我能不知道为啥吗?

main方法只创建了一个TestService实例,在第一次调用 getCallback方法的时候,callback这个字符串就被修改成 https://ip.com/token=89了,所以,之后不管你再调用多少次,都不会执行 replace动作了,因为 callback中已经没有 {token}这一段了。

TestService 在整个程序执行过程中就是一个单例,所以,在 callback第一次被修改后,后面再执行

callback.replace("{token}", String.valueOf(number));

的动作,拿到的 callback中就已经没有 {token}了,所以说,不会有替换的动作。

当然,这只是用最简单的程序说明单例中的这个问题,真正的项目中想用单例的话,还要借助于单例设计模式实现。

回到那个 bug

有个弟弟在做微信服务号的开发,微信服务号或者订阅号中有个 access_token的概念,这是所有请求的凭证,有效期 2 个小时,到期之前要进行刷新。

他是这样设计的,在项目启动的时候立即调用微信接口获取 access_token,然后写了一个定时任务每1个小时刷新一次,获取来的 access_token放到 redis 和 数据库中,当调用微信服务号其他接口的时候,在 redis 中获取 access_token并拼接到接口地址中。

开发调试的时候一起顺利,看上去非常完美。

问题出现了

当项目部署到测试环境测试的时候,问题出现了。项目刚发版的时候,测试都正常,但是过一段时间,就会出现错误,查看日志的时候,发现是微信服务号的接口返回了错误码,意思就是 access_token已过期,需要重新获取。

弟弟第一时间怀疑是定时任务出现了问题,但是通过日志和数据库中的更新时间,发现定时任务是完全没有问题的,刷新 access_token的时间和定时任务是完全吻合的,说明已经及时刷新了。

我让他用 redis 或数据库中的access_token去调一下服务号接口,看看是不是也有同样的过期问题。

结果一试,redis 中存的是没问题的,可以正常使用。

那彻底排除是定时任务的问题了,问题的症结应该就出在两个地方:

1、在获取 redis 中的access_token的过程;

2、将获取到的 access_token拼接到请求接口 URL 上发生了错误;

到这里就很好判断了,他把从 redis 拿到的access_token和最后拼接好的 URL 都输出到日志中一看,果然,两个是不一致的。

从 redis 取出的确实是最新可用的 access_token ,但是拼接到接口 URL 上之后,发现是另外一个。那就确定是拿到的 access_token 是没问题的,但是最后拼接到 URL 却有问题。这时,弟弟仔细检查了代码,然后彻底蒙了。

讲点武德

既然问题出在哪儿已经确定了,那就分析那段代码就好了。

项目整体采用的是 Spring Boot,代码很简单,就是在一个 Controller 中调用 Service 中的一个方法。大致 demo 是这样的。

@RestController
@RequestMapping(value = "test")
public class TestController {@Autowiredprivate TestService testService;@GetMapping(value = "call")public Object getCallback() {return testService.getCallback();}
}@Service
public class TestService {private String callback = "https://ip.com/token={token}";public String getCallback() {Random random = new Random();int number = random.nextInt(100);System.out.println("本次随机数为:" + number);callback = callback.replace("{token}", String.valueOf(number));return callback;}
}  

看到这里,各位肯定已经发现问题原因了。虽然有多次请求,但因为 Spring Bean 默认是单例模式,所以实际上和前面演示的那个控制台程序是类似的,从头到尾都只有一个 TestService 实例,所以只有第一次能将{token}替换成真正的access_token

对应到实际的服务号场景中,在第一次调用这个接口时,从 redis 拿到 access_token拼接到具体的 URL中是没问题的,但是一旦这个access_token过期(1小时后),再次请求这个接口就会出现 access_token过期的问题。

这里违反了 Spring 单例模式的一个点,那就是 Spring 单例模式,不适合存储有状态的值,比如这里的 callback就是个有状态的值,它应该随着定时任务的进行,获取到不同的值。

关于 Spring 或 Spring Boot 工作流程的介绍可以阅读文末的两篇文章,其中包括 Bean 实例化过程。

修改建议

如何解决这个问题呢?

其实很简单,不让callback每次调用发生变化就可以了,每次拼接 URL 的时候,先将 callback赋给一个局部变量,然后在这个变量上操作就好了。

public String getCallback() {Random random = new Random();int number = random.nextInt(100);System.out.println("本次随机数为:" + number);String tempCallback = callback;tempCallback = tempCallback.replace("{token}", String.valueOf(number));return tempCallback;
}

另外,说到 Spring 单例模式,Spring 本身还支持其他几种模式,与单例模式对应的就是 prototype模式,这种模式是每个请求都重新生成实例。所以,如果你确定这个 Controller 和 Service 可以不用单例模式,可以加上 @Scope(value = "prototype")注解。

@RestController
@RequestMapping(value = "test")
@Scope(value = "prototype")
public class TestController {@Autowiredprivate TestService testService;@GetMapping(value = "call")public Object getCallback() {return testService.getCallback();}
}@Service
@Scope(value = "prototype")
public class TestService {private String callback = "https://ip.com/token={token}";public String getCallback() {Random random = new Random();int number = random.nextInt(100);System.out.println("本次随机数为:" + number);callback = callback.replace("{token}", String.valueOf(number));return callback;}
}

这样一来,每次都是新的实例,自然就不存在那个问题了。


往期推荐

SpringBoot接口幂等性实现的4种方案!

2021-02-01

Google 开源的依赖注入库,比 Spring 更小更快!

2021-02-08

Docker部署SpringBoot的两种方法,后一种一键部署超好用!

2021-01-19

关注我↓↓↓,每天收获干货.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544974.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

mysql优化--叶金荣老师讲座笔记

copy to tmp table执行ALTER TABLE修改表结构时建议:凌晨执行Copying to tmp table拷贝数据到内存中的临时表,常见于GROUP BY操作时建议:创建索引Copying to tmp table on disk临时结果集太大,内存中放不下,需要将内存…

JS判断文本框中只能输入数字和小数点

http://liva-zheng.iteye.com/blog/1733827 1.文本框只能输入数字(不包括小数点) <input οnkeyup"this.valuethis.value.replace(/\D/g,)" onafterpaste"this.valuethis.value.replace(/\D/g,)"> 2.只能输入数字和小数点. <input οn…

java 方法 示例_Java集合asLifoQueue()方法和示例

java 方法 示例集合类asLifoQueue()方法 (Collections Class asLifoQueue() method) asLifoQueue() Method is available in java.lang package. asLifoQueue()方法在java.lang包中可用。 asLifoQueue() Method is used to represent the given deque as a Lifo queue (LIFO me…

废弃fastjson!大型项目迁移Gson保姆级实战

前言本篇文章是我这一个多月来帮助组内废弃fastjson框架的总结&#xff0c;我们将大部分Java仓库从fastjson迁移至了Gson。这么做的主要的原因是公司受够了fastjson频繁的安全漏洞问题&#xff0c;每一次出现漏洞都要推一次全公司的fastjson强制版本升级&#xff0c;很令公司头…

网页如何做到适应在手机上浏览

http://bbs.php100.com/read-htm-tid-482066.html 目前有很多不错的mobile开发框架可以使用&#xff0c;这些框架已经为手机端的特殊性提供了很好的支持和效果插件&#xff0c;比如&#xff1a;jquery mobile、kendoui等~~ 不过&#xff0c;谢谢框架因为其开源性或商业化&…

学到了!MySQL 8 新增的「隐藏索引」真不错

MySQL 8.0 虽然发布很久了&#xff0c;但可能大家都停留在 5.7.x&#xff0c;甚至更老&#xff0c;其实 MySQL 8.0 新增了许多重磅新特性&#xff0c;比如栈长今天要介绍的 "隐藏索引" 或者 "不可见索引"。隐藏索引是什么鬼&#xff1f; 隐藏索引 字面意思…

Java ClassLoader findLibrary()方法与示例

ClassLoader类findLibrary()方法 (ClassLoader Class findLibrary() method) findLibrary() method is available in java.lang package. findLibrary()方法在java.lang包中可用。 findLibrary() method is used to find the absolute pathname of the given native library. f…

iOS开发-自定义UIAlterView(iOS 7)

App中不可能少了弹框&#xff0c;弹框是交互的必要形式&#xff0c;使用起来也非常简单&#xff0c;不过最近需要自定义一个弹框&#xff0c;虽然iOS本身的弹框已经能满足大部分的需求&#xff0c;但是不可避免还是需要做一些自定义的工作。iOS7之前是可以自定义AlterView的&am…

jsp中redirect和forward的区别

在网上看到一些帖子&#xff0c;总结了一些区别&#xff0c;可以从以下几个方面来看&#xff1a;1.从地址栏显示来说 forward是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,然后把这些内容再发给浏览器.浏览器根本不知道服务器发送的内容从哪里来…

Spring 事务失效的 8 大场景,面试官直呼666...

前几天发了一篇文章里面有一个关于事务失效的问题&#xff1a;用 Spring 的 Transactional 注解控制事务有哪些不生效的场景&#xff1f;其中有个热心粉丝留言分享了下&#xff0c;我觉得总结得有点经验&#xff0c;给置顶了&#xff1a;但是我觉得还是总结得不够全&#xff0c…

python 示例_Python条件类| release()方法与示例

python 示例Python Condition.release()方法 (Python Condition.release() Method) release() is an inbuilt method of the Condition class of the threading module in Python. release()是Python中线程模块的Condition类的内置方法。 Condition class implements conditio…

Java BigInteger类| hashCode()方法与示例

BigInteger类hashCode()方法 (BigInteger Class hashCode() method) hashCode() method is available in java.math package. hashCode()方法在java.math包中可用。 hashCode() method is used to return the hash code value of this BigInteger. hashCode()方法用于返回此Big…

认真聊一下MySQL索引的底层实现!

前言当我们发现SQL执行很慢的时候&#xff0c;自然而然想到的就是加索引&#xff0c;当然他也是高频的面试问题&#xff0c;所以今天我们一起来学习一下MySQL索引的底层实现&#xff1a;B树。树简介、树种类B-树、B树简介B树插入B树查找B树删除B树经典面试题树的简介树的简介树…

js的四舍五入

Math.ceil求最小的整数但不小于本身. Math.round求本身的四舍五入。 Math.floor求最大的整数但不大于本身.

Java BigInteger类| bitCount()方法与示例

BigInteger类的bitCount()方法 (BigInteger Class bitCount() method) bitCount() method is available in java.math package. bitCount()方法在java.math包中可用。 bitCount() method is used to count the number of bits in 2’s complement denotation of this BigIntege…

head first python(第三章)–学习笔记

1.介绍基础文件&#xff0c;输入&#xff0c;输出 open() 打开文件&#xff0c;一次传入一行数据&#xff0c;可以结合for循环和readline()来使用 close() 用来关闭open打开的文件 the_file open(sketch.txt)the_file.close()例子&#xff1a; >>> data open(/root/…

最新大厂面试真题集锦

年后又是一波求职季&#xff0c;正是“金三银四”这个求职黄金期&#xff0c;很多人扎堆在这个时间段跳槽&#xff0c;找工作&#xff0c;程序员也不例外。春节刚过&#xff0c;各公司企业都开始启动了新一年的招聘计划&#xff0c;招聘岗位倍增&#xff0c;求职人数远超于岗位…

java.lang.IllegalThreadStateException 线程运行报错

写程序线程再运行第二遍的时候报java.lang.IllegalThreadStateException。发现一个Thread不能重复用start方法。 解决方法&#xff1a; 1、将extends Thread线程类改成implements Runnable&#xff0c;或者将Thread a new Thread改为Runnable a new Runnable&#xff1b; …

使用OpenCV在Python中进行人脸和眼睛检测

Modules Used: 使用的模块&#xff1a; python-opencv(cv2)python-opencv(cv2) python-opencv(cv2) Opencv(Open source computer vision) is a python library that will help us to solve computer vision problems. Opencv(开源计算机视觉)是一个Python库&#xff0c;可帮…

Java打造一款SSH客户端,已开源!

最近由于项目需求&#xff0c;项目中需要实现一个WebSSH连接终端的功能&#xff0c;由于自己第一次做这类型功能&#xff0c;所以首先上了GitHub找了找有没有现成的轮子可以拿来直接用&#xff0c;当时看到了很多这方面的项目&#xff0c;例如&#xff1a;GateOne、webssh、she…