sagacity-sqltoy 緩存翻譯功能整理房资,源碼拆解

今天介紹一個(gè)讓我覺得很特別蜕劝、用起來特別舒服的 ORM 框架:sagacity-sqltoy,簡(jiǎn)稱:sqltoy轰异,這個(gè)框架完全國(guó)產(chǎn)岖沛,框架作者也是中國(guó)人。

作者也一直在推廣該框架搭独,讓更多人了解 sqltoy婴削,sqltoy 的文檔完善,對(duì)開發(fā)者友好牙肝,上手特別簡(jiǎn)單唉俗,我寫一些自己玩的項(xiàng)目時(shí),就用的 sqltoy 框架作為持久層框架配椭,也是我選擇 ORM 框架的首選虫溜。

這個(gè)框架我還和同事吐槽過,說框架太智能了股缸,用了這個(gè)框架之后衡楞,我都不會(huì)使用 Mybatis 了,可想而知敦姻,這個(gè)框架有多優(yōu)秀瘾境,推薦所有人可以學(xué)習(xí)一下,哪怕你不使用替劈,學(xué)習(xí)一下這個(gè)框架的優(yōu)點(diǎn)和特別的地方寄雀,也能學(xué)到很多。

image

2.sqltoy 是個(gè)什么框架

Github 開源地址:https://github.com/chenrenfei/sagacity-sqltoy

Gitee 開源地址:https://gitee.com/sagacity/sagacity-sqltoy

在線文檔:https://chenrenfei.github.io/sqltoy/#/

關(guān)于 sqltoy 的介紹我從開源倉(cāng)庫(kù)上截取了一部分

2.1 sqltoy-orm是什么
sqltoy-orm是比hibernate+myBatis更加貼合項(xiàng)目的orm框架陨献,具有hibernate增刪改和對(duì)象加載的便捷性同時(shí)也具有比myBatis更加靈活優(yōu)雅的自定義sql查詢功能盒犹。 

支持以下數(shù)據(jù)庫(kù):
- oracle 從oracle11g到19c
- db2 9.5+,建議從10.5 開始
- mysql 支持5.6、5.7、8.0 版本
- postgresql 支持9.5 以及以上版本
- sqlserver 支持2008到2019版本急膀,建議使用2012或以上版本
- sqlite
- DM達(dá)夢(mèng)數(shù)據(jù)庫(kù)
- elasticsearch 只支持查詢,版本支持5.7+版本沮协,建議使用7.3以上版本
- clickhouse
- mongodb (只支持查詢)
- sybase_iq 支持15.4以上版本,建議使用16版本

2.2 是否重復(fù)造輪子卓嫂,我只想首先說五個(gè)特性:
2.2.1 根本上杜絕了sql注入問題慷暂,sql支持寫注釋、sql文件動(dòng)態(tài)更新檢測(cè)晨雳,開發(fā)時(shí)sql變更會(huì)自動(dòng)重載
2.2.2 最直觀的sql編寫模式行瑞,當(dāng)查詢條件稍微復(fù)雜一點(diǎn)的時(shí)候就會(huì)體現(xiàn)價(jià)值,后期變更維護(hù)的時(shí)候尤為凸顯
2.2.3 極為強(qiáng)大的緩存翻譯查詢:巧妙的結(jié)合緩存減少查詢語(yǔ)句表關(guān)聯(lián)餐禁,極大簡(jiǎn)化sql和提升性能血久。
2.2.3 最強(qiáng)大的分頁(yè)查詢:很多人第一次了解到何為快速分頁(yè)、分頁(yè)優(yōu)化這種極為巧妙的處理帮非,還有在count語(yǔ)句上的極度優(yōu)化氧吐。
2.2.3 跨數(shù)據(jù)庫(kù)函數(shù)方言替換,如:isnull/ifnull/nvl末盔、substr/substring 等不同數(shù)據(jù)庫(kù)

當(dāng)然這只是sqltoy其中的五個(gè)特點(diǎn)筑舅,還有行列轉(zhuǎn)換(俗稱數(shù)據(jù)旋轉(zhuǎn))、多級(jí)分組匯總陨舱、統(tǒng)一樹結(jié)構(gòu)表(如機(jī)構(gòu))查詢翠拣、分庫(kù)分表sharding、取隨機(jī)記錄隅忿、取top記錄心剥、修改并返回記錄、慢sql提醒等這些貼合項(xiàng)目應(yīng)用的功能背桐, 當(dāng)你真正了解上述特點(diǎn)帶來的巨大優(yōu)勢(shì)之后优烧,您就會(huì)對(duì)中國(guó)人創(chuàng)造的sqltoy-orm有了信心!

sqltoy-orm 來源于個(gè)人親身經(jīng)歷的無數(shù)個(gè)項(xiàng)目的總結(jié)和思考链峭,尤其是性能優(yōu)化上不斷的挖掘畦娄,至于是不是重復(fù)的輪子并不重要,希望能夠幫到大家

這里的介紹我只摘取了一部分弊仪,更多的特性介紹可以前往 Github 上查看熙卡,下面我們進(jìn)入本文的主題:緩存翻譯

3.緩存翻譯功能

緩存翻譯,這是一個(gè)什么功能励饵?

  1. 通過緩存翻譯: 將 code (編碼)轉(zhuǎn)化為名稱驳癌,無需關(guān)聯(lián)查詢,極大簡(jiǎn)化sql并提升查詢效率役听。
  2. 通過緩存名稱模糊匹配: 獲取精準(zhǔn)的編碼作為條件颓鲜,避免關(guān)聯(lián)like 模糊查詢表窘。

例如 MyBatis:

SELECT
    i.staff_id,
    i.staff_name,
    i.sex_type,
    d1.DICT_NAME AS sex_type_name,
    i.post,
    d2.DICT_NAME AS post_name,
    i.create_by,
    d3.STAFF_NAME 
FROM
    sqltoy_staff_info i
    LEFT JOIN ( SELECT d.DICT_KEY, d.DICT_NAME FROM sqltoy_dict_detail d WHERE d.DICT_TYPE = "SEX_TYPE" ) d1 ON i.SEX_TYPE = d1.DICT_KEY
    LEFT JOIN ( SELECT d.DICT_KEY, d.DICT_NAME FROM sqltoy_dict_detail d WHERE d.DICT_TYPE = "POST_TYPE" ) d2 ON i.post = d2.DICT_KEY
    LEFT JOIN ( SELECT info.STAFF_ID, info.STAFF_NAME FROM sqltoy_staff_info info ) d3 ON i.create_by = d3.STAFF_ID 
WHERE
    i.staff_id = "S0003"

解釋下這個(gè) SQL,我要獲取 S0003 的個(gè)人信息甜滨,sex_type乐严、post、create_by 是 code 編碼衣摩,需要轉(zhuǎn)化為名稱昂验,方便前端展示。那我就需要去關(guān)聯(lián)字典表和員工表艾扮,造成 SQL 需要進(jìn)行三次關(guān)聯(lián)既琴。

image

而 sqltoy:


image

只需要在 xml 中的 sql 語(yǔ)句上配置 translate 緩存翻譯功能就行了,code 編碼則會(huì)自動(dòng)轉(zhuǎn)化為名稱栏渺。

是不是簡(jiǎn)化了 sql呛梆,至于效率也不用擔(dān)心,首先在 sql 層面省去了多表關(guān)聯(lián)磕诊,code 翻譯的結(jié)果值是從緩存中獲取,效率更高纹腌■眨總的來看,提高了效率升薯、簡(jiǎn)化了 sql 的復(fù)雜性莱褒,十分方便。

看完了案例涎劈,我們就來仔細(xì)看看關(guān)于緩存翻譯的配置使用广凸,以及作者沒在文檔中詳細(xì)說的另外兩種方式(service、rest)的緩存翻譯蛛枚。

3.1.緩存翻譯初始化

緩存翻譯的功能實(shí)現(xiàn)其實(shí)并不復(fù)雜谅海,通過閱讀源碼能了解到緩存翻譯的加載和使用。

例如:需要獲取員工信息蹦浦,順便把員工的性別編碼進(jìn)行翻譯扭吁,sqltoy 的流程如下:

  1. 根據(jù)業(yè)務(wù) sql 去查詢員工信息。
  2. jdbc 查詢的結(jié)果值進(jìn)行 aop 過濾盲镶,判斷是否需要翻譯侥袜。
  3. 需要翻譯進(jìn)入翻譯邏輯,不需要進(jìn)行結(jié)果值封裝溉贿,返回結(jié)果枫吧。
  4. 進(jìn)入翻譯邏輯,按照翻譯 sql 去查詢字典值宇色。
  5. 業(yè)務(wù)數(shù)據(jù)和翻譯數(shù)據(jù)結(jié)果集九杂,進(jìn)行業(yè)務(wù)數(shù)據(jù)的 code 翻譯闽寡,替換。
  6. 封裝翻譯之后的結(jié)果尼酿,返回 service 層爷狈。

總結(jié)一下,整個(gè)翻譯就是用 Spring AOP 在底層對(duì)查詢結(jié)果進(jìn)行統(tǒng)一替換處理裳擎。

下面我們一起來看一下緩存翻譯的加載流程涎永,方便我們后面理解緩存翻譯使用,這里建議大家去 Github 上把源碼 clone 下來鹿响,打斷點(diǎn)調(diào)兩遍羡微,會(huì)理解的更加透徹。

3.1.1 加載入口

org.sagacity.sqltoy.SqlToyContext 文件就是 sqltoy 框架加載主入口惶我,這里加載的配置有:sqlToy 配置解析插件妈倔、實(shí)體對(duì)象管理器、翻譯器插件绸贡、緩存管理器盯蝴、統(tǒng)一公共字段賦值處理、延時(shí)檢測(cè)時(shí)長(zhǎng)听怕、數(shù)據(jù)庫(kù)方言參數(shù)捧挺、es的地址配置等等。然后翻譯器插件就是緩存翻譯尿瞭。

image

在 SqlToyContext.initialize 初始化方法中找到翻譯器的初始化方法闽烙,點(diǎn)進(jìn)去。

image

這個(gè)方法的主要目的就是:配置緩存翻譯声搁、緩存路徑黑竞、載入具體的緩存翻譯配置。

TranslateConfigParse.parseTranslateConfig 方法才是真正的緩存翻譯文件的內(nèi)容解析疏旨。

由于方法內(nèi)容過長(zhǎng)很魂,不好截圖,我簡(jiǎn)單概述下方法所做的事充石,具體內(nèi)容大家可以通過源碼進(jìn)行了解莫换。

parseTranslateConfig 主要功能有:設(shè)置緩存的存儲(chǔ)地址、內(nèi)存大小骤铃、過期時(shí)間拉岁、sql 語(yǔ)句加載、sql 語(yǔ)句參數(shù)加載惰爬、數(shù)據(jù)源以及增量緩存的刷新時(shí)間等喊暖。用大白話來說就是,把緩存 xml 文件中的內(nèi)容和配置進(jìn)行解析撕瞧,加載到 SqlToyConfig 實(shí)例里陵叽,方便后續(xù)的使用狞尔。

到這里,緩存翻譯的初始化加載流程就是介紹完了巩掺,介紹起來幾句話就講完了呻惕,但實(shí)際的加載流程嚣艇,建議大家去看看源碼。

3.2 三種緩存翻譯方式

下面我們就來看看緩存翻譯的具體使用方法,先說明屈雄,本篇文章的緩存翻譯贬芥,只是 cache-translates 部分赶促,沒有 cache-update-checkers 部分社付。

  • cache-translates負(fù)責(zé)將數(shù)據(jù)加載到緩存
  • cache-update-checkers則負(fù)責(zé)檢查數(shù)據(jù)是否發(fā)生變化清理緩存,當(dāng)下次使用緩存時(shí)會(huì)自動(dòng)重新獲取數(shù)據(jù)放入緩存燃箭,從而實(shí)現(xiàn)緩存的刷新

3.2.1 sql 緩存翻譯

sql 類型的緩存翻譯冲呢,是最基本的使用,也是作者在文檔中公開的使用方式招狸,使用的方式也最簡(jiǎn)單敬拓、廣泛,適用于絕大部分翻譯場(chǎng)景瓢颅。

sql 類型緩存翻譯案例走起恩尾。

第一步,先在緩存翻譯的 xml 中挽懦,書寫好緩存值的 sql 語(yǔ)句。例如我要緩存員工的姓名木人,員工ID作為緩存 key,姓名作為 value信柿,sql 如下:

<!-- 員工ID和姓名的緩存 -->
<sql-translate cache="staffIdName" datasource="dataSource">
  <sql>
    <![CDATA[
      SELECT
        STAFF_ID,
        STAFF_NAME
      FROM
        sqltoy_staff_info
     ]]>
  </sql>
</sql-translate>

cache 是緩存名稱,名稱必須要唯一(必填)醒第,datasource 是當(dāng)前數(shù)據(jù)庫(kù)數(shù)據(jù)源(非必填)渔嚷。

第二步,在需要翻譯的業(yè)務(wù) sql 語(yǔ)句上稠曼,配置緩存翻譯功能形病,并指定使用的緩存名稱、需要翻譯的 key 值霞幅。例如:

<sql id="getStaffInfoByStaffId">
  <translate cache="staffIdName" columns="create_by" />
    <value>
      <![CDATA[
        SELECT
          i.staff_id,
          i.staff_name,
          i.create_by
        FROM
          sqltoy_staff_info i
        WHERE i.staff_id = :staffId
      ]]>
    </value>
</sql>

translate 可配置的屬性列表:

  • cache:具體的緩存定義的名稱
  • cache-type:一般針對(duì)數(shù)據(jù)字典漠吻,提供一個(gè)分類條件過濾
  • columns:sql中的查詢字段名稱,可以逗號(hào)分隔對(duì)多個(gè)字段進(jìn)行翻譯
  • cache-indexs:緩存數(shù)據(jù)名稱對(duì)應(yīng)的列,不填則默認(rèn)為第二列(從0開始,1則表示第二列)司恳,例如緩存的數(shù)據(jù)結(jié)構(gòu)是:key途乃、name、fullName,則第三列表示全稱

然后我們測(cè)試一下結(jié)果:

service 層扔傅,調(diào)用上面的業(yè)務(wù) sql(getStaffInfoByStaffId):

  /**
  * 根據(jù) ID 獲取 vo
  *
  * @param staffId
  * @return
  */
  @Override
  public SqltoyStaffInfoVO getStaffInfoByStaffId(String staffId) {
    return this.sqlToyLazyDao.loadBySql("getStaffInfoByStaffId", new String[]{"staffId"}, new String[]{staffId}, SqltoyStaffInfoVO.class);
  }

test 層:

@Test
void testFive() {
   SqltoyStaffInfoVO vo = this.passwordService.getStaffInfoByStaffId("S0001");
   Assert.assertEquals("測(cè)試失敗-性別",vo.getSexType(),"男");
   Assert.assertEquals("測(cè)試失敗-職位類別",vo.getPost(),"管理崗");
   Assert.assertEquals("測(cè)試失敗-職位等級(jí)",vo.getPostGrade(),"L10");
   System.out.printf("性別:%s,職位:%s,職位等級(jí):%s",vo.getSexType(),vo.getPost(),vo.getPostGrade());
}

測(cè)試結(jié)果:


image

可以看到耍共,斷言測(cè)試通過烫饼,并且打印的日志顯示,翻譯的結(jié)果成功试读,成功把性別杠纵、職位類別、職位等級(jí)等編碼翻譯為中文钩骇。

3.2.2 service 緩存翻譯

service 類型的緩存翻譯比藻,作者只是在 sqltoy-starter-showcase 模塊項(xiàng)目中的 sqltoy-translate.xml 文件中提到過,具體的案例伊履,在項(xiàng)目中我沒有找到韩容,以為功能沒實(shí)現(xiàn),作者說實(shí)現(xiàn)了唐瀑,就自己研究了一下群凶,測(cè)試了一遍翻譯功能。

service 類型的緩存翻譯我一開始認(rèn)為有點(diǎn)多余哄辣,我的想法是请梢,都有 sql 類型的緩存翻譯了,干嘛多此一舉弄一個(gè) service力穗,并且 service 的緩存翻譯最終還是用 sql 獲取數(shù)據(jù)毅弧。

仔細(xì)思考過后,我發(fā)現(xiàn)自己錯(cuò)了当窗,存在就是合理的够坐,我認(rèn)為沒有、多余崖面,只是我沒有使用場(chǎng)景元咙,而作者開發(fā) service 類型,肯定就是有使用場(chǎng)景的巫员。

思考一番過后庶香,說下我認(rèn)為 service 類型的使用場(chǎng)景,在微服務(wù)系統(tǒng)中简识,A赶掖、B 兩個(gè)系統(tǒng)是分開的,分別使用各自的庫(kù) A 庫(kù)和 B 庫(kù)七扰,這個(gè)時(shí)候奢赂,我在 A 系統(tǒng)中查詢一些業(yè)務(wù)數(shù)據(jù),其中某個(gè)字段的編碼所對(duì)應(yīng)的 value 值并不在 A 庫(kù)中戳寸,而是 B 庫(kù)呈驶,這種場(chǎng)景下,數(shù)據(jù)是跨庫(kù)的疫鹊,無法關(guān)聯(lián)查詢袖瞻,只能在業(yè)務(wù)代碼中進(jìn)行 B 庫(kù)數(shù)據(jù)查詢司致,進(jìn)行編碼轉(zhuǎn)換。

而 service 類型的緩存翻譯聋迎,則可以很順利的解決這個(gè)問題脂矫,在 A 系統(tǒng)中通過 Fegin 調(diào)用 B 系統(tǒng)的 API,形成一個(gè) service 方法霉晕,然后 A 系統(tǒng)在業(yè)務(wù) sql 使用 service 類型的緩存翻譯庭再,就可以翻譯 sql 中的編碼了。

說起來有點(diǎn)繞牺堰,我們案例整起:

我這里沒有開兩個(gè)服務(wù)拄轻,而是在 service 里新寫一個(gè)方法,然后在業(yè)務(wù) sql 中調(diào)用這個(gè)方法伟葫,模擬跨服務(wù)的場(chǎng)景恨搓。

第一步,先把緩存方法書寫好筏养。依然是把員工ID翻譯為員工姓名斧抱。

    /**
     * 獲取所有員工信息記錄
     *
     * @return
     */
    @Override
    public List<Object[]> queryStaffInof() {
        List<SqltoyStaffInfoVO> findStaffInof = this.sqlToyLazyDao.findBySql("findStaffInof", new SqltoyStaffInfoVO());
        List<Object[]> list = new ArrayList<>(findStaffInof.size());
        findStaffInof.stream().forEach(e -> {
            Object[] arr = new Object[]{e.getStaffId(),e.getStaffName()};
            list.add(arr);
        });
        return list;
    }

這個(gè)方法可以看作是 A 系統(tǒng)中通過 Fegin 調(diào)用 B 系統(tǒng)的 API 返回的值。

方法返回類型是:List<Object[]>渐溶,這個(gè)是緩存翻譯底層的限制辉浦,返回類型可以是:List<List>>、List<Object[]>茎辐。

找到對(duì)應(yīng)的源碼可以看到限制:

private static HashMap<String, Object[]> wrapCacheResult(Object target, TranslateConfigModel cacheModel) {
        if (target == null) {
            return null;
        } else if (target instanceof HashMap && ((HashMap)target).isEmpty()) {
            return null;
        } else if (target instanceof HashMap && ((HashMap)target).values().iterator().next().getClass().isArray()) {
            return (HashMap)target;
        } else {
            LinkedHashMap<String, Object[]> result = new LinkedHashMap();
            Object[] row;
            if (target instanceof HashMap) {
                if (!((HashMap)target).isEmpty()) {
                    Iterator iter;
                    Entry entry;
                    if (((HashMap)target).values().iterator().next() instanceof List) {
                        iter = ((HashMap)target).entrySet().iterator();

                        while(iter.hasNext()) {
                            entry = (Entry)iter.next();
                            row = new Object[((List)entry.getValue()).size()];
                            ((List)entry.getValue()).toArray(row);
                            result.put(entry.getKey(), row);
                        }
                    } else {
                        iter = ((HashMap)target).entrySet().iterator();

                        while(iter.hasNext()) {
                            entry = (Entry)iter.next();
                            result.put(entry.getKey(), new Object[]{entry.getKey(), entry.getValue()});
                        }
                    }
                }
            } else if (target instanceof List) {
                List tempList = (List)target;
                if (!tempList.isEmpty()) {
                    int cacheIndex = cacheModel.getKeyIndex();
                    int i;
                    int n;
                    List dataSet;
                    if (tempList.get(0) instanceof List) {
                        i = 0;

                        for(n = tempList.size(); i < n; ++i) {
                            dataSet = (List)tempList.get(i);
                            Object[] rowAry = new Object[dataSet.size()];
                            dataSet.toArray(rowAry);
                            result.put(rowAry[cacheIndex].toString(), rowAry);
                        }
                    } else if (tempList.get(0) instanceof Object[]) {
                        i = 0;

                        for(n = tempList.size(); i < n; ++i) {
                            row = (Object[])((Object[])tempList.get(i));
                            result.put(row[cacheIndex].toString(), row);
                        }
                    } else if (cacheModel.getProperties() != null && cacheModel.getProperties().length > 1) {
                        dataSet = BeanUtil.reflectBeansToInnerAry(tempList, cacheModel.getProperties(), (Object[])null, (ReflectPropertyHandler)null, false, 0);
                        Iterator var12 = dataSet.iterator();

                        while(var12.hasNext()) {
                            Object[] row = (Object[])var12.next();
                            result.put(row[cacheIndex].toString(), row);
                        }
                    }
                }
            }

            return result;
        }
    }

wrapCacheResult 方法的參數(shù):

  • target 就是 queryStaffInof() 方法(可以理解為 B 系統(tǒng)的方法)的返回值宪郊。
  • TranslateConfigModel 是緩存翻譯的模型 Bean,Bean 里面有緩存翻譯的相關(guān)基礎(chǔ)屬性拖陆,例如:
    • 緩存類型(sql,service,rest)废膘,
    • 數(shù)據(jù)源,
    • 緩存名稱慕蔚,
    • 自定義的 ServiceBean,
    • 自定義的 ServiceMethod斋配,
    • rest 類型的 url 等等孔飒。

第二步,在 A 系統(tǒng)中的緩存翻譯文件中艰争,配置 service 類型的緩存坏瞄,方法有參,無參甩卓,在緩存的 xml 中沒去區(qū)別鸠匀。。

<!-- service 緩存翻譯 -->
<service-translate service="com.boyguhui.manage.service.PasswordService" method="queryStaffInof" cache="staffInfoServiceCache" />
  • service service 類的路徑
  • method 具體調(diào)用方法
  • cache 緩存名稱

第三步逾柿,A 系統(tǒng)的業(yè)務(wù) sql 上配置 service 緩存缀棍。這里和 sql 類型的使用方式是一模一樣的宅此,service 方法如果有參數(shù),也是通過 cache-type 屬性傳入爬范。唯一不一樣的地方就是 cache 改為 servcie 的緩存名稱父腕。

<sql id="getStaffInfoByStaffId">
  <translate cache="staffInfoServiceCache" columns="create_by" />
    <value>
      <![CDATA[
        SELECT
          i.staff_id,
          i.staff_name,
          i.create_by
        FROM
          sqltoy_staff_info i
      ]]>
    </value>
</sql>

然后我們測(cè)試一下結(jié)果,調(diào)用的 service 層方法不變青瀑,只是把 service 對(duì)應(yīng)的業(yè)務(wù) sql 上的緩存由 sql 類型換為 servcie 類型璧亮。

test 層

@Test
void testEight() {
  SqltoyStaffInfoVO vo = this.passwordService.getStaffInfoByStaffId("S0001");
  Assert.assertEquals("測(cè)試失敗-創(chuàng)建人姓名",vo.getCreateBy(),"張三");
  System.out.printf("創(chuàng)建人姓名:%s",vo.getCreateBy());
}

測(cè)試結(jié)果:


image

斷言測(cè)試通過,并且打印的日志顯示斥难,翻譯的結(jié)果成功枝嘶,成功把創(chuàng)建人ID翻譯為中文名稱。

我補(bǔ)充一下哑诊,service 緩存是如何通過你配置的 service 和 method 就獲取到緩存數(shù)據(jù)的群扶?其實(shí)是通過配置的 service 反射調(diào)用 method 來獲取數(shù)據(jù)的,這點(diǎn)可以在源碼中找到搭儒。

image

這是緩存翻譯的三種類型判斷穷当,根據(jù)類型調(diào)用不同的緩存數(shù)據(jù)獲取方法,我們進(jìn)入 service 類型看看淹禾,看下底層是不是反射馁菜。

image

image

image

TranslateFactory.getServiceCacheData() -> SqlToyContext.getServiceData() -> BeanUtil.invokeMethod() -> Method.invoke()。

從 servcie 類型調(diào)用 getServiceCacheData() 一直往下铃岔,會(huì)走到 Method.invoke()汪疮,可以證明 servcie 類型緩存翻譯方法調(diào)用方式通過反射來進(jìn)行的。

3.2.3 rest 緩存翻譯

rest 緩存翻譯毁习,它和 servcie智嚷、sql 類型都不一樣,rest 緩存翻譯是通過 url 地址向第三方服務(wù)發(fā)起請(qǐng)求纺且,獲取所需要的緩存值或字典數(shù)據(jù)盏道。

前面 service 緩存翻譯可以跨服務(wù),從 A 服務(wù)調(diào)用 B 服務(wù)的數(shù)據(jù)载碌,而 rest 則可以跨系統(tǒng)猜嘱,從 A 系統(tǒng)調(diào)用 B 系統(tǒng)的數(shù)據(jù)(當(dāng)然, service 也可以做到嫁艇,在本地 servcie 層通過 HttpClient 調(diào)用第三方服務(wù)和通過 Fegin 調(diào)用其他服務(wù)都是一樣的)朗伶。

說下我認(rèn)為 rest 緩存翻譯的使用場(chǎng)景,假設(shè)我公司有兩個(gè)單體應(yīng)用 A 和 B步咪,A 和 B 各自有各自的服務(wù)器论皆、數(shù)據(jù)庫(kù)、nginx,如果 A 需要調(diào)用 B 的數(shù)據(jù)字典点晴,來翻譯自己的數(shù)據(jù)里的某個(gè)字段感凤。

這個(gè)場(chǎng)景用 service 也能做到,不過需要自己去寫 HttpClient 部分的代碼觉鼻,而 rest 則在底層幫用戶做好了俊扭,只需要提供調(diào)用 url 就行。

如果調(diào)用的 B 系統(tǒng)接口還有用戶身份驗(yàn)證坠陈,也可以配置一個(gè)賬號(hào)萨惑,進(jìn)行請(qǐng)求認(rèn)證。

說了這么多仇矾,人都整懵了庸蔼,我們案例走起。

第一步贮匕,在緩存文件中姐仅,配置 rest 緩存。

<rest-translate url="http://localhost:8082/password/findDictKeyNameByType" cache="findDictKeyNameByTypeRestCache" username="user" password="123" />
  • url 就是你請(qǐng)求的第三方系統(tǒng)地址 (必填)
  • cache 緩存名稱 (必填)
  • username 身份認(rèn)證的用戶名 (非必填)
  • password 身份認(rèn)證的密碼 (非必填)

用戶名和密碼兩個(gè)屬性刻盐,看請(qǐng)求的接口掏膏,接口需要進(jìn)行身份認(rèn)證,就需要加上敦锌,不需要認(rèn)證馒疹,則可以沒有。

其實(shí)到這里乙墙,rest 的緩存就配置好了颖变,很簡(jiǎn)單的一個(gè)配置,使用就直接在業(yè)務(wù) sql 上配置緩存听想,指定使用緩存名稱為第一步的緩存名稱就行了腥刹,和使用 sql 類型、service 類型的緩存翻譯方式?jīng)]有區(qū)別汉买。

下面通過案例和 rest 類型的源碼衔峰,來幫助大家更好的理解 rest 類型的原理和身份認(rèn)證這部分,以及如果是帶參數(shù)請(qǐng)求蛙粘,第三方的接口如何接收參數(shù)朽色。

image

這本地寫了一個(gè)案例,翻譯員工的崗位组题,用的緩存翻譯,就是第一步中配置的 rest 翻譯抱冷,帶了一個(gè)字典編碼作為參數(shù)崔列。

然后我們看下 sqltoy 底層是如何調(diào)用第三方接口的的。

在 TranslateFactory 類下有 getCacheData 方法

image

getCacheData 方法是根據(jù)不同的緩存類型調(diào)用對(duì)應(yīng)的方法獲取緩存數(shù)據(jù),然后返回上層赵讯,進(jìn)行翻譯值的替換盈咳。

我們進(jìn)入 rest 類型的方法看看。

image

重點(diǎn)看下 332 行边翼,這一行鱼响,拿到了請(qǐng)求的 url、username组底、password丈积、請(qǐng)求參數(shù) Key、請(qǐng)求參數(shù) Value 等信息债鸡,通過封裝的 HttpClient 進(jìn)行發(fā)起了接口請(qǐng)求江滨。

332 行之后的代碼就是對(duì)接口響應(yīng)的數(shù)據(jù)進(jìn)行封裝處理,把 String 字符串轉(zhuǎn)為 JSON 格式厌均,再轉(zhuǎn)為 List<Object[]> 返回給上層調(diào)用方法唬滑。

我們繼續(xù)進(jìn)入封裝的 doPost 方法看看。

image

重點(diǎn)看兩個(gè)地方棺弊,75 -79 行晶密,這里是設(shè)置身份認(rèn)證的地方,另一個(gè)就是 84 - 92 行模她,這部分是設(shè)置請(qǐng)求參數(shù)稻艰,請(qǐng)求參數(shù)的封裝用的是:UrlEncodedFormEntity,body 參數(shù)格式會(huì)轉(zhuǎn)為“KEY1=VALUE1&KEY2=VALUE2&...”這種形式缝驳,服務(wù)端接收以后也要依據(jù)這種協(xié)議形式做處理连锯。

好了,rest 類型的底層說完了用狱,一起來看下服務(wù)端的代碼运怖,以及接收參數(shù)的處理。

image

在方法的第一行夏伊,是參數(shù)轉(zhuǎn)換處理方法摇展,第二行才是服務(wù)端的業(yè)務(wù)邏輯代碼。

image

由于參數(shù)是通過 UrlEncodedFormEntity 方式傳遞的溺忧,我通過 HttpServletRequest 讀取字符流咏连,然后轉(zhuǎn)為字符串,根據(jù) = 號(hào)切割鲁森,獲取 value 值祟滴,這個(gè) value 值就是 rest 請(qǐng)求帶過來字典參數(shù)。

整個(gè) rest 配置流程和底層源碼都講完了歌溉,我們測(cè)試一遍垄懂,看看翻譯功能對(duì)不對(duì)骑晶。我這里的測(cè)試,是把測(cè)試的服務(wù)草慧,打成 jar 包桶蛔,用 8082 端口啟動(dòng),模擬 A漫谷、B 兩個(gè)單體應(yīng)用仔雷,下面是我的測(cè)試結(jié)果。

image

可以看到舔示,(8080 A 系統(tǒng))本地調(diào)用 servcie 方法獲取員工信息碟婆,斷言的結(jié)果是正確通過的,IDEA 控制臺(tái)打印的日志顯示員工崗位翻譯成功斩郎。

然后在 (8082 B 系統(tǒng))的日志上脑融,能看到獲取的參數(shù) Value 和查詢 sql 日志。

4.緩存翻譯底層 Ehcache

這一節(jié)缩宜,主要說下緩存翻譯的底層緩存和一些源碼上的內(nèi)容肘迎。

4.1 Ehcache

緩存翻譯的緩存,底層實(shí)現(xiàn)是:Ehcache锻煌,這點(diǎn)在框架的源碼能找到妓布,源碼位置在:org.sagacity.sqltoy.translate.cache 包下面。

image

紅色部分是緩存翻譯相關(guān)的文件宋梧,包括:解析緩存翻譯的配置匣沼、定時(shí)檢測(cè)緩存是否更新程序、緩存刷新檢測(cè)捂龄、緩存翻譯器释涛、緩存相關(guān) model、緩存實(shí)現(xiàn)等倦沧。

綠色部分就是緩存的實(shí)現(xiàn)唇撬,TranslateCacheManager 是抽象類,提供緩存接口規(guī)范展融。

image

TranslateEhcacheManager 是具體緩存實(shí)現(xiàn)類窖认,繼承抽象類,實(shí)現(xiàn)緩存具體功能告希。

image

從實(shí)現(xiàn)類中我們可以看到扑浸,實(shí)現(xiàn)類用的緩存是 CacheManager,而 CacheManager 就是 Ehcache 的緩存管理器燕偶。

在實(shí)現(xiàn)類重寫的 init 方法可以看到喝噪,如果 CacheManager 是 null,就為 CacheManager 創(chuàng)建一個(gè)實(shí)例指么,提供給其他方法(put仙逻、getCache)使用驰吓。

4.2 緩存翻譯的 AOP 原理

3.1.緩存翻譯初始化 節(jié)我講了,緩存翻譯的工作流程系奉,但只是進(jìn)行文字描述,沒用過這個(gè)框架的朋友可能會(huì)有點(diǎn)懵姑廉,這一小節(jié)缺亮,我通過源碼來仔細(xì)梳理緩存翻譯的工作流程。

緩存翻譯是如何獲取值的桥言?又是如何把 code 編碼替換為中文名稱的萌踱?又是如何在翻譯之后把結(jié)果集返回給我們的 service 層?這一小節(jié)具體解決這三個(gè)問題号阿。

4.2.1 AOP 處理

我們從業(yè)務(wù)邏輯代碼 servcie 層調(diào)用的 SqlToyLazyDao.loadBySql 方法進(jìn)去并鸵,一直往下找:SqlToyDaoSupport.loadBySql -> SqlToyDaoSupport.loadByQuery。

image

第一行的 SqlToyConfig 是一個(gè) sql 解析對(duì)象扔涧,里面有 sql 語(yǔ)句屬性园担、參數(shù) key、參數(shù) value枯夜、數(shù)據(jù)庫(kù)方言弯汰、翻譯器、脫敏配置等湖雹,其實(shí)就是 sql.xml 解析出來的對(duì)象咏闪,里面包含當(dāng)前 sql 的各種配置。

第二行就是拿到 sqltoyCofing 實(shí)例摔吏,去查詢數(shù)據(jù)以及根據(jù)實(shí)例里的配置鸽嫂,去解析 sql 的轉(zhuǎn)換、解析征讲、翻譯据某、脫敏、行轉(zhuǎn)列的操作稳诚。進(jìn)入 findByQuery 方法哗脖,看里面的具體操作。

image

方法的 750 - 755 行扳还,操作的是把 sql 中的參數(shù) key 替換為占位符號(hào)才避,并且把參數(shù) values 按照順序整理到 queryParam 實(shí)例中。

image

queryParam 的 sql 就是一個(gè)完整的 sql氨距,例如:sql select id,name,age from users u where u.name = ? and u.age ?

而 paramsValue 就是 桑逝?的數(shù)組 value :["java",21]。

然后我們看具體的調(diào)用 findBySql 方法俏让,我本地用的 Mysql 數(shù)據(jù)庫(kù)楞遏,所以我進(jìn)入的是 Mysql 的實(shí)現(xiàn)方法茬暇。

image

一直往下找:MySqlDialect.findBySql -> DialectUtils.findBySql。

image

現(xiàn)在我們到底層了寡喝,相信讀者對(duì)我標(biāo)記出來的代碼不陌生糙俗,JDBC 代碼而已,根據(jù) sql 查詢數(shù)據(jù)预鬓,然后看 222 行巧骚,這里很重要,框架在這里把 JDBC 查詢出來的實(shí)例 rs 進(jìn)行了結(jié)果值封裝格二、替換劈彪,然后把封裝、替換之后的 rs 結(jié)果集返回給 service 層顶猜。

對(duì)這個(gè)功能有沒有很熟悉沧奴?像不像 Spring AOP 切面處理?其實(shí)就是 AOP 原理长窄,本來我沒意識(shí)到是 AOP滔吠,后面在 sqltoy 的 QQ 群里,作者提了一句抄淑,我才意識(shí)到屠凶。

到這里就能知道我們的結(jié)果集為什么會(huì)進(jìn)行 code 編碼翻譯,翻譯為中文名稱肆资,因?yàn)榭蚣艿讓訉?duì) rs(ResultSet) 集合進(jìn)行了替換矗愧。

然后我們繼續(xù)看緩存翻譯是如何獲取值的?又是如何把 code 編碼進(jìn)行替換的郑原?

4.2.2 緩存翻譯是如何獲取值的唉韭?又是如何把 code 編碼進(jìn)行替換的?

點(diǎn)擊 222 行的 ResultUtils.processResultSet() 方法犯犁。

image

再進(jìn)入 104 行的 getResultSet() 方法属愤。

image

看到 288 行,這里通過 sqlToyConfig.getTranslateMap() 方法獲取當(dāng)前運(yùn)行的 sql 是否有解析到緩存翻譯器酸役。

如果你在業(yè)務(wù) sql 上配置了<translate cache="dictKeyName" columns="post" cache-type="POST_TYPE" /> 就有緩存翻譯器住诸,沒配置就代表不需要翻譯,就沒有緩存翻譯器涣澡。

然后看到 289 行贱呐,如果有緩存翻譯器,就獲取翻譯器入桂,并在 292 根據(jù)翻譯器獲取緩存數(shù)據(jù)奄薇,我們進(jìn)入到 sqlToyContext.getTranslateManager().getTranslates() 方法看看。

image

方法 139 行在循環(huán)翻譯器抗愁,然后挨個(gè)處理每個(gè)翻譯器馁蒂,141 行呵晚,判斷加載的緩存有沒有翻譯器里的緩存名稱,有就把當(dāng)前翻譯器取出來沫屡,通過 143 行的 getCacheData 方法取獲取緩存數(shù)據(jù)饵隙。

image

175 行,先是根據(jù)緩存名稱取緩存數(shù)據(jù)沮脖,如果獲取到的數(shù)據(jù)為 null 或?yàn)榭振荆瑒t通過 TranslateFactory.getCacheData()方法去數(shù)據(jù)庫(kù)獲取數(shù)據(jù),查到數(shù)據(jù)之后倘潜,放入緩存( 181 行)。

這就是 TranslateFactory.getCacheData()志于,根據(jù)翻譯器的類型涮因,進(jìn)入不同的緩存數(shù)據(jù)加載方法。

image

這里我們看 sql 類型方法伺绽。

image

283 行獲取緩存上數(shù)據(jù)源养泡,如果沒獲取到就獲取當(dāng)前 sql 解析的數(shù)據(jù)源,然后 288 行判斷緩存有沒有傳入?yún)?shù)奈应,也就是 <translate cache="dictKeyName" columns="post" cache-type="POST_TYPE" /> cache-type 屬性的值澜掩,有參數(shù)則構(gòu)造一個(gè)帶條件 QueryExecutor,調(diào)用 DialectFactory.getInstance().findByQuery() 方法就又進(jìn)入了 AOP 小節(jié)的講的 findByQuery() 方法杖挣。

image

這個(gè)方法是不是有點(diǎn)眼熟肩榕?其實(shí)就是 service 層調(diào)用的底層方法,緩存器調(diào)用 findByQuery 方法惩妇,通過 JDBC 把緩存器解析的 sql 執(zhí)行株汉,獲取到翻譯數(shù)據(jù)。

image

緩存數(shù)據(jù)取到了歌殃,我們回到 getResultSet()乔妈,看到 293 行,緩存數(shù)據(jù)為 null 或?yàn)榭彰ブ澹瑒t把 288 行的緩存翻譯器標(biāo)記賦值為:false路召。

image

假設(shè)緩存數(shù)據(jù)不為空,緩存標(biāo)記為 true波材,方法一直往下運(yùn)行股淡,找到 370 行(我標(biāo)記的位置),先是判斷緩存標(biāo)記是否為 true各聘,為真則進(jìn)入 processResultRowWithTranslate 方法揣非,我們進(jìn)入該方法。

image

注意看方法上的注釋:@todo 存在緩存翻譯的結(jié)果處理躲因。這就是我們要找的翻譯方法早敬,把 code 編碼替換為中文名稱忌傻。

這里我在本地?cái)帱c(diǎn),把方法的運(yùn)行情況截圖下來搞监,方便大家理解各個(gè)參數(shù)的意思和值以及是如何替換的水孩。

image
  • labelNames 是業(yè)務(wù) sql 上的所有字段,例如業(yè)務(wù) sql 為:select id,name from users琐驴,那 labelNames = [id,name]
  • fieldValue 是根據(jù)當(dāng)前 labelNames 獲取的 value值俘种,例如當(dāng)前 label 為 name,fieldValue 則為:張三,也就是業(yè)務(wù) sql 查詢得到的數(shù)據(jù)绝淡。
  • keyIndex 是方法參數(shù) size( labelNames 的長(zhǎng)度 ) 的循環(huán)當(dāng)前值宙刘。

然后我們看下 translateKey() 方法,還是斷點(diǎn)調(diào)試的截圖牢酵。

image

方法第一行獲取了需要翻譯的 code 編碼悬包,然后判斷是不是單值翻譯,如果是多個(gè)值翻譯馍乙,就進(jìn)入 else 進(jìn)行切割布近,然后循環(huán)翻譯,我這里是單值丝格。

cacheValues 是根據(jù)翻譯的 code 編碼獲取到對(duì)應(yīng)的緩存對(duì)象撑瞧,如果沒有獲取到,則代表沒查到這個(gè) code 編碼的中文信息显蝌,打印錯(cuò)誤日志预伺。對(duì)象存在,就根據(jù)翻譯器的下標(biāo)獲取中文名稱琅束,賦值給 fieldValue 并返回扭屁。

這里說一下 translate.getIndex() 下標(biāo)問題,下標(biāo)默認(rèn)是 1涩禀,可以在 <translate> 更改 cache-indexs 屬性料滥,這個(gè)下標(biāo)為 1 是什么意思呢?它代表的就是把 code 編碼替換為緩存 sql 的哪一個(gè)字段艾船。

translateKey() 方法把 code 替換為中文后葵腹,返回到 processResultRowWithTranslate() 方法,添加到 rowData(List 集合)實(shí)例中屿岂,然后逐步返回到 DialectUtils.findBySql()践宴,最終返回到 service 層,被我們獲取爷怀。

這就是完整的緩存翻譯流程阻肩,從一開始的業(yè)務(wù) sql 查詢,到返回 rs AOP 處理,到判斷是否有翻譯器烤惊,到獲取緩存數(shù)據(jù)乔煞,到翻譯 code,再到翻譯完成柒室,返回到 rs AOP 處理位置渡贾,返回到 servcie,整個(gè)流程雄右,就講完了空骚。

5.結(jié)尾

緩存翻譯這個(gè)功能,是 sqltoy 框架最吸引我的地方擂仍,它的便捷性囤屹、性能、sql 簡(jiǎn)化和設(shè)計(jì)逢渔,都讓我著迷牺丙。

這里我建議大家把 sqltoy 的源碼 clone 下來,仔細(xì)看看复局,會(huì)看到很多我們用起來沒注意的小細(xì)節(jié),能幫助我們更好的理解 sqltoy粟判。

最后亿昏,如果我的文章有幫助到大家或者認(rèn)為寫的不錯(cuò),請(qǐng)分享給更多人档礁,謝謝角钩。

如文章有錯(cuò)誤地方,歡迎大家留言或私信(boyguhui@qq.com)呻澜,告知我递礼,感激不盡。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末羹幸,一起剝皮案震驚了整個(gè)濱河市脊髓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌栅受,老刑警劉巖将硝,帶你破解...
    沈念sama閱讀 212,029評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異屏镊,居然都是意外死亡依疼,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,395評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門而芥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來律罢,“玉大人,你說我怎么就攤上這事棍丐∥蠹” “怎么了沧踏?”我有些...
    開封第一講書人閱讀 157,570評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)稀余。 經(jīng)常有香客問我悦冀,道長(zhǎng),這世上最難降的妖魔是什么睛琳? 我笑而不...
    開封第一講書人閱讀 56,535評(píng)論 1 284
  • 正文 為了忘掉前任盒蟆,我火速辦了婚禮,結(jié)果婚禮上师骗,老公的妹妹穿的比我還像新娘历等。我一直安慰自己,他們只是感情好辟癌,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,650評(píng)論 6 386
  • 文/花漫 我一把揭開白布寒屯。 她就那樣靜靜地躺著,像睡著了一般黍少。 火紅的嫁衣襯著肌膚如雪寡夹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,850評(píng)論 1 290
  • 那天厂置,我揣著相機(jī)與錄音菩掏,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的从绘。 我是一名探鬼主播,決...
    沈念sama閱讀 39,006評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼瞧栗,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了海铆?” 一聲冷哼從身側(cè)響起迹恐,我...
    開封第一講書人閱讀 37,747評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎卧斟,沒想到半個(gè)月后系草,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,207評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡唆涝,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,536評(píng)論 2 327
  • 正文 我和宋清朗相戀三年找都,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片廊酣。...
    茶點(diǎn)故事閱讀 38,683評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡能耻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情晓猛,我是刑警寧澤饿幅,帶...
    沈念sama閱讀 34,342評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站戒职,受9級(jí)特大地震影響栗恩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜洪燥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,964評(píng)論 3 315
  • 文/蒙蒙 一磕秤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧捧韵,春花似錦市咆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,772評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至芒篷,卻和暖如春搜变,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背针炉。 一陣腳步聲響...
    開封第一講書人閱讀 32,004評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工痹雅, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人糊识。 一個(gè)月前我還...
    沈念sama閱讀 46,401評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像摔蓝,于是被迫代替她去往敵國(guó)和親赂苗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,566評(píng)論 2 349