网站数据库是什么意思,域名有关的网站,yahoo网站提交,网页设计制作代码大全谁还没在 Spring 里栽过跟头呢#xff0c;从哪儿跌倒#xff0c;就从哪儿睡一会儿#xff0c;然后再爬起来。讲点儿武德 这是由一个真实的 bug 引起的#xff0c;bug 产生的原因就是忽略了 Spring Bean 的单例模式。来#xff0c;先看一段简单的代码。public class TestS… 谁还没在 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/token88
然后在 main方法中接收控制台输入每次输入的数字大于0调用 getCallback方法然后输出 url。相信各位都能轻易的看出这段程序的输出。执行程序之后不管你输入多少次数字最后输出的 callback都是第一次的那个。虽然每次生成的随机数都变了但是 callback没变。其实就是单例有同学说你过分了啊这我能不知道为啥吗main方法只创建了一个TestService实例在第一次调用 getCallback方法的时候callback这个字符串就被修改成 https://ip.com/token89了所以之后不管你再调用多少次都不会执行 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 关注我↓↓↓每天收获干货.