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

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

作者也一直在推廣該框架,讓更多人了解 sqltoy,sqltoy 的文檔完善,對開發(fā)者友好,上手特別簡單,我寫一些自己玩的項(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 的介紹我從開源倉庫上截取了一部分

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

支持以下數(shù)據(jù)庫:
- 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á)夢數(shù)據(jù)庫
- 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)更新檢測,開發(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é)合緩存減少查詢語句表關(guān)聯(lián),極大簡化sql和提升性能。
2.2.3 最強(qiáng)大的分頁查詢:很多人第一次了解到何為快速分頁、分頁優(yōu)化這種極為巧妙的處理,還有在count語句上的極度優(yōu)化。
2.2.3 跨數(shù)據(jù)庫函數(shù)方言替換,如:isnull/ifnull/nvl、substr/substring 等不同數(shù)據(jù)庫

當(dāng)然這只是sqltoy其中的五個(gè)特點(diǎn),還有行列轉(zhuǎn)換(俗稱數(shù)據(jù)旋轉(zhuǎn))、多級分組匯總、統(tǒng)一樹結(jié)構(gòu)表(如機(jī)構(gòu))查詢、分庫分表sharding、取隨機(jī)記錄、取top記錄、修改并返回記錄、慢sql提醒等這些貼合項(xiàng)目應(yīng)用的功能, 當(dāng)你真正了解上述特點(diǎn)帶來的巨大優(yōu)勢之后,您就會(huì)對中國人創(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)查詢,極大簡化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 語句上配置 translate 緩存翻譯功能就行了,code 編碼則會(huì)自動(dòng)轉(zhuǎn)化為名稱。

是不是簡化了 sql,至于效率也不用擔(dān)心,首先在 sql 層面省去了多表關(guān)聯(lián),code 翻譯的結(jié)果值是從緩存中獲取,效率更高。總的來看,提高了效率、簡化了 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 在底層對查詢結(jié)果進(jìn)行統(tǒng)一替換處理。

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

3.1.1 加載入口

org.sagacity.sqltoy.SqlToyContext 文件就是 sqltoy 框架加載主入口,這里加載的配置有:sqlToy 配置解析插件、實(shí)體對象管理器、翻譯器插件、緩存管理器、統(tǒng)一公共字段賦值處理、延時(shí)檢測時(shí)長、數(shù)據(jù)庫方言參數(shù)、es的地址配置等等。然后翻譯器插件就是緩存翻譯。

image

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

image

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

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

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

parseTranslateConfig 主要功能有:設(shè)置緩存的存儲(chǔ)地址、內(nèi)存大小、過期時(shí)間、sql 語句加載、sql 語句參數(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 類型的緩存翻譯,是最基本的使用,也是作者在文檔中公開的使用方式,使用的方式也最簡單、廣泛,適用于絕大部分翻譯場景。

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

第一步,先在緩存翻譯的 xml 中,書寫好緩存值的 sql 語句。例如我要緩存員工的姓名,員工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ù)庫數(shù)據(jù)源(非必填)。

第二步,在需要翻譯的業(yè)務(wù) sql 語句上,配置緩存翻譯功能,并指定使用的緩存名稱、需要翻譯的 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:一般針對數(shù)據(jù)字典,提供一個(gè)分類條件過濾
  • columns:sql中的查詢字段名稱,可以逗號分隔對多個(gè)字段進(jìn)行翻譯
  • cache-indexs:緩存數(shù)據(jù)名稱對應(yīng)的列,不填則默認(rèn)為第二列(從0開始,1則表示第二列),例如緩存的數(shù)據(jù)結(jié)構(gòu)是:key、name、fullName,則第三列表示全稱

然后我們測試一下結(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("測試失敗-性別",vo.getSexType(),"男");
   Assert.assertEquals("測試失敗-職位類別",vo.getPost(),"管理崗");
   Assert.assertEquals("測試失敗-職位等級",vo.getPostGrade(),"L10");
   System.out.printf("性別:%s,職位:%s,職位等級:%s",vo.getSexType(),vo.getPost(),vo.getPostGrade());
}

測試結(jié)果:


image

可以看到,斷言測試通過,并且打印的日志顯示,翻譯的結(jié)果成功,成功把性別、職位類別、職位等級等編碼翻譯為中文。

3.2.2 service 緩存翻譯

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

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

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

思考一番過后,說下我認(rèn)為 service 類型的使用場景,在微服務(wù)系統(tǒng)中,A、B 兩個(gè)系統(tǒng)是分開的,分別使用各自的庫 A 庫和 B 庫,這個(gè)時(shí)候,我在 A 系統(tǒng)中查詢一些業(yè)務(wù)數(shù)據(jù),其中某個(gè)字段的編碼所對應(yīng)的 value 值并不在 A 庫中,而是 B 庫,這種場景下,數(shù)據(jù)是跨庫的,無法關(guān)聯(lián)查詢,只能在業(yè)務(wù)代碼中進(jìn)行 B 庫數(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ù)的場景。

第一步,先把緩存方法書寫好。依然是把員工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[]>。

找到對應(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>

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

test 層

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

測試結(jié)果:


image

斷言測試通過,并且打印的日志顯示,翻譯的結(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ā)起請求,獲取所需要的緩存值或字典數(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 緩存翻譯的使用場景,假設(shè)我公司有兩個(gè)單體應(yīng)用 A 和 B,A 和 B 各自有各自的服務(wù)器、數(shù)據(jù)庫、nginx,如果 A 需要調(diào)用 B 的數(shù)據(jù)字典,來翻譯自己的數(shù)據(jù)里的某個(gè)字段。

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

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

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

第一步,在緩存文件中,配置 rest 緩存。

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

用戶名和密碼兩個(gè)屬性,看請求的接口,接口需要進(jìn)行身份認(rèn)證,就需要加上,不需要認(rèn)證,則可以沒有。

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

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

image

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

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

在 TranslateFactory 類下有 getCacheData 方法

image

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

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

image

重點(diǎn)看下 332 行,這一行,拿到了請求的 url、username、password、請求參數(shù) Key、請求參數(shù) Value 等信息,通過封裝的 HttpClient 進(jìn)行發(fā)起了接口請求。

332 行之后的代碼就是對接口響應(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è)置請求參數(shù),請求參數(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ù) = 號切割,獲取 value 值,這個(gè) value 值就是 rest 請求帶過來字典參數(shù)。

整個(gè) rest 配置流程和底層源碼都講完了,我們測試一遍,看看翻譯功能對不對。我這里的測試,是把測試的服務(wù),打成 jar 包,用 8082 端口啟動(dòng),模擬 A、B 兩個(gè)單體應(yīng)用,下面是我的測試結(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í)檢測緩存是否更新程序、緩存刷新檢測、緩存翻譯器、緩存相關(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 解析對象,里面有 sql 語句屬性、參數(shù) key、參數(shù) value、數(shù)據(jù)庫方言、翻譯器、脫敏配置等,其實(shí)就是 sql.xml 解析出來的對象,里面包含當(dāng)前 sql 的各種配置。

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

image

方法的 750 - 755 行,操作的是把 sql 中的參數(shù) key 替換為占位符號,并且把參數(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ù)庫,所以我進(jìn)入的是 Mysql 的實(shí)現(xiàn)方法。

image

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

image

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

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

到這里就能知道我們的結(jié)果集為什么會(huì)進(jìn)行 code 編碼翻譯,翻譯為中文名稱,因?yàn)榭蚣艿讓訉?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)榭?,則通過 TranslateFactory.getCacheData()方法去數(shù)據(jù)庫獲取數(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)榭?,則把 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 的長度 ) 的循環(huán)當(dāng)前值。

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

image

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

cacheValues 是根據(jù)翻譯的 code 編碼獲取到對應(yīng)的緩存對象,如果沒有獲取到,則代表沒查到這個(gè) code 編碼的中文信息,打印錯(cuò)誤日志。對象存在,就根據(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 簡化和設(shè)計(jì),都讓我著迷。

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

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容