从一个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;}
}

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

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

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

相关文章

网络层之无分类编址CIDR(内涵计算例题)

学习的最大理由是想摆脱平庸,早一天就多一份人生的精彩;迟一天就多一天平庸的困扰。各位小伙伴,如果您: 想系统/深入学习某技术知识点… 一个人摸索学习很难坚持,想组团高效学习… 想写博客但无从下手,急需…

初学vue3与ts:element-plus的警告(Extraneous non-props attributes (ref_key) ...)

用了vue3与ts,ui我就选了element-plus element-plus官网:https://element-plus.org/zh-CN/ element-plus官网(国内镜像站点):https://element-plus.gitee.io/zh-CN/ 国内镜像站点如果进不去的话,在element-plus官网最下面的链接-&…

Jupyter Notebook中设置Cell主题

1. 获取本机Jupyter的配置目录 C:\Users\Administrator>jupyter --data-dir C:\Users\Administrator\AppData\Roaming\jupyter2. 进入获取的目录,创建指定路径 C:\Users\Administrator>cd C:\Users\Administrator\AppData\Roaming\jupyter C:\Users\Adminis…

TikTok新闻视角:短视频如何改变信息传递方式?

随着数字时代的不断发展,信息传递的方式也在不断演变。近年来,短视频平台TikTok崭露头角,通过其独特的15秒短视频形式,逐渐在新闻传播领域占据一席之地。本文将深入探讨TikTok在新闻视角下是如何改变信息传递方式的,以…

计算机毕设:基于机器学习的生物医学语音检测识别 附完整代码数据可直接运行

项目视频讲解: 基于机器学习的生物医学语音检测识别 完整代码数据可直接运行_哔哩哔哩_bilibili 运行效果图: 数据展示: 完整代码: #导入python的 numpy matplotlib pandas库 import pandas as pd import numpy as np import matplotlib.pyplot as plt #绘图 import se…

jupyter notebook中添加内核kernel

step1 检查环境中是否有kernel python -m ipykernel --versionstep2 若没有kernel,则需要安装 kernel conda install ipykernel -i https://pypi.tuna.tsinghua.edu.cn/simplestep3 查看已添加的内核 jupyter kernelspec liststep4 添加内核 python -m ipykerne…

学习php中使用composer下载安装firebase/php-jwt 以及调用方法

学习php中使用composer下载安装firebase/php-jwt 以及调用方法 1、安装firebase/php-jwt2、封装jwt类 1、安装firebase/php-jwt composer require firebase/php-jwt安装好以后出现以下文件: 2、封装jwt类 根据所使用的php框架&#xff0c;在指定目录创建 Token.php <?ph…

外贸建站要国外服务器吗?海外服务器推荐?

外贸建站如何选国外服务器&#xff1f;海洋建站用什么服务器好&#xff1f; 外贸建站已经成为企业拓展国际市场的一项重要举措。然而&#xff0c;一个关键问题摆在许多企业面前&#xff1a;外贸建站是否需要选择国外服务器呢&#xff1f;这个问题涉及到多方面的考虑因素&#…

智能优化算法应用:基于吉萨金字塔建造算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于吉萨金字塔建造算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于吉萨金字塔建造算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.吉萨金字塔建造算法4.实验参数设…

航空复合材料行业分析:预计2028年全球市场规模将达3725.28亿元

航空复合材料是由多种材料层面组合而成的&#xff0c;在冷热不均的环境中&#xff0c;各个组成材料之间的温度承受度不同很容易引起部件损坏等情况的发生&#xff0c;因此随着复合材料在通用航空中应用的增加&#xff0c;航空复合材料维修市场已形成一定规模。目前&#xff0c;…

Node.js入门指南(完结)

目录 接口 介绍 RESTful json-server 接口测试工具 会话控制 介绍 cookie session token 上一篇文章我们介绍了MongoDB&#xff0c;这一篇文章是Node.js入门指南的最后一篇啦&#xff01;主要介绍接口以及会话控制。 接口 介绍 接口是前后端通信的桥梁 &#xff0…

【Unity动画】Unity 动画播放的流程

本文以2D为案例&#xff0c;讲解Unity 播放动画的流程 准备和导入2D动画资源 外部导入序列帧生成的 Unity内部制作的 外部导入的3D动画 2.创建动画过程 打开时间轴Ctrl6 选中场景中的一个未来需要播放动画的物体 回到时间轴点击Create一个新动画片段 拖动2D动画资源放入…

【数据结构和算法】到达首都的最少油耗

其他系列文章导航 Java基础合集数据结构与算法合集 设计模式合集 多线程合集 分布式合集 ES合集 文章目录 其他系列文章导航 文章目录 前言 一、题目描述 二、题解 三、代码 四、复杂度分析 前言 这是力扣的2477题&#xff0c;难度为中等&#xff0c;解题方案有很多种&…

Interpretable Multimodal Misinformation Detection with Logic Reasoning

原文链接 Hui Liu, Wenya Wang, and Haoliang Li. 2023. Interpretable Multimodal Misinformation Detection with Logic Reasoning. In Findings of the Association for Computational Linguistics: ACL 2023, pages 9781–9796, Toronto, Canada. Association for Computa…

前后端验证码分析(字母计算)

样式&#xff1a; 前端&#xff1a; login.vue <template> <view class"normal-login-container"> <view class"login-form-content"> <view class"input-item flex align-center"> <view class"iconfont ic…

【Android】解决安卓中并不存在ActivityMainBinding

安卓中并不存在ActivityMainBinding这个类&#xff0c;这个类是在XML布局的最外层加入就会自动生成。但是你在最后绑定主布局时会报错获取不到根节点getRoot(). 最好的办法就是&#xff0c;删除原来的最外层节点&#xff0c;再重新添加&#xff0c;感觉是因为复制时并没有让系…

基于阿里云服务网格流量泳道的全链路流量管理(一):严格模式流量泳道

作者&#xff1a;尹航 概述 灰度发布是一种常见的对新版本应用服务的发布手段&#xff0c;其特点在于能够将流量在服务的稳定版本和灰度版本之间时刻切换&#xff0c;以帮助我们用更加可靠的方式实现服务的升级。在流量比例切换的过程中&#xff0c;我们可以逐步验证新版本服…

人工智能时代AIGC绘画实战

系列文章目录 送书第一期 《用户画像&#xff1a;平台构建与业务实践》 送书活动之抽奖工具的打造 《获取博客评论用户抽取幸运中奖者》 送书第二期 《Spring Cloud Alibaba核心技术与实战案例》 送书第三期 《深入浅出Java虚拟机》 送书第四期 《AI时代项目经理成长之道》 …

编译WSL内核,用于操作usb读卡器

wsl2默认不能操作usb读卡器&#xff0c;但是对于嵌入式linux开发来说&#xff0c;需要经常对tf卡进行操作&#xff0c;随时都会使用到usb读卡器的访问。下面讲述如何开启wsl2的usb读卡器的访问&#xff0c;主要涉及到以下2个步骤&#xff1a; wsl2本质是一个虚拟机&#xff0c…

C++作业5

完成沙发床的多继承&#xff08;有指针成员&#xff09; 代码&#xff1a; #include <iostream>using namespace std;class Bed { private:double *money; public:Bed(){cout << "Bed::无参构造函数" << endl;}Bed(double money):money(new doub…