我是风筝,公众号「古时的风筝」,一个兼具深度与广度的程序员鼓励师,一个本打算写诗却写起了代码的田园码农!
文章会收录在 JavaNewBee 中,更有 Java 后端知识图谱,从小白到大牛要走的路都在里面。java
谁还没在 Spring 里栽过跟头呢,从哪儿跌倒,就从哪儿睡一下子,而后再爬起来。git
这是由一个真实的 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
是不肯定的。github
getCallback
方法每次调用,会随机生成一个100之内的数字,而后将 callback
中的{token}
替换为这个随机数字,最后的格式就像这样的:redis
https://ip.com/token=88
而后在 main
方法中接收控制台输入,每次输入的数字大于0,调用 getCallback
方法,而后输出 url。数据库
相信各位都能轻易的看出这段程序的输出。后端
执行程序以后,无论你输入多少次数字,最后输出的 callback
都是第一次的那个。设计模式
虽然每次生成的随机数都变了,可是 callback
没变。微信
有同窗说,你过度了啊,这我能不知道为啥吗?app
main
方法只建立了一个TestService
实例,在第一次调用 getCallback
方法的时候,callback
这个字符串就被修改为 https://ip.com/token=89
了,因此,以后无论你再调用多少次,都不会执行 replace
动做了,由于 callback
中已经没有 {token}
这一段了。
TestService
在整个程序执行过程当中就是一个单例,因此,在 callback
第一次被修改后,后面再执行
callback.replace("{token}", String.valueOf(number));
的动做,拿到的 callback
中就已经没有 {token}
了,因此说,不会有替换的动做。
固然,这只是用最简单的程序说明单例中的这个问题,真正的项目中想用单例的话,还要借助于单例设计模式实现。
有个弟弟在作微信服务号的开发,微信服务号或者订阅号中有个 access_token
的概念,这是全部请求的凭证,有效期 2 个小时,到期以前要进行刷新。
他是这样设计的,在项目启动的时候当即调用微信接口获取 access_token
,而后写了一个定时任务每1个小时刷新一次,获取来的 access_token
放到 redis 和 数据库中,当调用微信服务号其余接口的时候,在 redis 中获取 access_token
并拼接到接口地址中。
开发调试的时候一块儿顺利,看上去很是完美。
当项目部署到测试环境测试的时候,问题出现了。项目刚发版的时候,测试都正常,可是过一段时间,就会出现错误,查看日志的时候,发现是微信服务号的接口返回了错误码,意思就是 access_token
已过时,须要从新获取。
弟弟第一时间怀疑是定时任务出现了问题,可是经过日志和数据库中的更新时间,发现定时任务是彻底没有问题的,刷新 access_token
的时间和定时任务是彻底吻合的,说明已经及时刷新了。
我让他用 redis 或数据库中的access_token
去调一下服务号接口,看看是否是也有一样的过时问题。
结果一试,redis 中存的是没问题的,能够正常使用。
那完全排除是定时任务的问题了,问题的症结应该就出在两个地方:
一、在获取 redis 中的access_token
的过程;
二、将获取到的 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 { @Autowired private 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 { @Autowired private 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; } }
这样一来,每次都是新的实例,天然就不存在那个问题了。
从 Spring Boot 出发,分析 Spring IoC 过程
这位英俊潇洒的少年,若是以为还不错的话,给个推荐可好!
公众号「古时的风筝」,Java 开发者,全栈工程师,bug 杀手,擅长解决问题。
一个兼具深度与广度的程序员鼓励师,本打算写诗却写起了代码的田园码农!坚持原创干货输出,你可选择如今就关注我,或者看看历史文章再关注也不迟。长按二维码关注,跟我一块儿变优秀!