本篇內(nèi)容介紹了“如何理解Spring單例”的有關(guān)知識(shí),在實(shí)際案例的操作過(guò)程中,不少人都會(huì)遇到這樣的困境,接下來(lái)就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!
創(chuàng)新互聯(lián)公司成都企業(yè)網(wǎng)站建設(shè)服務(wù),提供網(wǎng)站建設(shè)、網(wǎng)站制作網(wǎng)站開(kāi)發(fā),網(wǎng)站定制,建網(wǎng)站,網(wǎng)站搭建,網(wǎng)站設(shè)計(jì),響應(yīng)式網(wǎng)站開(kāi)發(fā),網(wǎng)頁(yè)設(shè)計(jì)師打造企業(yè)風(fēng)格網(wǎng)站,提供周到的售前咨詢(xún)和貼心的售后服務(wù)。歡迎咨詢(xún)做網(wǎng)站需要多少錢(qián):028-86922220
這是由一個(gè)真實(shí)的 bug 引起的,bug 產(chǎn)生的原因就是忽略了 Spring Bean 的單例模式。來(lái),先看一段簡(jiǎn)單的代碼。
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("本次隨機(jī)數(shù)為:" + 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
是一個(gè)帶有一個(gè)回調(diào)地址,參數(shù) token
是不確定的。
getCallback
方法每次調(diào)用,會(huì)隨機(jī)生成一個(gè)100以?xún)?nèi)的數(shù)字,然后將 callback
中的{token}
替換為這個(gè)隨機(jī)數(shù)字,最后的格式就像這樣的:
https://ip.com/token=88
然后在 main
方法中接收控制臺(tái)輸入,每次輸入的數(shù)字大于0,調(diào)用 getCallback
方法,然后輸出 url。
相信各位都能輕易的看出這段程序的輸出。
執(zhí)行程序之后,不管你輸入多少次數(shù)字,最后輸出的 callback
都是第一次的那個(gè)。
雖然每次生成的隨機(jī)數(shù)都變了,但是 callback
沒(méi)變。
有同學(xué)說(shuō),你過(guò)分了啊,這我能不知道為啥嗎?
main
方法只創(chuàng)建了一個(gè)TestService
實(shí)例,在第一次調(diào)用 getCallback
方法的時(shí)候,callback
這個(gè)字符串就被修改成 https://ip.com/token=89
了,所以,之后不管你再調(diào)用多少次,都不會(huì)執(zhí)行 replace
動(dòng)作了,因?yàn)?callback
中已經(jīng)沒(méi)有 {token}
這一段了。
TestService
在整個(gè)程序執(zhí)行過(guò)程中就是一個(gè)單例,所以,在 callback
第一次被修改后,后面再執(zhí)行
callback.replace("{token}", String.valueOf(number));
的動(dòng)作,拿到的 callback
中就已經(jīng)沒(méi)有 {token}
了,所以說(shuō),不會(huì)有替換的動(dòng)作。
當(dāng)然,這只是用最簡(jiǎn)單的程序說(shuō)明單例中的這個(gè)問(wèn)題,真正的項(xiàng)目中想用單例的話(huà),還要借助于單例設(shè)計(jì)模式實(shí)現(xiàn)。
有個(gè)弟弟在做微信服務(wù)號(hào)的開(kāi)發(fā),微信服務(wù)號(hào)或者訂閱號(hào)中有個(gè) access_token
的概念,這是所有請(qǐng)求的憑證,有效期 2 個(gè)小時(shí),到期之前要進(jìn)行刷新。
他是這樣設(shè)計(jì)的,在項(xiàng)目啟動(dòng)的時(shí)候立即調(diào)用微信接口獲取 access_token
,然后寫(xiě)了一個(gè)定時(shí)任務(wù)每1個(gè)小時(shí)刷新一次,獲取來(lái)的 access_token
放到 redis 和 數(shù)據(jù)庫(kù)中,當(dāng)調(diào)用微信服務(wù)號(hào)其他接口的時(shí)候,在 redis 中獲取 access_token
并拼接到接口地址中。
開(kāi)發(fā)調(diào)試的時(shí)候一起順利,看上去非常完美。
當(dāng)項(xiàng)目部署到測(cè)試環(huán)境測(cè)試的時(shí)候,問(wèn)題出現(xiàn)了。項(xiàng)目剛發(fā)版的時(shí)候,測(cè)試都正常,但是過(guò)一段時(shí)間,就會(huì)出現(xiàn)錯(cuò)誤,查看日志的時(shí)候,發(fā)現(xiàn)是微信服務(wù)號(hào)的接口返回了錯(cuò)誤碼,意思就是 access_token
已過(guò)期,需要重新獲取。
弟弟第一時(shí)間懷疑是定時(shí)任務(wù)出現(xiàn)了問(wèn)題,但是通過(guò)日志和數(shù)據(jù)庫(kù)中的更新時(shí)間,發(fā)現(xiàn)定時(shí)任務(wù)是完全沒(méi)有問(wèn)題的,刷新 access_token
的時(shí)間和定時(shí)任務(wù)是完全吻合的,說(shuō)明已經(jīng)及時(shí)刷新了。
我讓他用 redis 或數(shù)據(jù)庫(kù)中的access_token
去調(diào)一下服務(wù)號(hào)接口,看看是不是也有同樣的過(guò)期問(wèn)題。
結(jié)果一試,redis 中存的是沒(méi)問(wèn)題的,可以正常使用。
那徹底排除是定時(shí)任務(wù)的問(wèn)題了,問(wèn)題的癥結(jié)應(yīng)該就出在兩個(gè)地方:
1、在獲取 redis 中的access_token
的過(guò)程;
2、將獲取到的 access_token
拼接到請(qǐng)求接口 URL 上發(fā)生了錯(cuò)誤;
到這里就很好判斷了,他把從 redis 拿到的access_token
和最后拼接好的 URL 都輸出到日志中一看,果然,兩個(gè)是不一致的。
從 redis 取出的確實(shí)是最新可用的 access_token
,但是拼接到接口 URL 上之后,發(fā)現(xiàn)是另外一個(gè)。那就確定是拿到的 access_token
是沒(méi)問(wèn)題的,但是最后拼接到 URL 卻有問(wèn)題。這時(shí),弟弟仔細(xì)檢查了代碼,然后徹底蒙了。
既然問(wèn)題出在哪兒已經(jīng)確定了,那就分析那段代碼就好了。
項(xiàng)目整體采用的是 Spring Boot,代碼很簡(jiǎn)單,就是在一個(gè) Controller 中調(diào)用 Service 中的一個(gè)方法。大致 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("本次隨機(jī)數(shù)為:" + number); callback = callback.replace("{token}", String.valueOf(number)); return callback; } }
看到這里,各位肯定已經(jīng)發(fā)現(xiàn)問(wèn)題原因了。雖然有多次請(qǐng)求,但因?yàn)?Spring Bean 默認(rèn)是單例模式,所以實(shí)際上和前面演示的那個(gè)控制臺(tái)程序是類(lèi)似的,從頭到尾都只有一個(gè) TestService 實(shí)例,所以只有第一次能將{token}
替換成真正的access_token
。
對(duì)應(yīng)到實(shí)際的服務(wù)號(hào)場(chǎng)景中,在第一次調(diào)用這個(gè)接口時(shí),從 redis 拿到 access_token
拼接到具體的 URL中是沒(méi)問(wèn)題的,但是一旦這個(gè)access_token
過(guò)期(1小時(shí)后),再次請(qǐng)求這個(gè)接口就會(huì)出現(xiàn) access_token
過(guò)期的問(wèn)題。
這里違反了 Spring 單例模式的一個(gè)點(diǎn),那就是 Spring 單例模式,不適合存儲(chǔ)有狀態(tài)的值,比如這里的 callback
就是個(gè)有狀態(tài)的值,它應(yīng)該隨著定時(shí)任務(wù)的進(jìn)行,獲取到不同的值。
關(guān)于 Spring 或 Spring Boot 工作流程的介紹可以閱讀文末的兩篇文章,其中包括 Bean 實(shí)例化過(guò)程。
如何解決這個(gè)問(wèn)題呢?
其實(shí)很簡(jiǎn)單,不讓callback
每次調(diào)用發(fā)生變化就可以了,每次拼接 URL 的時(shí)候,先將 callback
賦給一個(gè)局部變量,然后在這個(gè)變量上操作就好了。
public String getCallback() { Random random = new Random(); int number = random.nextInt(100); System.out.println("本次隨機(jī)數(shù)為:" + number); String tempCallback = callback; tempCallback = tempCallback.replace("{token}", String.valueOf(number)); return tempCallback; }
另外,說(shuō)到 Spring 單例模式,Spring 本身還支持其他幾種模式,與單例模式對(duì)應(yīng)的就是 prototype
模式,這種模式是每個(gè)請(qǐng)求都重新生成實(shí)例。所以,如果你確定這個(gè) 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("本次隨機(jī)數(shù)為:" + number); callback = callback.replace("{token}", String.valueOf(number)); return callback; } }
這樣一來(lái),每次都是新的實(shí)例,自然就不存在那個(gè)問(wèn)題了。
“如何理解Spring單例”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!