在线观看www成人影院-在线观看www日本免费网站-在线观看www视频-在线观看操-欧美18在线-欧美1级

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

關(guān)于編程模式的總結(jié)與思考

OSC開源社區(qū) ? 來源: 大淘寶技術(shù) ? 2024-01-03 10:14 ? 次閱讀

淘寶創(chuàng)新業(yè)務(wù)的優(yōu)化迭代是非常高頻且迅速的,在這過程中要求技術(shù)也必須是快且穩(wěn)的,而為了適應(yīng)這種快速變化的節(jié)奏,我們在項目開發(fā)過程中采用了一些面向拓展以及敏捷開發(fā)的設(shè)計,本文旨在總結(jié)并思考其中一些通用的編程模式。

前言

靜心守護業(yè)務(wù)是淘寶今年4月份啟動的創(chuàng)新項目,項目的核心邏輯是通過敲木魚、冥想、盤手串等療愈玩法為用戶帶來內(nèi)心寧靜的同時推動文物的保護與修復(fù),進一步弘揚我們的傳統(tǒng)文化。

作為創(chuàng)新項目,業(yè)務(wù)形態(tài)與產(chǎn)品方案的優(yōu)化迭代是非常高頻且迅速的:項目從4月底投入開發(fā)到7月份最終外灰,整體方案經(jīng)歷過大的推倒重建,也經(jīng)歷過多輪小型重構(gòu)優(yōu)化,項目上線后也在做持續(xù)的迭代優(yōu)化甚至改版升級。

模式清單

基于Spring容器與反射的策略模式

策略模式是一種經(jīng)典的行為設(shè)計模式,它的本質(zhì)是定義一系列算法, 并將每種算法分別放入獨立的類中, 以使算法的對象能夠相互替換,后續(xù)也能根據(jù)需要靈活拓展出新的算法。這里推薦的是一種基于Spring容器和反射結(jié)合的策略模式,這種模式的核心思路是:每個策略模式的實現(xiàn)都是一個bean,在Spring容器啟動時基于反射獲取每個策略場景的接口類型,并基于該接口類型再獲取此類型的所有策略實現(xiàn)bean并記錄到一個map(key為該策略bean的唯一標(biāo)識符,value為bean對象)中,后續(xù)可以自定義路由策略來從該map中獲取bean對象并使用相應(yīng)的策略。

模式解構(gòu)

模式具體實現(xiàn)方式大致如下面的UML類圖所描述的:

07fb23a4-a964-11ee-8b88-92fbcf53809c.png

其中涉及的各個組件及作用分別為:

Handler(interface):策略的頂層接口,定義的type方法表示策略唯一標(biāo)識的獲取方式。

HandlerFactory(abstract class):策略工廠的抽象實現(xiàn),封裝了反射獲取Spring bean并維護策略與其標(biāo)識映射的邏輯,但不感知策略的真實類型。

AbstractHandler(interface or abstracr class):各個具體場景下的策略接口定義,該接口定義了具體場景下策略所需要完成的行為。如果各個具體策略實現(xiàn)有可復(fù)用的邏輯,可以結(jié)合模版方法模式在該接口內(nèi)定義模版方法,如果模板方法依賴外部bean注入,則該接口的類型需要為abstract class,否則為interface即可。

HandlerImpl(class):各個場景下策略接口的具體實現(xiàn),承載主要的業(yè)務(wù)邏輯,也可以根據(jù)需要橫向拓展。

HandlerFactoryImpl(class):策略工廠的具體實現(xiàn),感知具體場景策略接口的類型,如果有定制的策略路由邏輯也可以在此實現(xiàn)。

這種模式的主要優(yōu)點有:

策略標(biāo)識維護自動化:策略實現(xiàn)與標(biāo)識之間的映射關(guān)系完全委托給Spring容器進行維護(在HandlerFactory中封裝,每個場景的策略工廠直接繼承該類即可,無需重復(fù)實現(xiàn)),后續(xù)新增策略不用再手動修改關(guān)系映射。

場景維度維護標(biāo)識映射:HandlerFactory中在掃描策略bean時是按照AbstractHandler的類型來分類維護的,從而避免了不同場景的同名策略發(fā)生沖突。

策略接口按場景靈活定義:具體場景的策略行為定義在AbstractHandler中,在這里可以根據(jù)真實的業(yè)務(wù)需求靈活定義行為,甚至也可以結(jié)合其他設(shè)計模式做進一步抽象處理,在提供靈活拓展的同時減少重復(fù)代碼。

實踐案例分析

該模式在靜心守護項目中的許多功能模塊都有使用,下面以稱號解鎖模塊為例來介紹其實際應(yīng)用。

我們先簡單了解下該模塊的業(yè)務(wù)背景:靜心守護的成就體系中有一類是稱號,如下圖。用戶可以通過多種行為去解鎖不同類型的稱號,比如說通過參與主玩法(敲木魚、冥想、盤手串),主玩法參與達到一定次數(shù)后即可解鎖特定類型的稱號。當(dāng)然后續(xù)也可能會有其他種類的稱號:比如簽到類(按照用戶簽到天數(shù)解鎖)、捐贈類(按照用戶捐贈項目的行為解鎖),所以對于稱號的解鎖操作應(yīng)該是面向未來可持續(xù)拓展的。

基于這樣的思考,我選擇使用上面的策略模式去實現(xiàn)稱號解鎖模塊。該模塊的核心類圖組織如下:

083ae908-a964-11ee-8b88-92fbcf53809c.png

下面是其中部分核心代碼的分析解讀:

public interface Handler {
    /**
     * handler類型
     *
     * @return
     */
    T type();
}
如上文所說,Handler是策略的頂層抽象,它只定義了type方法,該方法用于獲取策略的標(biāo)識,標(biāo)識的類型支持子接口定義。
@Slf4j
public abstract class HandlerFactory> implements InitializingBean, ApplicationContextAware {
    private Map handlerMap;


    private ApplicationContext appContext;


    /**
     * 根據(jù) type 獲得對應(yīng)的handler
     *
     * @param type
     * @return
     */
    public H getHandler(T type) {
        return handlerMap.get(type);
    }


    /**
     * 根據(jù) type 獲得對應(yīng)的handler,支持返回默認(rèn)
     *
     * @param type
     * @param defaultHandler
     * @return
     */
    public H getHandlerOrDefault(T type, H defaultHandler) {
        return handlerMap.getOrDefault(type, defaultHandler);
    }


    /**
     * 反射獲取泛型參數(shù)handler類型
     *
     * @return handler類型
     */
    @SuppressWarnings("unchecked")
    protected Class getHandlerType() {
        Type type = ((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[1];
        //策略接口使用了范型參數(shù)
        if (type instanceof ParameterizedTypeImpl) {
            return (Class) ((ParameterizedTypeImpl)type).getRawType();
        } else {
            return (Class) type;
        }
    }


    @Override
    public void afterPropertiesSet() {
        // 獲取所有 H 類型的 handlers
        Collection handlers = appContext.getBeansOfType(getHandlerType()).values();


        handlerMap = Maps.newHashMapWithExpectedSize(handlers.size());


        for (final H handler : handlers) {
            log.info("HandlerFactory {}, {}", this.getClass().getCanonicalName(), handler.type());
            handlerMap.put(handler.type(), handler);
        }
        log.info("handlerMap:{}", JSON.toJSONString(handlerMap));


    }


    @Override
    public void setApplicationContext(@Nonnull ApplicationContext applicationContext) throws BeansException {
        this.appContext = applicationContext;
    }
}

HandlerFactory在前面也提到過,是策略工廠的抽象實現(xiàn),封裝了反射獲取具體場景策略接口類型,并查找策略bean在內(nèi)存中維護策略與其標(biāo)識的映射關(guān)系,后續(xù)可以直接通過標(biāo)識或者對應(yīng)的策略實現(xiàn)。這里有二個細節(jié):

為什么HandlerFactory是abstract class?其實可以看到該類并沒有任何抽象方法,直接將其定義為class也不會有什么問題。這里將其定義為abstract class主要是起到實例創(chuàng)建的約束作用,因為我們對該類的定義是工廠的抽象實現(xiàn),只希望針對具體場景來創(chuàng)建實例,針對該工廠本身創(chuàng)建實例其實是沒有任何實際意義的。

getHandlerType方法使用了@SuppressWarnings注解并標(biāo)記了unchecked。這里也確實是存在潛在風(fēng)險的,因為Type類型轉(zhuǎn)Class類型屬于向下類型轉(zhuǎn)換,是存在風(fēng)險的,可能其實際類型并非Class而是其他類型,那么此處強轉(zhuǎn)就會出錯。這里處理了兩種最通用的情況:AbstractHandler是帶范型的class和最普通的class。

@Component
public class TitleUnlockHandlerFactory
        extends HandlerFactory> {}
TitleUnlockHandlerFactory是策略工廠的具體實現(xiàn),由于不需要在此定制策略的路由邏輯,所以只聲明了相關(guān)的參數(shù)類型,而沒有對父類的方法做什么覆蓋。
public abstract class BaseTitleUnlockHandler implements Handler {


    @Resource
    private UserTitleTairManager userTitleTairManager;


    @Resource
    private AchievementCountManager achievementCountManager;


    @Resource
    private UserUnreadAchievementTairManager userUnreadAchievementTairManager;


    ......


    /**
     * 解鎖稱號
     *
     * @param params
     * @return
     */
    public @CheckForNull TitleUnlockResult unlockTitles(T params) {
        TitleUnlockResult titleUnlockResult = this.doUnlock(params);
        if (null == titleUnlockResult) {
            return null;
        }


        List titleAchievements = titleUnlockResult.getUnlockedTitles();
        if (CollectionUtils.isEmpty(titleAchievements)) {
            titleUnlockResult.setUnlockedTitles(new ArrayList<>());
            return titleUnlockResult;
        }


        //基于注入的bean和計算出的稱號列表進行后置操作,如:更新成就計數(shù)、更新用戶稱號緩存、更新用戶未讀成就等
        ......


        return titleUnlockResult;
    }


    /**
     * 計算出要解鎖的稱號
     *
     * @param param
     * @return
     */
    protected abstract TitleUnlockResult doUnlock(T param);


    @Override
    public abstract String type();


}

BaseTitleUnlockHandler定義了稱號解鎖行為,并且在此確定了策略標(biāo)識的類型為String。此外,該類是一個abstract class,是因為該類定義了一個模版方法unlockTitles,在該方法里封裝了稱號解鎖所要進行的一些公共操作,比如更新用戶的稱號計數(shù)、用戶的稱號緩存數(shù)據(jù)等,這些都依賴于注入的一些外部bean,而interface不支持非靜態(tài)成員變量,所以該類通過abstract class來定義。具體的稱號解鎖行為通過doUnlock定義,這也是該策略的具體實現(xiàn)類需要實現(xiàn)的方法。

另外也許你還注意到了doUnlock方法的行參是一個范型參數(shù)T,因為我們考慮到了不同類型稱號解鎖所需要的參數(shù)可能是不同的,因此在場景抽象接口側(cè)只依賴于稱號解鎖的公共參數(shù)類型,而在策略接口具體實現(xiàn)側(cè)才與該類型策略的具體參數(shù)類型進行耦合。

@Component
public class GameplayTitleUnlockHandler extends BaseTitleUnlockHandler {


    @Resource
    private BlessTitleAchievementDiamondConfig blessTitleAchievementDiamondConfig;


    @Resource
    private UserTitleTairManager userTitleTairManager;


    @Override
    protected TitleUnlockResult doUnlock(GameplayTitleUnlockParams params) {
        //獲取稱號元數(shù)據(jù)
        List titleMetadata = blessTitleAchievementDiamondConfig.getTitleMetadata();


        if (CollectionUtils.isEmpty(titleMetadata)) {
            return null;
        }


        List titleAchievements = new ArrayList<>();


        Result result = userTitleTairManager.queryRawCache(params.getUserId());


        //用戶稱號數(shù)據(jù)查詢異常
        if (null == result || !result.isSuccess()) {
            return null;
        }


        if (Objects.equals(result.getRc(), ResultCode.SUCCESS)) {
            //解鎖新稱號
            titleAchievements = unlockNewTitles(params, titleMetadata);


        } else if (Objects.equals(result.getRc(), ResultCode.DATANOTEXSITS)) {
            //初始化歷史稱號
            titleAchievements = initHistoricalTitles(params, titleMetadata);


        }


        TitleUnlockResult titleUnlockResult = new TitleUnlockResult();
        titleUnlockResult.setUserTitleCache(result);
        titleUnlockResult.setUnlockedTitles(titleAchievements);
        return titleUnlockResult;
    }


    @Override
    public String type() {
        return TitleType.GAMEPLAY;
    }


    ......
}

上面是一個策略的具體實現(xiàn)類的大致示例,可以看到該實現(xiàn)類核心明確了以下信息:

策略標(biāo)識:給出了type方法的具體實現(xiàn),返回了一個策略標(biāo)識的常量

策略處理邏輯:此處是玩法類稱號解鎖的業(yè)務(wù)邏輯,讀者無需關(guān)注其細節(jié)

稱號解鎖行參:給出了玩法類稱號解鎖所需的真實參數(shù)類型

抽象疲勞度管控體系

在我們的業(yè)務(wù)需求中經(jīng)常會遇到涉及疲勞度管控相關(guān)的邏輯,比如每日簽到允許用戶每天完成1次、首頁項目進展彈窗要求對所有用戶只彈1次、首頁限時回訪任務(wù)入口則要對用戶每天都展示一次,但用戶累計完成3次后便不再展示......因此我們設(shè)計了一套疲勞度管控的模式,以降低后續(xù)諸如上述涉及疲勞度管控相關(guān)需求的開發(fā)成本。

自頂向下的視角

這套疲勞度管控體系的類層次大致如下圖: 0856ab3e-a964-11ee-8b88-92fbcf53809c.png ? 接下來我們自頂向下逐層進行介紹:

FatigueLimiter(interface):FatigueLimiter是最頂層抽象的疲勞度管控接口,它定義了疲勞度管控相關(guān)的行為,比如:疲勞度的查詢、疲勞度清空、疲勞度增加、是否達到疲勞度限制的判斷等。

BaseFatigueLdbLimiter(abstract class):疲勞度數(shù)據(jù)的存儲方案可以是多種多樣的,在我們項目中主要利用ldb進行疲勞度存儲,而BaseFatigueLdbLimiter正是基于ldb【注:阿里內(nèi)部自研的一款持久化k-v數(shù)據(jù)庫,讀者可將其理解為類似level db的項目】對疲勞度數(shù)據(jù)進行管控的抽象實現(xiàn),它封裝了ldb相關(guān)的操作,并基于ldb的數(shù)據(jù)操作實現(xiàn)了FatigueLimiter的疲勞度管控方法。但它并不感知具體業(yè)務(wù)的身份和邏輯,因此定義了幾個業(yè)務(wù)相關(guān)的方法交給下層去實現(xiàn),分別是:

scene:標(biāo)識具體業(yè)務(wù)的場景,會利用該方法返回值去構(gòu)造Ldb存儲的key

buildCustomKey:對Ldb存儲key的定制邏輯

getExpireSeconds:對應(yīng)著Ldb存儲kv失效時間,對應(yīng)著疲勞度的管控周期

Ldb周期性疲勞度管控的解決方案層(abstract class):在這一層提供了多種周期的開箱即用的疲勞度管控實現(xiàn)類,如BaseFatigueDailyLimiter提供的是天級別的疲勞度管控能力,BaseFatigueNoCycleLimiter則表示疲勞度永不過期,而BaseFatigueCycleLimiter則支持用戶實現(xiàn)cycle方法定制疲勞度周期。

業(yè)務(wù)場景層:這一層則是各個業(yè)務(wù)場景對疲勞度管控的具體實現(xiàn),實現(xiàn)類只需要實現(xiàn)scene方法來聲明業(yè)務(wù)場景的身份標(biāo)識,隨后繼承對應(yīng)的解決方案,即可實現(xiàn)快速的疲勞度管控。比如上面的DailyWishSignLimiter就對應(yīng)著本篇開頭我們所說的“每日簽到允許用戶每天完成1次”,這就要求為用戶的簽到行為以天維度構(gòu)建key同時失效時間也為1天,因此直接繼承解決方案層的BaseFatigueDailyLimiter即可。其代碼實現(xiàn)非常簡單,如下:

@Component
public class DailyWishSignLimiter extends BaseFatigueLdbDailyLimiter {


    @Override
    protected String scene() {
        return LimiterScene.dailyWish;
    }
}

有一個“異類”

也許你注意到了上面的類層次圖中有一個“異類”——HomeEnterGuideLimiter。它其實就是我們在上文說的“首頁限時回訪任務(wù)入口則要對用戶每天都展示一次,但用戶累計完成3次后便不再展示”,它的邏輯其實也很簡單:因為它有2條管控條件,所以需要繼承2個管控周期的解決方案——天維度和永久維度,最后實際使用的類再聚合了天維度和永久維度的實現(xiàn)類(每個實現(xiàn)類對應(yīng)ldb的一類key)并實現(xiàn)了頂層的疲勞度管控接口,標(biāo)識這也是一個疲勞度管理器。它們的代碼如下:

/**
 * 首頁入口引導(dǎo)限時任務(wù)-天級疲勞度管控
 *
 */
@Component
public class HomeEnterGuideDailyLimiter extends BaseFatigueLdbDailyLimiter {


    @Override
    protected String scene() {
        return LimiterScene.homeEnterGuide;
    }
}


/**
 * 首頁入口引導(dǎo)限時任務(wù)-總次數(shù)疲勞度管控
 *
 */
@Component
public class HomeEnterGuideNoCycleLimiter extends BaseFatigueLdbNoCycleLimiter {


    @Override
    protected String scene() {
        return LimiterScene.homeEnterGuide;
    }


    @Override
    protected int maxSize() {
        return 3;
    }
}


/**
 * 首頁入口引導(dǎo)限時任務(wù)-疲勞度服務(wù)
 *
 */
@Component
public class HomeEnterGuideLimiter implements FatigueLimiter {


    @Resource
    private FatigueLimiter homeEnterGuideDailyLimiter;


    @Resource
    private FatigueLimiter homeEnterGuideNoCycleLimiter;


    @Override
    public boolean isLimit(String customKey) {
        return homeEnterGuideNoCycleLimiter.isLimit(customKey) || homeEnterGuideDailyLimiter.isLimit(customKey);
    }


    @Override
    public Integer incrLimit(String customKey) {
        homeEnterGuideDailyLimiter.incrLimit(customKey);
        return homeEnterGuideNoCycleLimiter.incrLimit(customKey);
    }


    @Override
    public boolean isLimit(Integer fatigue) {
        throw new UnsupportedOperationException();
    }


    @Override
    public Map batchQueryLimit(List keys) {
        throw new UnsupportedOperationException();
    }


    @Override
    public void removeLimit(String customKey) {
        homeEnterGuideDailyLimiter.removeLimit(customKey);
        homeEnterGuideNoCycleLimiter.removeLimit(customKey);
    }


    @Override
    public Integer queryLimit(String customKey) {
        throw new UnsupportedOperationException();
    }


    /**
     * 查詢首頁限時任務(wù)的每日疲勞度
     *
     * @param customKey 用戶自定義key
     * @return 疲勞度計數(shù)
     */
    public Integer queryDailyLimit(String customKey) {
        return homeEnterGuideDailyLimiter.queryLimit(customKey);
    }


    /**
     * 查詢首頁限時任務(wù)的全周期疲勞度
     *
     * @param customKey 用戶自定義key
     * @return 疲勞度計數(shù)
     */
    public Integer queryNoCycleLimit(String customKey) {
        return homeEnterGuideNoCycleLimiter.queryLimit(customKey);
    }
}

函數(shù)式行為參數(shù)化

Java 21在今年9月份發(fā)布了,而距離Java 8發(fā)布已經(jīng)過去9年多了,但也許,我是說也許......我們有些同學(xué)對Java 8還是不太熟悉......

再談行為參數(shù)化

最早聽到“行為參數(shù)化”這個詞是在經(jīng)典的Java技術(shù)書籍《Java 8實戰(zhàn)》中。在此書中,作者以一個篩選蘋果的案例,基于行為參數(shù)化的思維一步步優(yōu)化重構(gòu)代碼,在提升代碼抽象能力的同時,保證了代碼的簡潔性和可讀性,而其中的秘密武器就是Java 8所引入的Lambda表達式和函數(shù)式接口。Java 8發(fā)布已經(jīng)9年,對于Lambda表達式,大多數(shù)同學(xué)都已經(jīng)耳熟能詳,但函數(shù)式接口也許有同學(xué)不知道代表著什么。簡單來說,如果一個接口,它只有一個沒有被實現(xiàn)的方法,那它就是函數(shù)式接口。java.lang.function包下定義JDK提供的一系列函數(shù)式接口。如果一個接口是函數(shù)式接口,推薦用@FunctionalInterface注解來顯式標(biāo)明。那函數(shù)式接口有什么用呢?如果一個方法的行參里有函數(shù)式接口,那么函數(shù)式接口對應(yīng)的參數(shù)可以支持傳遞Lambda表達式或者方法引用。 那何為“行為參數(shù)化”?直觀地來說就是將行為作為方法/函數(shù)的參數(shù)來進行傳遞。在Java 8之前,這可以通過匿名類實現(xiàn),而在Java 8以后,可以基于函數(shù)式特性來實現(xiàn)行為參數(shù)化,即方法參數(shù)定義為函數(shù)式接口,在具體傳參時使用Lambda表達式/方法。相比匿名類,后者在簡潔性上有極大的提升。 在我們的日常開發(fā)中,如果我們看到兩個方法的結(jié)構(gòu)十分相似,只有其中部分行為存在差別,那么就可以考慮采用函數(shù)式的行為參數(shù)化來重構(gòu)優(yōu)化這段代碼,將其中存在差異的行為抽象成參數(shù),從而減少重復(fù)代碼。

從實踐中來,到代碼中去

下面給出一個例子。在靜心守護項目中,我們基于ldb維護了用戶未讀成就的列表,在用戶進入到個人成就頁時,會查詢未讀成就數(shù)據(jù),并對未讀的成就在成就列表進行置頂以及加紅點展示。下面是對用戶未讀成就列表進行新增和清除的兩個方法:

/**
 * 清除未讀成就
 *
 * @param uid             用戶ID
 * @param achievementType 需要清除未讀成就列表的成就類型
 * @return
 */
public boolean clearUnreadAchievements(long uid, Set achievementTypes) {


    if (CollectionUtils.isEmpty(achievementTypes)) {
        return true;
    }


    Result ldbRes = super.rawGet(buildKey(uid), false);


    //用戶稱號數(shù)據(jù)查詢失敗
    if (Objects.isNull(ldbRes)) {
        recordErrorCode(InteractErrorCode.UNREAD_ACHIEVEMENT_UPSERT_ERROR, ExceptionBizParams.builder().uid(uid).build());
        return false;
    }


    boolean success = false;


    ResultCode resultCode = ldbRes.getRc();


    //不存在用戶稱號數(shù)據(jù)則進行初始化
    if (Objects.equals(resultCode, ResultCode.DATANOTEXSITS)) {
    UserUnreadAchievementsCache userUnreadAchievementsCache = new UserUnreadAchievementsCache();
        achievementTypes.forEach(type -> clearCertainTypeIds(userUnreadAchievementsCache, type));
        success = putCache(uid, userUnreadAchievementsCache, DEFAULT_VERSION);


    } else if (Objects.equals(resultCode, ResultCode.SUCCESS)) {


        DataEntry ldbEntry = ldbRes.getValue();


        //存在新數(shù)據(jù)則對其進行更新
        if (Objects.nonNull(ldbEntry)) {
            Object data = ldbEntry.getValue();


            if (data instanceof String) {
                UserUnreadAchievementsCache userUnreadAchievementsCache = JSON.parseObject(String.valueOf(data), UserUnreadAchievementsCache.class);
                achievementTypes.forEach(type -> clearCertainTypeIds(userUnreadAchievementsCache, type))
                success = putCache(uid, userUnreadAchievementsCache, ldbEntry.getVersion());
            }
        }
    }
    //緩存解鎖的稱號失敗
    if (!success) {
        recordErrorCode(InteractErrorCode.UNREAD_ACHIEVEMENT_UPSERT_ERROR, ExceptionBizParams.builder().uid(uid).build());
    }
    return success;
}
/**
 * 寫入新的未讀成就
 *
 * @param uid                  用戶ID
 * @param achievementTypeIdMap 需要新增的成就類型和成就ID列表的映射
 * @return
 */
public boolean writeUnreadAchievements(long uid, Map> achievementTypeIdMap) {


    if (MapUtils.isEmpty(achievementTypeIdMap)) {
        return true;
    }


    Result ldbRes = super.rawGet(buildKey(uid), false);


    //用戶稱號數(shù)據(jù)查詢失敗
    if (Objects.isNull(ldbRes)) {
        recordErrorCode(InteractErrorCode.UNREAD_ACHIEVEMENT_UPSERT_ERROR, ExceptionBizParams.builder().uid(uid).build());
        return false;
    }


    boolean success = false;


    ResultCode resultCode = ldbRes.getRc();


    //不存在用戶稱號數(shù)據(jù)則進行初始化
    if (Objects.equals(resultCode, ResultCode.DATANOTEXSITS)) {
    UserUnreadAchievementsCache userUnreadAchievementsCache = new UserUnreadAchievementsCache();
        achievementTypeIdMap.forEach((key, value) -> updateCertainTypeIds(userUnreadAchievementsCache, key, value));
        success = putCache(uid, userUnreadAchievementsCache, DEFAULT_VERSION);


    } else if (Objects.equals(resultCode, ResultCode.SUCCESS)) {


        DataEntry ldbEntry = ldbRes.getValue();


        //存在新數(shù)據(jù)則對其進行更新
        if (Objects.nonNull(ldbEntry)) {
            Object data = ldbEntry.getValue();


            if (data instanceof String) {
                UserUnreadAchievementsCache userUnreadAchievementsCache = JSON.parseObject(String.valueOf(data), UserUnreadAchievementsCache.class);
                achievementTypeIdMap.forEach((key, value) -> updateCertainTypeIds(oldCache, key, value));
                success = putCache(uid, userUnreadAchievementsCache, ldbEntry.getVersion());
            }
        }
    }
    //緩存解鎖的稱號失敗
    if (!success) {
        recordErrorCode(InteractErrorCode.UNREAD_ACHIEVEMENT_UPSERT_ERROR, ExceptionBizParams.builder().uid(uid).build());
    }
    return success;
}

從結(jié)構(gòu)上看,上面兩段代碼其實是非常類似的:整個結(jié)構(gòu)都是先判空,然后查詢歷史的未讀成就數(shù)據(jù),如果數(shù)據(jù)未初始化,則進行初始化,如果已經(jīng)初始化,則對數(shù)據(jù)進行更新。只不過寫入/清除對數(shù)據(jù)的初始化和更新邏輯并不相同。因此可以將數(shù)據(jù)初始化和更新抽象為行為參數(shù),將剩余部分提取為公共方法,基于這樣的思路重構(gòu)后的代碼如下:

/**
 * 創(chuàng)建or更新緩存
 *
 * @param uid               用戶ID
 * @param initCacheSupplier 緩存初始化策略
 * @param updater           緩存更新策略
 * @return
 */
private boolean upsertCache(long uid, Supplier initCacheSupplier,
                            Function updater) {


    Result ldbRes = super.rawGet(buildKey(uid), false);


    //用戶稱號數(shù)據(jù)查詢失敗
    if (Objects.isNull(ldbRes)) {
        recordErrorCode(InteractErrorCode.UNREAD_ACHIEVEMENT_UPSERT_ERROR, ExceptionBizParams.builder().uid(uid).build());
        return false;
    }


    boolean success = false;


    ResultCode resultCode = ldbRes.getRc();


    //不存在用戶稱號數(shù)據(jù)則進行初始化
    if (Objects.equals(resultCode, ResultCode.DATANOTEXSITS)) {


        UserUnreadAchievementsCache userUnreadAchievementsCache = initCacheSupplier.get();
        success = putCache(uid, userUnreadAchievementsCache, DEFAULT_VERSION);


    } else if (Objects.equals(resultCode, ResultCode.SUCCESS)) {


        DataEntry ldbEntry = ldbRes.getValue();


        //存在新數(shù)據(jù)則對其進行更新
        if (Objects.nonNull(ldbEntry)) {
            Object data = ldbEntry.getValue();


            if (data instanceof String) {
                UserUnreadAchievementsCache userUnreadAchievementsCache = JSON.parseObject(String.valueOf(data), UserUnreadAchievementsCache.class);
                userUnreadAchievementsCache = updater.apply(userUnreadAchievementsCache);
                success = putCache(uid, userUnreadAchievementsCache, ldbEntry.getVersion());
            }
        }
    }
    //緩存解鎖的稱號失敗
    if (!success) {
        recordErrorCode(InteractErrorCode.UNREAD_ACHIEVEMENT_UPSERT_ERROR, ExceptionBizParams.builder().uid(uid).build());
    }
    return success;
}


/**
 * 寫入新的未讀成就
 *
 * @param uid                  用戶ID
 * @param achievementTypeIdMap 需要新增的成就類型和成就ID列表的映射
 * @return
 */
public boolean writeUnreadAchievements(long uid, Map> achievementTypeIdMap) {


    if (MapUtils.isEmpty(achievementTypeIdMap)) {
        return true;
    }


    return upsertCache(uid,
            () -> {
                UserUnreadAchievementsCache userUnreadAchievementsCache = new UserUnreadAchievementsCache();
                achievementTypeIdMap.forEach((key, value) -> updateCertainTypeIds(userUnreadAchievementsCache, key, value));
                return userUnreadAchievementsCache;
            },
            oldCache -> {
                achievementTypeIdMap.forEach((key, value) -> updateCertainTypeIds(oldCache, key, value));
                return oldCache;
            }
    );
}


/**
 * 清除未讀成就
 *
 * @param uid             用戶ID
 * @param achievementType 需要清除未讀成就列表的成就類型
 * @return
 */
public boolean clearUnreadAchievements(long uid, Set achievementTypes) {


    if (CollectionUtils.isEmpty(achievementTypes)) {
        return true;
    }


    return upsertCache(uid,
            () -> {
                UserUnreadAchievementsCache userUnreadAchievementsCache = new UserUnreadAchievementsCache();
                achievementTypes.forEach(type -> clearCertainTypeIds(userUnreadAchievementsCache, type));
                return userUnreadAchievementsCache;
            },
            oldCache -> {
                achievementTypes.forEach(type -> clearCertainTypeIds(oldCache, type));
                return oldCache;
            }
    );
}

重構(gòu)的核心是提取了upsert方法,該方法將緩存數(shù)據(jù)的初始化和更新策略以函數(shù)式接口進行定義,從而支持從調(diào)用側(cè)進行透傳,避免了模板方法的重復(fù)編寫。這是一個拋磚引玉的例子,在日常開發(fā)中,我們可以更多地嘗試用函數(shù)式編程的思維去思考和重構(gòu)代碼,也許會發(fā)現(xiàn)另一個神奇的編程世界。

切面編程的一些實踐

AOP想必大家都已經(jīng)十分熟悉了,在此便不再贅述其基本概念,而是開門見山直接分享一些AOP在靜心守護項目中的實際應(yīng)用。

服務(wù)層異常統(tǒng)一收口

靜心守護項目采用了在阿里系統(tǒng)中常用的service-manager-dao的分層模式,其中service層是距離終端最近的一層。為了防止下層預(yù)期外的異常拋到終端,我們需要在service層對異常進行統(tǒng)一攔截并且記錄,同時最好將相關(guān)的錯誤碼、請求參數(shù)以及traceId都一并記下,便于問題排查。這個場景就非常適合使用AOP。在引入AOP之前,我們需要對每個service中面向終端的方法都進行異常攔截和監(jiān)控日志打印的操作。比方說下面這個類,它有3個面向終端mtop【注:阿里內(nèi)部自研的API網(wǎng)關(guān)平臺】服務(wù)的方法(api具體參數(shù)和名稱做了模糊化處理),這3個方法都采用了同樣的try-catch結(jié)構(gòu)來進行異常捕捉和監(jiān)控日志打印,其中存在大量的重復(fù)代碼,而更糟糕的事,如果后續(xù)增加新的方法,這樣的重復(fù)代碼還會不斷增加。

@Slf4j
@HSFProvider(serviceInterface = MtopBlessHomeService.class)
public class MtopBlessHomeServiceImpl implements MtopBlessHomeService {


    //依賴的bean注入
  ......


    @Override
    public MtopResult entranceA(EntranceARequest request) {
        try {
            startDiagnose(request.getUserId());


            //該入口下的業(yè)務(wù)邏輯
            ......


        } catch (InteractBizException e) {
            log.error("Service invoke fail. Method name:{}, params:{}, errorCode:{}, trace:{}",
                    "MtopBlessHomeServiceImpl.entranceA", buildMethodParamsStr(request), e.getErrCode(), EagleEye.getTraceId());
            recordErrorCode(e);
            return MtopUtils.errMtopResult(e.getErrCode(), e.getErrMsg());
        } catch (Exception e) {
            log.error("Service invoke fail. Method name:{}, params:{}, trace:{}",
                    "MtopBlessHomeServiceImpl.entranceA", buildMethodParamsStr(request), EagleEye.getTraceId(), e);
            recordErrorCode(InteractErrorCode.SYSTEM_ERROR, ExceptionBizParams.builder().build());
            return MtopUtils.sysErrMtopResult();
        } finally {
            DiagnoseClient.end();
        }
    }


    @Override
    public MtopResult entranceB(EntranceBRequest request) {
        try {
            startDiagnose(request.getUserId());


            //該入口下的業(yè)務(wù)邏輯
            ......


        } catch (InteractBizException e) {
            log.error("Service invoke fail. Method name:{}, params:{}, errorCode:{}, trace:{}",
                    "MtopBlessHomeServiceImpl.entranceB", buildMethodParamsStr(request), e.getErrCode(), EagleEye.getTraceId());
            recordErrorCode(e);
            return MtopUtils.errMtopResult(e.getErrCode(), e.getErrMsg());
        } catch (Exception e) {
            log.error("Service invoke fail. Method name:{}, params:{}, trace:{}",
                    "MtopBlessHomeServiceImpl.entranceB", buildMethodParamsStr(request), EagleEye.getTraceId(), e);
            recordErrorCode(InteractErrorCode.SYSTEM_ERROR, ExceptionBizParams.builder().build());
            return MtopUtils.sysErrMtopResult();
        } finally {
            DiagnoseClient.end();
        }
    }


    @Override
    public MtopResult entranceC(EntranceCRequest request) {
        try {
            startDiagnose(query.getUserId());


            //該入口下的業(yè)務(wù)邏輯
            ......


        } catch (InteractBizException e) {
            log.error("Service invoke fail. Method name:{}, params:{}, errorCode:{}, trace:{}",
                    "MtopBlessHomeServiceImpl.entranceC", buildMethodParamsStr(request), e.getErrCode(), EagleEye.getTraceId());
            recordErrorCode(e);
            return MtopUtils.errMtopResult(e.getErrCode(), e.getErrMsg());
        } catch (Exception e) {
            log.error("Service invoke fail. Method name:{}, params:{}, trace:{}",
                    "MtopBlessHomeServiceImpl.entranceC", buildMethodParamsStr(request), EagleEye.getTraceId(), e);
            recordErrorCode(InteractErrorCode.SYSTEM_ERROR, ExceptionBizParams.builder().build());
            return MtopUtils.sysErrMtopResult();
        } finally {
            DiagnoseClient.end();
        }    
    }


}
看到這樣重復(fù)的代碼結(jié)構(gòu)而只是局部行為的不同,也許我們可以考慮著用上一節(jié)的函數(shù)式行為參數(shù)化進行重構(gòu):將重復(fù)的代碼結(jié)構(gòu)抽取為公共的工具方法,將對manager層的調(diào)用抽象為行為參數(shù)。但在上述場景下,這種做法還是存在一些弊端:

每個服務(wù)的方法還是需要顯式調(diào)用工具類方法

為了保證監(jiān)控信息的齊全,還需要在參數(shù)里手動透傳一些監(jiān)控相關(guān)的信息

而AOP則不存在這些問題:AOP基于動態(tài)代理實現(xiàn),在實現(xiàn)上述邏輯時對服務(wù)層的代碼編寫完全透明。此外,AOP還封裝了調(diào)用端方法的各種元信息,可以輕松實現(xiàn)各種監(jiān)控信息的自動化打印。下面是我們提供的AOP切面。其中值得注意的點是切點的選擇要盡量準(zhǔn)確,避免增強了不必要的方法。下面我們選擇的切點是mtop包下所有Impl結(jié)尾類的public方法。

@Aspect
@Component
@Slf4j
public class MtopServiceAspect {


    /**
     * MtopService層服務(wù)
     */
    @Pointcut("execution(public com.taobao.mtop.common.MtopResult com.taobao.gaia.veyron.bless.service.mtop.*Impl.*(..))")
    public void mtopService(){}


    /**
     * 對mtop服務(wù)進行增強
     *
     * @param pjp 接入點
     * @return
     * @throws Throwable
     */
    @Around("com.taobao.gaia.veyron.bless.aspect.MtopServiceAspect.mtopService()")
    public Object enhanceService(ProceedingJoinPoint pjp) throws Throwable {
        try {
            startDiagnose(pjp);
            return pjp.proceed();
        } catch (InteractBizException e) {
            log.error("Service invoke fail. Method name:{}, params:{}, errorCode:{}, trace:{}",
                    AspectUtils.extractMethodName(pjp), buildMethodParamsStr(pjp), e.getErrCode(), EagleEye.getTraceId());
            recordErrorCode(e);
            return MtopUtils.errMtopResult(e.getErrCode(), e.getErrMsg());
        } catch (Exception e) {
            log.error("Service invoke fail. Method name:{}, params:{}, trace:{}",
                    AspectUtils.extractMethodName(pjp), buildMethodParamsStr(pjp), EagleEye.getTraceId(), e);
            recordErrorCode(InteractErrorCode.SYSTEM_ERROR, ExceptionBizParams.builder().build());
            return MtopUtils.sysErrMtopResult();
        } finally {
            DiagnoseClient.end();
        }
    }


}

存在這樣一個切面后,service層的代碼就可以變得非常簡潔:只需要純粹專注于業(yè)務(wù)邏輯。同樣以剛才的MtopBlessHomeServiceImpl類為例,在AOP改寫后的代碼里可以去除掉原先異常收口和監(jiān)控相關(guān)的內(nèi)容,而僅保留業(yè)務(wù)邏輯部分,代碼簡潔性大大提升。

@Slf4j
@HSFProvider(serviceInterface = MtopBlessHomeService.class)
public class MtopBlessHomeServiceImpl implements MtopBlessHomeService {


    //依賴的bean注入
  ......


    @Override
    public MtopResult entranceA(EntranceARequest request) {
        //業(yè)務(wù)邏輯
        ......
    }


    @Override
    public MtopResult entranceB(EntranceBRequest request) {
        //業(yè)務(wù)邏輯
        ......
    }


    @Override
    public MtopResult entranceC(EntranceCRequest request) {
        //業(yè)務(wù)邏輯
        ......
    }


}

切點選擇的策略

除了服務(wù)層以外,我們還想對數(shù)據(jù)訪問層進行監(jiān)控,監(jiān)控項目中各種數(shù)據(jù)存儲工具的RT以及成功率相關(guān)指標(biāo),并且監(jiān)控粒度要盡可能地貼近業(yè)務(wù)維度(整體的數(shù)據(jù)訪問監(jiān)控直接通過eagleeye查看即可),便于具體問題的定位排查。這種面向?qū)蛹墑e的邏輯定制,我們很自然而然地想到了AOP,這也正是它可以大顯身手的場景。 這節(jié)核心想要分享的則是切點的選擇。靜心守護項目的數(shù)據(jù)存儲主要依賴于Tair【注:阿里內(nèi)部自研的高性能K-V存儲系統(tǒng)。根據(jù)存儲介質(zhì)和使用場景不同又分為LDB、MDB、RDB】、Lindorm【注:阿里內(nèi)部自研的大規(guī)模云原生多模數(shù)據(jù)庫服務(wù)】和Mysql,這三種存儲工具在代碼中的使用各不相同,導(dǎo)致切點的選擇策略也大相徑庭。

目標(biāo)對象規(guī)律分布

如果我們要選擇增強的對象在項目中分布的非常規(guī)律,那么我們往往可以直接利用Spring AOP的PointCut語法來選擇切點。以靜心守護項目中的Mysql數(shù)據(jù)訪問對象為例:我們使用的ORM框架是mybatis,并且主要的用法是注解模式,所有的SQL邏輯都放在一個DAO包下,每個業(yè)務(wù)場景定義一個DAO結(jié)尾的Mapper接口,接口下的每個方法都對應(yīng)著一種數(shù)據(jù)訪問的方式。因此在切點選擇時,我們可以直接選擇DAO包下以DAO結(jié)尾的類,并選擇其中public方法即可準(zhǔn)確織入所有滿足條件的切點。

@Pointcut("execution(public * com.taobao.gaia.serverless.veyron.bless.dao.*DAO.*(..))")
public void charityProjectDataAccess() {
}

這樣實現(xiàn)的監(jiān)控粒度是具體到每個DAO對象-方法級別的粒度,監(jiān)控效果如下:

08798438-a964-11ee-8b88-92fbcf53809c.png

一個失效案例

靜心守護項目中對tair的使用方式是:通過一個抽象類對tair的各種基礎(chǔ)操作進行封裝(包括參數(shù)校驗、響應(yīng)判空、異常處理等),但將具體tair實例相關(guān)的參數(shù)設(shè)置行為抽象化,由實現(xiàn)類決定。各個業(yè)務(wù)場景的tair管理類最終會基于抽象類封裝的基礎(chǔ)操作來對tair進行數(shù)據(jù)訪問。 如下圖,AbstractLdbManager是封裝 08ad9b10-a964-11ee-8b88-92fbcf53809c.png

由于各個業(yè)務(wù)場景的tair管理實現(xiàn)類分散在各個業(yè)務(wù)包下,想要對它們進行統(tǒng)一切入比較困難。因此我們選擇對抽象類進行切入。但這樣就會遇到一個同類調(diào)用導(dǎo)致AOP失效的問題:抽象類本身不會有實例對象,因此基于CGLIB創(chuàng)建代理對象后,代理對象本質(zhì)上調(diào)用的還是各個業(yè)務(wù)場景tair管理類的對象,而在使用這些對象時,我們不會直接調(diào)用tair抽象類封裝的數(shù)據(jù)訪問方法,而是調(diào)用這些業(yè)務(wù)tair管理對象進一步封裝的帶業(yè)務(wù)語義的方法,基于這些方法再去調(diào)用tair抽象類的數(shù)據(jù)訪問方法。這種同類方法間接調(diào)用最終就導(dǎo)致了抽象類的方法沒有如期被增強。文字描述興許有些繞,可以參考下面的圖:

08c986a4-a964-11ee-8b88-92fbcf53809c.png

我們選擇的解決方法則是從上面的MultiClusterTairManager入手,這個類是tair為我們提供的TairManger的一種默認(rèn)實現(xiàn),我們之前的做法是為該類實例化一個bean,然后提供給所有業(yè)務(wù)Tair管理類使用,也就是說所有業(yè)務(wù)Tair管理類使用的TairManager都是同一個bean實例(因為業(yè)務(wù)流量沒那么大,一個tair實例暫時綽綽有余)。那么我們可以自己提供一個TairManager的實現(xiàn),基于繼承+組合MultiClusterTairManager的方式,只對我們項目內(nèi)用到數(shù)據(jù)訪問操作進行重寫,并委托給原先的MultiClusterTairManager bean進行處理。這樣我們可以在設(shè)置AOP切點時選擇對自己實現(xiàn)的TairManager的所有方法做增強,進而避開上面的問題。經(jīng)過這樣改寫后,上面的兩張圖會演變成下面這樣:

08c986a4-a964-11ee-8b88-92fbcf53809c.png

08fefed8-a964-11ee-8b88-92fbcf53809c.png

基于注解切入

還有一種場景是我們要增強的方法分布毫無規(guī)律,可能都在同一個類中,但方法的名稱毫無規(guī)律,也無法簡單通過private或者public來區(qū)別。針對這樣的場景,我們的做法是自定義注解,專門用于標(biāo)識需要做增強的方法。比如靜心守護項目中l(wèi)indorm相關(guān)的數(shù)據(jù)操作就是這樣。我們定義注解:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface VeyronJoinPoint {}

并將該注解標(biāo)識在需要增強的方法上,隨后通過下面的方式描述切點,即可獲取到所有需要增強的方法。

@Pointcut("@annotation(com.taobao.gaia.serverless.veyron.aspect.VeyronJoinPoint)")
public void lindormDataAccess() {}

上面的方法也有進一步改良的空間:在注解內(nèi)增加屬性來描述具體的業(yè)務(wù)場景,不同的切面根據(jù)業(yè)務(wù)場景來對捕獲的方法進行過濾,只留下當(dāng)前業(yè)務(wù)場景所需要的方法。不然按照現(xiàn)有的做法,如果新的切面也要基于注解來尋找切點,那只能定義新的注解,否則會與原先注解產(chǎn)生沖突。

總結(jié)

業(yè)務(wù)需求千變?nèi)f化,對應(yīng)的解法也見仁見智。在研發(fā)過程中對各種變化中不變的部分進行總結(jié),從中提取出自己的模式與方法論進行整理沉淀,會讓我們以后跑的更快。也正應(yīng)了學(xué)生時期,老師常說的那句話:“我們要把厚厚的書本讀薄才能裝進腦子里?!?/p>

審核編輯:湯梓紅

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 編程
    +關(guān)注

    關(guān)注

    88

    文章

    3671

    瀏覽量

    94675
  • 函數(shù)
    +關(guān)注

    關(guān)注

    3

    文章

    4364

    瀏覽量

    63814
  • 容器
    +關(guān)注

    關(guān)注

    0

    文章

    503

    瀏覽量

    22313
  • spring
    +關(guān)注

    關(guān)注

    0

    文章

    340

    瀏覽量

    14780

原文標(biāo)題:關(guān)于編程模式的總結(jié)與思考

文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    關(guān)于C++模板總結(jié)

    大家好,今天給大家分享一篇關(guān)于 C++ 模板總結(jié)概述。
    發(fā)表于 09-19 11:55 ?617次閱讀

    編程思考還是打字

    有些人的工作需要大量的思考,還有些人則只是敲敲代碼而已。其實這兩種人從事的是非常不同的工作,需要采取截然不同的方式進行管理。 有時編程就是打字“我們首先是個打字員,其次才是程序員”。很多業(yè)務(wù)
    發(fā)表于 12-16 17:22

    關(guān)于USB的知識總結(jié)

    關(guān)于USB的知識總結(jié)協(xié)議版本USB 協(xié)議版本有USB1.1、USB2.0,而目前公布的最新USB協(xié)議USB3.0,主要由于數(shù)據(jù)線的增加USB3.0 數(shù)據(jù)傳輸速度有了很大的提高。對于 USB1.1
    發(fā)表于 08-05 06:16

    關(guān)于Arduino的認(rèn)識與思考不看肯定后悔

    關(guān)于Arduino的認(rèn)識與思考不看肯定后悔
    發(fā)表于 09-26 07:28

    ARM嵌入式系統(tǒng)的問題總結(jié)分析

    摘要: 本文是作者關(guān)于嵌入式系統(tǒng)一些基本問題的思考總結(jié)。主要是從嵌入式處理器與硬件、ARM處
    發(fā)表于 11-17 18:28 ?842次閱讀

    DXP關(guān)于板層說明及總結(jié)

    DXP關(guān)于板層說明及總結(jié)DXP-設(shè)置板層(D+K )在PCB編輯 Design->layer Stack Manager(層管理)
    發(fā)表于 01-11 14:56 ?0次下載

    集成電路發(fā)展思考

    關(guān)于集成電路的一些個人的看法與思考總結(jié),個人觀點,僅供參考
    發(fā)表于 05-20 14:47 ?4次下載

    Linux下的網(wǎng)絡(luò)編程總結(jié)

    linux開發(fā)編程教程資料——Linux下的網(wǎng)絡(luò)編程總結(jié),感興趣的小伙伴們可以看一看。
    發(fā)表于 08-23 16:23 ?0次下載

    Java NIO (中文版)編程總結(jié)

    Java NIO 編程總結(jié)
    發(fā)表于 09-21 11:17 ?0次下載

    關(guān)于Linux下多線程編程技術(shù)學(xué)習(xí)總結(jié)

    Linux下多線程編程技術(shù) 作為一個IT人員,不斷的學(xué)習(xí)和總結(jié)是我們這個職業(yè)習(xí)慣,所以我會將每個階段的學(xué)習(xí)都會通過一點的總結(jié)來記錄和檢測自己的學(xué)習(xí)效果,今天為大家總結(jié)
    發(fā)表于 04-22 03:12 ?2325次閱讀
    <b class='flag-5'>關(guān)于</b>Linux下多線程<b class='flag-5'>編程</b>技術(shù)學(xué)習(xí)<b class='flag-5'>總結(jié)</b>

    AT燒錄軟件Progisp和使用手冊和對于ISP編程進入不了編程模式總結(jié)

    本文的主要內(nèi)容詳細介紹的是AT系列燒錄軟件Progisp和使用手冊和對于ISP編程進入不了編程模式總結(jié)
    發(fā)表于 05-31 14:17 ?43次下載
    AT燒錄軟件Progisp和使用手冊和對于ISP<b class='flag-5'>編程</b>進入不了<b class='flag-5'>編程</b><b class='flag-5'>模式</b>的<b class='flag-5'>總結(jié)</b>

    事件總線模式知識總結(jié)

    經(jīng)過對多個有關(guān)事件總線模式的文檔介紹的閱讀,對事件總線模式有了一定的了解,并作出如下總結(jié)
    發(fā)表于 09-22 10:32 ?1911次閱讀

    關(guān)于risc-v啟動部分的思考

    關(guān)于risc-v啟動部分思考 1.本文說明 1.1 risc-v的誕生的時代背景 1.2 發(fā)展現(xiàn)狀 2.risc-v 的芯片boot過程 2.1 risc-v的啟動模式 2.2 risc-v的啟動
    的頭像 發(fā)表于 12-28 10:25 ?5882次閱讀
    <b class='flag-5'>關(guān)于</b>risc-v啟動部分的<b class='flag-5'>思考</b>

    C 語言編程習(xí)慣總結(jié)

    編程習(xí)慣的培養(yǎng)需要的是一個長期的過程,需要不斷地總結(jié),積累,并且我們需要從意識上認(rèn)識其重要性,一個良好的編程習(xí)慣對于我們能力的...
    發(fā)表于 01-26 17:15 ?0次下載
    C 語言<b class='flag-5'>編程</b>習(xí)慣<b class='flag-5'>總結(jié)</b>

    單片機編程實例總結(jié)

    單片機編程實例總結(jié)
    的頭像 發(fā)表于 01-16 09:17 ?1401次閱讀
    主站蜘蛛池模板: 色综合啪啪 | 操他射他影院 | 黄色福利网 | 五月婷婷在线免费观看 | 日韩成人在线影院 | 额去鲁97在线观看视频 | www.99色.com| 亚洲欧美色一区二区三区 | 波多野结衣在线免费视频 | 亚洲精品久久久久久久蜜桃 | 老司机成人精品视频lsj | 国产1区2区三区不卡 | 2022第二三四天堂网 | 8天堂资源在线 | 五月天狠狠| 精品视频一二三区 | 亚洲综合久久综合激情久久 | 青草99 | 饥渴少妇videos | 美女网站视频一区 | www四虎影视 | 青楼社区51在线视频视频 | 波多野结衣第一页 | tube亚洲高清老少配 | 国内一级特黄女人精品毛片 | 成人永久免费视频网站在线观看 | 色秀视频免费高清网站 | 欧美一级免费片 | 午夜精品福利在线 | 日本三级三级三级免费看 | 日韩一区二区三区免费 | 欧美美女福利视频 | 四虎免费久久影院 | 人人入人人爱 | 亚洲美女视频一区 | 一级骚片超级骚在线观看 | 人人干视频在线观看 | 精品成人毛片一区二区视 | 狠狠干网| 四虎影院在线看 | 亚洲1314 |