大家好,我是3y,今天繼續來聊我的開源專案austin
啊,但實際內容更新不多,這文章主是想吹下水,主要聊聊我在更新專案中學到的小技巧,
今天所說的小技巧可能有很多人都會,但肯定也會有跟我一樣之前沒用過的,
訊息推送平臺??推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別,
- https://gitee.com/zhongfucheng/austin/
- https://github.com/ZhongFuCheng3y/austin
Spring注入集合
之前我一直不知道,原來Spring是能注入集合的,直到一個pull request
被提了過來,
https://gitee.com/zhongfucheng/austin/pulls/31
我之前寫了一個自定義注解,它的作用就是收集自定義注解所標識的Bean
,然后最后把這些Bean
放到Map
里
@Component
public class SmsScriptHolder {
private Map<String, SmsScript> handlers = new HashMap<>(8);
public void putHandler(String scriptName, SmsScript handler) {
handlers.put(scriptName, handler);
}
public SmsScript route(String scriptName) {
return handlers.get(scriptName);
}
}
/**
* 標識 短信渠道
*
* @author 3y
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Component
public @interface SmsScriptHandler {
/**
* 這里輸入腳本名
*
* @return
*/
String value();
}
/**
* sms發送腳本的抽象類
*
* @author 3y
*/
@Slf4j
public abstract class BaseSmsScript implements SmsScript {
@Autowired
private SmsScriptHolder smsScriptHolder;
@PostConstruct
public void registerProcessScript() {
if (ArrayUtils.isEmpty(this.getClass().getAnnotations())) {
log.error("BaseSmsScript can not find annotation!");
return;
}
Annotation handlerAnnotations = null;
for (Annotation annotation : this.getClass().getAnnotations()) {
if (annotation instanceof SmsScriptHandler) {
handlerAnnotations = annotation;
break;
}
}
if (handlerAnnotations == null) {
log.error("handler annotations not declared");
return;
}
//注冊handler
smsScriptHolder.putHandler(((SmsScriptHandler) handlerAnnotations).value(), this);
}
}
結果,pull request
提的代碼過來特別簡單就替代了我的代碼了,只要在使用的時候,直接注入Map
:
@Autowired
private Map<String, SmsScript> smsScripts;
這一行代碼就能夠實作,把SmsScript
的實作類都注入到這個Map
里,同樣的,我們亦可以使用List<Interface>
把該介面下的實作類都注入到這個List里,
這好奇讓我去看看Spring
到底是怎么實作的,但實際上并不難,入口在org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject
接著定位到:org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveDependency
深入 org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency
最后實作注入的位置: org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveMultipleBeans
陣列 相關實作
access_token存盤到Redis
在接入微信相關渠道時,我就說過austin
借助了wxjava
這個開源組件庫(該組件庫對接微信相關api
,使呼叫變得尤其簡單),
比如,我們呼叫微信的api
是需要access_token
的引數的,如果是我們自己撰寫代碼呼叫微信api
,那我們需要先獲取access_token
,然后把該access_token
拼接在url
上,此時,我們又需要考慮access_token
會不會失效了,失效了我們要有重試的策略,
而wxjava
把這些都封裝好了,屏蔽了內部實作細節,只要我們把微信渠道的賬號資訊寫到WxMpConfigStorage
里,那該組件就會幫我們去拿到access_token
,內部也會有相應的重試策略,
第一版我為了圖方便,我是使用WxMpDefaultConfigImpl
實作類把渠道相關資訊存盤在本地記憶體里(包括access_token
),而在上周我把渠道相關資訊轉都存盤至Redis
,
主要是獲取access_token
它的呼叫次數是有限的,如果專案集群部署,而access_token
又存盤在本地記憶體中,那就很大概率不到一天時間呼叫獲取access_token
次數就滿了,要是拿不到access_token
,那就沒辦法呼叫微信的介面了,
https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
對于wxjava
這個組件庫,呼叫微信的api
都是通過Wx(xx)Service
來使用的,而我是想把Wx(xx)Service
做成是單例的,那在實作access_token
存盤到Redis
的時候,我就很自然就要對舊代碼進行一波重構(因為第一版寫出來的代碼,多多少少都有點不滿意),
歷史背景:
1、WxServiceUtils
的邏輯是專案啟動的時檢索資料庫里所有的微信渠道賬號資訊,將Wx(xx)Service
寫入到Map
里,Wx(xx)Service
要做成單例自然就會想到用Map
存盤(因為訊息推送平臺很可能會對接很多個服務號或者小程式,這里資料結構肯定優先是Map
啦)
如果渠道的賬號通過后臺有存在變更行為,那程式內部會執行refresh()
重繪,但這個僅僅是在程式內能監聽到的變更,如果是直接通過SQL
修改表的記錄,目前是沒有機制重繪Map
的內容的,
2、AccountUtils
的邏輯是程式運行時得到發送賬號的Id
,通過Id
去資料庫檢索賬號配置,實時回傳賬號最新的內容,(除了微信渠道賬號,其他所有的渠道賬號都是在這里獲取資訊)
更新:把原有管理微信賬號資訊的WxServiceUtils
類給棄用了,將所有的發送渠道賬號資訊都歸到AccountUtils
進行管理,
Map.computeIfAbsent使用
在重構上面所講的邏輯時,我很快地寫出以下的代碼:
if (clazz.equals(WxMaService.class)) {
if (Objects.nonNull(miniProgramServiceMap.get(channelAccount))) {
return (T)miniProgramServiceMap.get(channelAccount);
}
WxMaService wxMaService = initMiniProgramService(JSON.parseObject(channelAccount.getAccountConfig(), WeChatMiniProgramAccount.class));
miniProgramServiceMap.put(channelAccount, wxMaService);
return (T) wxMaService;
} else if (clazz.equals(WxMpService.class)) {
if (Objects.nonNull(officialAccountServiceMap.get(channelAccount))) {
return (T)officialAccountServiceMap.get(channelAccount);
}
WxMpService wxMpService = initOfficialAccountService(JSON.parseObject(channelAccount.getAccountConfig(), WeChatOfficialAccount.class));
officialAccountServiceMap.put(channelAccount, wxMpService);
return (T) wxMpService;
}
等我寫完,然后簡單做了下自測,發現這代碼咋這么丑啊,兩個if
的邏輯實際上是一樣的,
我想,這一定會有什么工具類能幫我去優化下這個代碼的,我正準備翻Hutool
/Guava
這種工具包時,我突然想起:JDK在1.8好像就提供了putIfXXX的方法啦,我還找個毛啊,直接看看JDK的方法能不能用先,
很快啊,我就找到了,
我首先看的是putIfAbsent
,發現它實作很簡單,就是做了一層封裝,
default V putIfAbsent(K key, V value) {
V v = get(key);
if (v == null) {
v = put(key, value);
}
return v;
}
但卻很適合用來優化我上面的代碼,于是,很快啊,我就改成了這樣:
if (clazz.equals(WxMaService.class)) {
return (T) miniProgramServiceMap.putIfAbsent(channelAccount, initMiniProgramService(JSON.parseObject(channelAccount.getAccountConfig(), WeChatMiniProgramAccount.class)));
} else if (clazz.equals(WxMpService.class)) {
return (T) officialAccountServiceMap.putIfAbsent(channelAccount, initOfficialAccountService(JSON.parseObject(channelAccount.getAccountConfig(), WeChatOfficialAccount.class)));
}
這看著真簡潔啊,好像已經很完美了,本來有好幾行的代碼,優化了下變成了一行,
但我又思考了下,這個putIfAbsent
的V
我這邊傳入的是一個方法,每次這個方法都會執行的(不論我的Map
里有沒有這個K
),這又感覺不太優雅了,
我又點進去computeIfAbsent
看了下,嗯!這就是我想要的了:如果Map
的V
不存在時,才去執行我生成V
的邏輯
default V computeIfAbsent(K key,
Function<? super K, ? extends V> mappingFunction) {
Objects.requireNonNull(mappingFunction);
V v;
if ((v = get(key)) == null) {
V newValue;
if ((newValue = https://www.cnblogs.com/Java3y/archive/2023/05/08/mappingFunction.apply(key)) != null) {
put(key, newValue);
return newValue;
}
}
return v;
}
(這個其實我在學lambda
和stream
流的時候曾經是體驗過的,我日常也會簡單寫點,只是不知道在JDK
里Map
也有這樣的方法,)于是,最后的代碼就成了:
if (clazz.equals(WxMaService.class)) {
return (T) miniProgramServiceMap.computeIfAbsent(channelAccount, account -> initMiniProgramService(JSON.parseObject(account.getAccountConfig(), WeChatMiniProgramAccount.class)));
} else if (clazz.equals(WxMpService.class)) {
return (T) officialAccountServiceMap.computeIfAbsent(channelAccount, account -> initOfficialAccountService(JSON.parseObject(account.getAccountConfig(), WeChatOfficialAccount.class)));
}
又后來,等我發布到Git倉庫后,有人提了pull request
來修復ConcurrentHashMap
的computeIfAbsent
存在性能的問題,呀,不小心又學到了點東西,
https://bugs.openjdk.java.net/browse/JDK-8161372
微信掃碼登錄實作
我在生產環境下是沒有寫過「用戶登錄」的,導致有些業務功能我也不知道線上是怎么實作的,而「用戶登錄注冊」這個功能之前會聽過和見識過一些技術堆疊「Shiro」、「JWT」、「Spring Security」、「CAS」、「OAuth2.0」等等,
但是,我的需求只是用來做簡單的校驗,不需要那么復雜,如果就給我設計一張user表,對其簡單的增刪改查好像也滿足,但我又不想寫這樣的代碼,因為我在大學的時候實作過類似的,
現在不都流行掃碼登錄嘛?我不是已經接入了微信服務號的模板訊息了嗎,不正好有一個測驗號給我去做嗎?于是就開干了,
首先看看人家是怎么寫的,于是被我找到了一篇博客:https://blog.51cto.com/cxhit/4924932
程序挺好懂的,就按著他給出的時序圖對著實作就完了,后端對我來說實作并不難,花的時間最長的還是在前端的互動上,畢竟我這當時選用的是低代碼平臺啊,不能隨便實作各種邏輯的啊,
在前端,就一個「輪詢」功能,要輪詢查看用戶是否已經訂閱登錄,就耗費了我很多時間在官方檔案上,后來,寫了不少的奇淫技巧,最后也就被我實作出來了,實作程序很糟糕,也不值一提,反正你們也不會從中學到什么好東西,因為我也沒有,
程序還是簡單復述下吧,后期可能也會有同學去實作這個功能,
1、首先我們要有一個介面,給到微信回呼,所以我們一般會稱該介面為回呼介面,微信的一些重要的事件都會回呼給我們,我們做回應的邏輯處理,就比如,用戶關注了服務號,這種訊息微信就呼叫我們的介面,
2、在微信后臺配置我們的定義好的回呼介面,給到微信進行回呼,
(如果介面是通的,按正常的走,那就會配置成功)
3、撰寫一個獲取微信帶引數的二維碼給到前端做展示,
4、前端拿到二維碼做展示,并且得到隨機生成的引數輪詢查看是否已登錄,
5、撰寫檢查是否已登錄的介面給到前端進行判斷,(如果能從Redis里拿到隨機引數,說明已經登錄了)
6、當用戶掃碼關注了服務號,則得到微信的回呼,當用戶關注服務號時,會把隨機引數和openId傳給服務器,我則將資訊存入Redis,
7、前端得知已登錄后,將用戶資訊寫入localStorage
最后
每次代碼存在遇到“優雅”的寫法時,我都會懊惱自己怎么不會,還吭哧吭哧地寫這破代碼這么多年了,特別是Map.computeIfAbsent
這個,我感覺沒理由我不知道呀,我從初學到現在作業主要用JDK 1.8
,沒道理我現在才知道寫這個玩意,
有的時候都感覺我是不是已經是老古董了,新世界已經沒有承載我的船了,
不過寫開源專案有一大好處是,只要我的專案有人用,能大大提高我獲取“優雅”寫法的概率,這也是我一直推廣自己專案的一個原因之一,
如果想學Java專案的,強烈推薦我的開源專案訊息推送平臺Austin(8K stars) ,可以用作畢業設計,可以用作校招,可以看看生產環境是怎么推送訊息的,開源專案訊息推送平臺austin倉庫地址:
更多的文章可往:文章的目錄導航訊息推送平臺??推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別,
- https://gitee.com/zhongfucheng/austin/
- https://github.com/ZhongFuCheng3y/austin
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/551905.html
標籤:其他
上一篇:京東面經總結
下一篇:返回列表