谁还没在 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

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

这个 bug 让我更加理解 Spring 单例了相关推荐

  1. 转载 spring单例bug

    https://www.cnblogs.com/fengzheng/p/14171443.html 这个 bug 让我更加理解 Spring 单例了 我是风筝,公众号「古时的风筝」,一个兼具深度与广度 ...

  2. Spring单例Bean与单例模式的区别

    Spring单例Bean与单例模式的区别在于它们关联的环境不一样,单例模式是指在一个JVM进程中仅有一个实例,而Spring单例是指一个Spring Bean容器(ApplicationContext ...

  3. 责任链模式 多条链路时 spring单例 造成的深坑

    https://mp.weixin.qq.com/s/ipXPMtPawDdESMJYrYthcQ 一)线上事故: 催收系统每日自动分配案件时一直正常,突然某一天(2018-3-27)以后 案件分配不 ...

  4. spring 单例 获取多例的位_Spring 获取单例流程(一)

    读完这篇文章你将会收获到 在 getBean 方法中, Spring 处理别名以及 factoryBean 的 name Spring 如何从多级缓存中根据 beanName 获取 bean Spri ...

  5. java spring 单例_spring怎么实现单例模式?

    在Spring中,bean可以被定义为两种模式:prototype(多例)和singleton(单例) singleton(单例):只有一个共享的实例存在,所有对这个bean的请求都会返回这个唯一的实 ...

  6. spring 单例 获取多例的位_Spring系列第6篇:玩转bean scope,避免跳坑里!

    公众号关注"程序员二哥", 设为'星标',带你学习更多的知识. 本文内容 详细介绍5中bean的sope及使用注意点 自定义作用域的实现 应用中,有时候我们需要一个对象在整个应用中 ...

  7. spring单例的bean是单例还是原型

    转载请注明出处:https://blog.csdn.net/qq_27218667/article/details/99690798 有一次老大突然问spring管理的bean是单例还是多例,当时印象 ...

  8. Spring单例的线程安全性

    在Spring中单例bean是在beanfactory中用反射机制动态创建的,被缓存到ioc容器中相对于new了一个对象. 而这些创建的对象class,则是自己创建的. 所以是不是线程安全和Sprin ...

  9. spring的单例回收

    首先.spring采用注册单例,符合注册单例特征. 被保存在ioc的缓存容器中,给每个实例起一个名字beanname.ID 在拿一个实例的时候只能从这个beanname中去取,也就意味着ioc容器持有 ...

最新文章

  1. Django-Ajax进阶
  2. 27年前被Nature拒稿,如今斩获诺贝尔奖!学术投稿模式再引热议:都有神奇的评审2...
  3. CentOS6.9下手动编译并安装Python3.7.0
  4. 机器学习理论《统计学习方法》学习笔记:第十一章 条件随机场(CRF)
  5. LeetCode之Sqrt(x)
  6. merge函数_c语言中的merge函数
  7. hadoop3.1集成tez和tez-ui
  8. esp32在ubuntu搭建环境,clone esp-idf部分库clone失败
  9. 佳博打印机打印条码和二维码的方法
  10. OpenCV 模板匹配
  11. lookup无序查找_查找引用之王——Lookup函数实用技巧解读!
  12. kepp-alive的作用?keep-alive的属性?路由元信息?白名单黑名单?keep-alive的钩子函数
  13. 在Vue中自制视频播放器(上)
  14. sau交流学习社区第三方登陆github--oauth来实现用户登录
  15. springboot毕设项目社区分享系统的实现与分析48ig8(java+VUE+Mybatis+Maven+Mysql)
  16. html5游戏开发 网页版-捕鱼达人游戏源码下载
  17. 2019最新《布尔教育php设计模式项目实战 共17课》
  18. git查看打tag时间_使用git打tag标签/切换到某个tag时期,删除/查看分支
  19. 一等奖,iPhone XR来了!A/B Test,个人中心首页改版实验分析报告(5000字详解)...
  20. zabbix之Disk I/O is overloaded on {HOST.NAME}问题排查

热门文章

  1. wpf 代码获取contextmenu_[C#] 转:在WPF里面获取右键弹出菜单(ContextMenu)的鼠标点击源(Owner)控件...
  2. php如何新建xml文件,PHP中的生成XML文件的4种方法分享
  3. python调用窗口找到文件,使用Python在Mac OS X中查找当前活动窗口
  4. MySQL5.7.17的简单配置文件
  5. 连接postgresql
  6. 微服务架构会和分布式单体架构高度重合吗
  7. WEB平台架构之:LAMP(Linux+Apache+MySQL+PHP)
  8. Nginx学习笔记(五) 源码分析内存模块内存对齐
  9. 比较两大虚拟桌面厂商的系统镜像管理
  10. 《大话设计模式》读书笔记-索引