原來Spring能注入集合和Map的computeIfAbsent是這麼好用!

Java3y發表於2023-05-08

大家好,我是3y,今天繼續來聊我的開源專案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)));
}

這看著真簡潔啊,好像已經很完美了,本來有好幾行的程式碼,最佳化了下變成了一行。

但我又思考了下,這個putIfAbsentV我這邊傳入的是一個方法,每次這個方法都會執行的(不論我的Map裡有沒有這個K),這又感覺不太優雅了

我又點進去computeIfAbsent看了下,嗯!這就是我想要的了:如果MapV不存在時,才去執行我生成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 = mappingFunction.apply(key)) != null) {
            put(key, newValue);
            return newValue;
        }
    }
    return v;
}

(這個其實我在學lambdastream流的時候曾經是體驗過的,我日常也會簡單寫點,只是不知道在JDKMap也有這樣的方法。)於是,最後的程式碼就成了:

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來修復ConcurrentHashMapcomputeIfAbsent存在效能的問題。呀,不小心又學到了點東西。

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倉庫地址:

訊息推送平臺?推送下發【郵件】【簡訊】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別

相關文章