我是风筝,公众号「古时的风筝」,一个兼具深度与广度的程序员鼓励师,一个本打算写诗却写起了代码的田园码农!

文章会收录在 JavaNewBee 中,更有 Java 后端知识图谱,从小白到大牛要走的路都在里面。

谁还没在 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 { @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 IoC 实现过程

从 Spring Boot 出发,分析 Spring IoC 过程


这位英俊潇洒的少年,如果觉得还不错的话,给个推荐可好!

公众号「古时的风筝」,Java 开发者,全栈工程师,bug 杀手,擅长解决问题。

一个兼具深度与广度的程序员鼓励师,本打算写诗却写起了代码的田园码农!坚持原创干货输出,你可选择现在就关注我,或者看看历史文章再关注也不迟。长按二维码关注,跟我一起变优秀!

这个 bug 让我更加理解 Spring 单例了的更多相关文章

  1. Spring单例Bean和线程安全

    Spring的bean默认都是单例的,这些单例Bean在多线程程序下如何保证线程安全呢?例如对于Web应用来说,Web容器对于每个用户请求都创建一个单独的Sevlet线程来处理请求,引入Spring框 ...

  2. Spring单例 和 Scope注解

    关键字  @Scope  @Qualifier  Singleton 单例 Spring是单例模式.结合Springboot的例子. Controller @Autowired private Tes ...

  3. 002-创建型-03-单例模式(Singleton)【7种】、spring单例及原理

    一.概述 保证一个类仅有一个实例,并提供一个全局访问点 私有构造器.线程安全.延迟加载.序列化和反序列化安全.反射攻击 1.1.适用场景 1.在多个线程之间,比如servlet环境,共享同一个资源或者 ...

  4. Spring单例与线程安全小结

    一.Spring单例模式与线程安全   Spring框架里的bean,或者说组件,获取实例的时候都是默认的单例模式,这是在多线程开发的时候要尤其注意的地方. 单例模式的意思就是只有一个实例.单例模式确 ...

  5. (转载)spring单例和多例详解。如何在单例中调用多例对象

    spring生成对象默认是单例的.通过scope属性可以更改为多例. <bean id="user" class="modle.User" scope=& ...

  6. Spring 单例

    我们知道 Web 容器本身就是多线程的,Web 容器为一个 Http 请求创建一个独立的线程,所以由此请求所牵涉到的 Spring 容器中的 Bean 也是运行于多线程的环境下.在绝大多数情况下,Sp ...

  7. spring单例bean是线程安全的吗?

    如果在你不定义成员变量的情况下,spring默认是线程安全的 否则,设置scope="prototype"

  8. Spring 单例 httprequest 线程安全

    @Autowired HttpServletRequest之所以线程安全是因为, httpsevletRequest 储存在 RequestContextHolder中. 每次http请求的doXXX ...

  9. Spring 源码学习 - 单例bean的实例化过程

    本文作者:geek,一个聪明好学的同事 1. 简介 开发中我们常用@Commpont,@Service,@Resource等注解或者配置xml去声明一个类,使其成为spring容器中的bean,以下我 ...

随机推荐

  1. 使用Folx下载任务完成后,怎么自动完成关闭

    下载工具的优点是可以通过多线程的方式,提高文件的下载速度,减少用户的下载时间.但另一方面来说,下载工具为了达到高速下载,也会占据较多的带宽资源,甚至会拖慢电脑的运行. 因此,很多用户会利用电脑的空闲时 ...

  2. appium 启动参数配置

    启动配置参数,可以参照官网: http://appium.io/docs/en/writing-running-appium/caps/#general-capabilities from appiu ...

  3. python 几个循环的效率测试

    前言:对于我这种追求极致的人来说,效率很重要. 前面看到网上关于python循环的测评,到自己在项目中的应用,发现,并不是这么回事.所以,写下次博文,一次性了解这个问题. 语言版本:python3.6 ...

  4. 肝了一周的 UDP 基础知识终于出来了。

    我把自己以往的文章汇总成为了 Github ,欢迎各位大佬 star https://github.com/crisxuan/bestJavaer 已提交此篇文章 运输层位于应用层和网络层之间,是 O ...

  5. day100:MoFang:用户模型类的创建&Marshmallow模块&使用基本构造器Schema完成数据的序列化转换和反序列化转换

    目录 1.用户模型的创建 2.Marshmallow模块 3.MarshMallow基本构造器:Schema 1.基于Schema完成数据序列化转换 2.基于Schema完成数据反序列化转换 3.反序 ...

  6. 【刷题笔记】DP优化-斜率优化

    斜率优化,是一种利用斜率的优化(废话) 关于数论:咕咕咕 部分内容参考自学长 如果有这样的一个状态转移方程: \[f[i]=\min\limits_{j=L_j}^{R_j}\{f[j]+val(j, ...

  7. Beta冲刺随笔——Day_Four

    这个作业属于哪个课程 软件工程 (福州大学至诚学院 - 计算机工程系) 这个作业要求在哪里 Beta 冲刺 这个作业的目标 团队进行Beta冲刺 作业正文 正文 其他参考文献 无 今日事今日毕 林涛: ...

  8. 用了Dapper之后就不要再见到SqlConnection咯

    一.背景 前几天看公司一个新项目的底层使用了dapper,大家都知道dapper是一个非常强大的半自动化orm,帮程序员解决了繁琐的mapping问题,用起来非常爽,但我还是遇到了一件非常不爽的事情, ...

  9. 第三十六章、PyQt输入部件:QAbstractSpinBox派生类QSpinBox、 QDoubleSpinBox、QDateTimeEdit、QDateEdit和QTimeEdit

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 老猿学5G博文目录 一.概述 Designer输入部件中的Spin B ...

  10. windows下多Python环境指定pip安装模块到对应Python环境下

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 老猿在windows下装了2套Python,一套是直接安装的Pytho ...